# Anchor Positioning

_Box Kite 2.1.0 · a markdown copy of https://www.box-kite.dev/anchor/_

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**

```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`, `useFocusReturn` and `useRovingFocus` from `@box-kite/react/a11y`.
- **Not a portal.** The layer is `position: fixed`, so it escapes every `overflow: hidden` ancestor without one — but not a _transformed_ ancestor, which is a fixed element's containing block, and not the page's stacking order. `Overlay` is 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 — as `Tooltip`, the `Dropdown` popup 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.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

