Overlay
Popover
A panel anchored to a trigger. This is the overlay mechanism the rest of the library copies — the menu, the tooltip and the select each carry their own copy of it, on purpose.
Usage
import { Popover } from '@carabine/ui/popover';It grows out of the edge it lands on. transform-origin at the trigger's corner is the difference between a panel that came from the button and one that was switched on nearby.
Placement
Three steps, in order. It tries the side it was asked for; it flips to the opposite side only when that side genuinely has more room, rather than the moment the first one is tight; and then it slides along the other axis to stay inside the window, keeping at least offset from every edge.
The spike is cut into the panel's outline rather than drawn as a rotated square behind it — one SVG path for the whole shape, notch included. A rotated square needs its own border, its own shadow and its own background to match the panel's, and on glass it never does.
Examples
Every side
Customization
<Popover
placement="right"
align="start"
gap={8} // between the trigger and the panel
offset={12} // smallest distance kept from the window's edges
width={280} // left off, the panel is as wide as its content
padding={16}
radius={16}
arrow={false} // no spike
arrowSize={14} // base width; it protrudes by half of this
theme="light"
/>place() and shape()
Both are exported, because the arithmetic is useful on its own — an anchored menu of your own, a custom overlay, anything that has a box and needs somewhere to put another one.
import { place, origin, shape } from '@carabine/ui/popover';
place({
anchor: { left, top, width, height },
panel: { width, height },
viewport: { width, height },
side: 'bottom',
align: 'center',
gap: 8,
offset: 12,
}); // → { left, top, side }
origin('bottom', 'center'); // → the matching transform-origin
shape({ width: 200, height: 120, radius: 16, side: 'top', arrowSize: 14, arrowAt: 100 });There are four copies of this arithmetic in the library and that is deliberate. The fourth copy is cheaper than the edge that would let one component's placement bug become four components' regression.
Styling reference
A single-layer panel is a core, not a shell. It was built as a shell first, and the result was that you could see the page through a popover twice as much as through a toast — 60% against an effective 94%, on two surfaces meant to be the same material. Whichever layer the text lands on carries the core's weight.
API reference
| Prop | Type | |
|---|---|---|
| className | string | Added to the panel’s own classes, so yours wins |
| style | CSSProperties | Merged after the component’s own inline styles, so yours wins |
Both land on the panel. The trigger is already your element — you style it where you write it — and the panel is the part that portals away from your markup and is otherwise out of reach. State is legible from CSS as well — data-state, data-disabled, data-side — so a rule can answer it without knowing a single class name of ours. See Theming.
| Prop | Type | Default | |
|---|---|---|---|
| children | ReactNode | — | The trigger |
| content | ReactNode | — | What the panel holds |
| open | boolean | — | Controlled |
| defaultOpen | boolean | false | |
| onOpenChange | (open: boolean) => void | — | |
| placement | Side | 'bottom' | Preferred side; flips only if the other is better |
| align | Align | 'center' | Where it sits along that side |
| gap | number | 8 | Between the trigger and the panel |
| offset | number | 12 | Smallest distance kept from the viewport’s edges |
| width | number | — | Left off, the panel is as wide as its content |
| padding | number | 12 | Inside. The spike’s depth is added on its edge |
| radius | number | 16 | Corner radius, and how close the spike can get to one |
| arrow | boolean | true | The spike pointing at the trigger |
| arrowSize | number | 14 | Base width. It protrudes by half of this |
| zIndex | number | 50 | |
| container | HTMLElement | null | document.body | Where it portals to |
| stiffness | number | 420 | |
| damping | number | 34 | |
| openScale | number | 0.94 | Scale the panel grows from |
| label | string | 'Popover' | Accessible name of the panel |
| theme | 'dark' | 'light' | 'dark' |