Overlay

A floating layer anchored by the browser and rendered in its top layer — so it escapes overflow: hidden, clipped and transformed ancestors and the z-index wars, and flips when there is no room.
Import
JSX
import Overlay from '@box-kite/react/components/overlay';

A layer, not a pattern

Overlay owns no open state, no ARIA and no dismissal: it names an anchor, asks the browser to put the layer on a side of it, and renders it in the browser's top layer. That is all every popup in this library shares — Tooltip, Dropdown and the DataGrid menu each add a different pattern on top. If what you are rendering describes a control, reach for Tooltip instead: it adds role="tooltip", the aria-describedby wiring, hover-and-focus open and Escape.
The placement is CSS anchor positioning — useAnchorPosition under the hood, so on a browser that has it nothing runs at all: no measurement, no scroll listener, no state. The top layer answers the other half of the problem. position: fixed escapes every overflow: hidden ancestor but neither a transformed one nor the page's stacking order, and a layer has to come out on top of both.
The problem it solves
(position absolute)
overflow hidden box
(overlay)
overflow hidden box
JSX
function Component() {
  const [openAbsolute, setOpenAbsolute] = useState(false);
  const [openOverlay, setOpenOverlay] = useState(false);

  return (
    <Flex gap={4} flexWrap="wrap">
      {/* position: absolute — clipped by the scrolling parent */}
      <Box flex1 height={40} b={1} borderRadius={1} overflow="auto" position="relative" minWidth={80}>
        <Flex ml={4}>
          <Button onClick={() => setOpenAbsolute(!openAbsolute)} position="relative" width={30}>
            Click me!
            {openAbsolute && (
              <Box position="absolute" left={0} top={12} height={50} p={3} b={1} borderRadius={2}>
                position absolute box
              </Box>
            )}
          </Button>
        </Flex>
      </Box>

      {/* Overlay — in the top layer, so nothing clips it */}
      <Box flex1 height={40} b={1} borderRadius={1} overflow="auto" position="relative" minWidth={80}>
        <Flex ml={4}>
          <Box>
            <Button onClick={() => setOpenOverlay(!openOverlay)} display="block" width={30}>
              Click me!
            </Button>
            {openOverlay && (
              <Overlay height={50} borderRadius={2} p={3} offset={0.5} b={1}>
                overlay box
              </Overlay>
            )}
          </Box>
        </Flex>
      </Box>
    </Flex>
  );
}

The top layer, and the one rule that comes with it

The layer carries popover="manual" and is shown the moment it mounts, which puts it in the browser's top layer: it paints over every stacking context and out of every clipped and transformed ancestor, which position: fixed alone cannot do. Measured in Chrome 152 against a sibling with z-index: 9999, which covers a plain fixed layer at the same coordinates and loses to this one. manual rather than auto because a layer owns no dismissal — Popover is the auto one, with light dismiss and focus return from the browser.
Because nothing is moved, the layer inherits the theme, the custom properties and the text direction around it, and the tab order runs trigger then layer then the rest of the page. It also means a press inside the layer is a press inside whatever popover it was declared in, so a Dropdown inside a Popover panel no longer dismisses the panel — the bug a portal caused by moving the popup to the end of the body, where every press in it read as outside.
The rule: never declare a layer inside its trigger. A portal used to move it before a browser ever saw it, so <Button>{overlay}</Button> worked by accident. It stays where you put it now, and a role="listbox" or role="menu" full of buttons inside a button is content no keyboard can reach. Put the layer beside the trigger and give it anchor.
The one cost: the layer keeps the side it chose when it opened. Chrome re-evaluates position-try-fallbacks on scroll for an ordinary positioned element and never for one in the top layer, so a layer left open while the page scrolls does not flip when its side runs out of room — it slides past the viewport edge instead. Measured in Chrome 152, against an otherwise identical element outside the top layer, which does flip. Every open picks the right side, because a layer that mounts when it opens is laid out for the first time then; only a scroll while open is affected. Nothing but leaving and re-entering the top layer re-arms the browser, so close a layer if the page can scroll far underneath it. Popover shares this; the portal fallback, which measures, does not.
Where the browser has no Popover API the layer is portalled into #box-kite-portal instead, with everything a portal costs — including the direction, which is measured off the anchor and written back on as dir. Mounting is still yours (or Presence's): a closed dropdown renders none of its options, and an exit transition runs in the top layer like anywhere else.

Where it goes

  • anchor — the element to hang off. A trigger is almost always the right answer; with none, the layer anchors to the spot it was declared in, which is a real box with no size, so align and matchWidth have nothing to work from.
  • side / align / offset — the placement vocabulary Tooltip and useAnchorPosition share. side is top/bottom on the block axis or start/end on the inline one, so a layer beside its anchor mirrors in a right-to-left page; offset is the ÷4 scale.
  • flip — on by default, so a side with no room is swapped for its opposite by the browser. It shadows the CSS prop of that name.
  • onSideChange — the side it ended up on, which is how the dropdown popup knows to animate upwards instead. Asking for it turns on the one read the CSS path otherwise never does.
  • matchWidth — on by default: the layer is at least as wide as its anchor (anchor-size(width), measured nowhere), which is what lines a dropdown popup up with its trigger. Turn it off for content that should size to itself.
  • Every Box prop, applied to the layer's content.

Overlay props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on /box rather than repeated here.
PropTypeDefaultWhat it does
sideAnchorSide'bottom'Which side of the anchor the layer sits on: top/bottom are the block axis, start/end the inline one, so a layer beside its anchor mirrors in a right-to-left page. Default 'bottom'.
alignAnchorAlign'center'Which of the anchor's edges to line up with along the other axis. Default 'center'.
offsetnumber0The gap between anchor and layer, on the ÷4 spacing scale — offset={2} is 8px. Default 0.
flipbooleantrueWhether a side with no room may be swapped for its opposite. Default true. It shadows the CSS prop of that name — a mirrored layer is a rarity, and one placement vocabulary across the hook, this and Tooltip is worth more than the transform, which a child of the layer can still take.
onSideChange?(side: AnchorSide): void;—Fires with the side the layer ended up on, which is the requested one unless a flip moved it — what a popup that grows away from its trigger needs to know to animate the right way. Asking for it is what turns on the one read the CSS path otherwise never does, so leave it out when nothing uses it.
anchorElementLike—Anchor to this element rather than to the spot the layer was declared in. A trigger is almost always the right answer: the placeholder is a real box with no size, so align and matchWidth have nothing to work from, and inside a flex row it becomes a flex item of its own.
contentRefRef<HTMLDivElement>—The content Box, which is the one that animates. ref is the anchored layer, which transitions nothing on purpose — so a <Presence> measuring an exit has to reach past it.
matchWidthbooleantrueWhether the layer is at least as wide as its anchor. Default true, so a dropdown popup lines up with its trigger; a tooltip sizes to its own content and turns it off.

Overlay accessibility

  • No role, no aria-* and no focus handling: whatever renders a layer owns the pattern, and a layer given a role it does not implement is worse than one with none.
  • The layer stays in the DOM where it was declared, so the order a screen reader reads and the tab order both follow the markup — a layer declared after its trigger is reached by Tab from it. It must therefore not be declared *inside* its trigger: interactive content in a <button> is unreachable.
  • Only on the portal fallback does it leave that subtree, and there it carries the direction it was read in as a dir of its own, since a container hanging off the body inherits nothing.
Swept with axe on every release, in this state: Overlay. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.