Anchor Positioning
A floating layer placed against its trigger by the browser: one hook, six props, and no measuring — with a measured fallback where the browser has none.
Import
JSX
import { useAnchorPosition } from '@box-kite/react/anchor';Placed by the browser
Spread
anchorProps on the trigger and layerProps on the layer. On a browser with CSS anchor positioning that is the whole of it: the layer carries positionArea and positionTryFallbacks, and the browser does the placing — no measuring, no scroll listener, no state, and nothing to keep in sync when the page moves.side is which side of the anchor to sit on — top and bottom are the block axis, start and end the inline one, so a layer after the trigger is on its left in a right-to-left page. align is which of the anchor's edges to line up with on the other axis.Twelve placements, one hook
side
align
bottom / center
Placed by the browser — no JavaScript ran.
JSX
const [side, setSide] = useState('bottom');
const [align, setAlign] = useState('center');
const { anchorProps, layerProps } = useAnchorPosition({ side, align, offset: 2, flip: false });
<Button {...anchorProps}>anchor</Button>
<Box {...layerProps} p={2} px={3} borderRadius={2} bgColor="indigo-500" color="white">
{side} / {align}
</Box>The gap is a margin
There is no
offset property in CSS and there is none here either: the gap between anchor and layer is an ordinary margin, on the ÷4 spacing scale, and the hook puts it on the side facing the anchor. A flip flips the margin with it — measured in Chrome 152 — so a layer that opens upwards keeps its gap instead of landing on top of its trigger.A flip is sticky: once the browser takes one it keeps it until the layer is laid out afresh, which is what stops a layer oscillating as the page scrolls. Measured in Chrome 152 — hiding the layer and showing it again re-evaluates, so a popup that mounts when it opens always picks the side that fits, while one that stays mounted keeps the side it first chose.
A menu below its button, left-aligned, 8px away
JSX
const { anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true });
<Button {...anchorProps} onClick={() => setIsOpen(!isOpen)}>Options</Button>
{isOpen && (
<Box {...layerProps} p={2} b={1} borderRadius={2} bgColor="white">
…
</Box>
)}A length off the anchor
matchWidth is one line of CSS rather than a measurement: minWidth="anchor-size(width)". Every sizing prop takes an anchor-size() value and every single-side inset prop takes an anchor() one, so a layer can be sized and placed against its anchor with no hook at all — which is what makes a floating layer possible in a Server Component.The two value families, on ordinary props
JSX
<Box position="fixed" minWidth="anchor-size(width)" maxHeight="anchor-size(height, 20rem)" top="anchor(bottom)" insetStart="anchor(left)" />Where the browser has none
CSS anchor positioning is Chrome 125+, Firefox 147+ and Safari 26+. Everywhere else the hook measures instead: it flips to the opposite side when the requested one has no room, shifts along the other axis to stay in the viewport, and hands the layer its coordinates as an inline style. Both paths come out of one model, so the fallback is the CSS placement worked out by hand rather than a second set of rules.
css in the returned object says which one is running.The first render always assumes CSS, so a server-rendered anchor and the client agree; a browser without it says so before it paints.
What it is not
- Not a pattern. No role, no dismissal, no focus handling — those are
useDismiss,useFocusReturnanduseRovingFocusfrom@box-kite/react/a11y. - Not a portal. The layer is
position: fixed, so it escapes everyoverflow: hiddenancestor without one — but not a transformed ancestor, which is a fixed element's containing block, and not the page's stacking order.Overlayis this hook plus the browser's top layer, which beats both, and is what to reach for when the layer has to come out on top of everything — asTooltip, theDropdownpopup and the DataGrid column menu all do. - Not a class. The anchor's name is an inline style, deliberately: an identity is per instance, so a class for it would be a rule per instance that is never freed.