Overlay Components
Popup
A floating element anchored to a trigger element using CSS Anchor Positioning. Supports 12 placement options, width matching, and click-outside dismissal.
A floating element anchored to a trigger element using CSS Anchor Positioning. Supports 12 placement options, width matching, and click-outside dismissal.
Popup ships its own surface defaults: md, flex column, padding, gap, rounded, border, shadow, primary, outline, wFit, maxHeight, overflowAuto, bottom. Render children directly — wrapping content in another Card is usually redundant.
Browser support: Popup uses the CSS Anchor Positioning API (Chrome 125+, Edge 125+). Other browsers fall back to a JS positioning path that recomputes on scroll/resize.
Basic Popup
A controlled floating element anchored to a button. Closes on outside click or Escape. Pass an anchorRef pointing at the trigger element.
const [open, setOpen] = useState(false);const anchorRef = useRef<HTMLButtonElement>(null);
<Button ref={anchorRef} onClick={() => setOpen(!open)}>Toggle</Button><Popup open={open} onClose={() => setOpen(false)} anchorRef={anchorRef}> <Text>Popup content</Text></Popup>Uncontrolled with defaultOpen
Skip open/onClose and let Popup manage its own state. Use onOpenChange to react to opens/closes without owning the state.
const anchorRef = useRef<HTMLButtonElement>(null);
<Button ref={anchorRef}>Trigger</Button><Popup defaultOpen anchorRef={anchorRef} onOpenChange={(o) => console.log(o)}> <Text>Uncontrolled popup</Text></Popup>PopupTrigger (Click)
PopupTrigger is a convenience wrapper that manages open/close state and ref wiring automatically. No useState or useRef needed — wrap the trigger element and provide popup content via the popup prop. Default trigger mode is "click".
<PopupTrigger popup={<Text sm>Click-triggered popup</Text>}> <Button>Click Me</Button></PopupTrigger>PopupTrigger (Hover)
Set triggerOnHover to show on mouse enter and hide on mouse leave. openDelay (default 0) delays appearance; closeDelay (default 150) delays dismissal so the user can move the cursor over the popup. Keyboard focus also opens it for accessibility.
<PopupTrigger triggerOnHover openDelay={200} popup={<Text sm>Tooltip text</Text>}> <Button>Hover Me</Button></PopupTrigger>PopupTrigger (Focus)
Set triggerOnFocus to show on focus and hide on blur. Useful for search autocomplete, input hints, and dropdown suggestions.
<PopupTrigger triggerOnFocus popup={<Text sm>Search suggestions...</Text>}> <Input placeholder="Search..." /></PopupTrigger>All 12 Placements
Placement is set via boolean props: top, topStart, topEnd, bottom, bottomStart, bottomEnd, left, leftStart, leftEnd, right, rightStart, rightEnd. Exactly one wins (first-truthy per category). Default is bottom.
const ref = useRef<HTMLButtonElement>(null);const [open, setOpen] = useState(false);
<Button ref={ref} onClick={() => setOpen(!open)}>Anchor</Button>
{/* Above the anchor, aligned to the anchor's left edge */}<Popup topStart open={open} onClose={() => setOpen(false)} anchorRef={ref}> <Text>Top Start</Text></Popup>
{/* Centered to the right of the anchor */}<Popup right open={open} onClose={() => setOpen(false)} anchorRef={ref}> <Text>Right</Text></Popup>
{/* Below the anchor, aligned to the anchor's right edge */}<Popup bottomEnd open={open} onClose={() => setOpen(false)} anchorRef={ref}> <Text>Bottom End</Text></Popup>When the requested placement would overflow the viewport, Popup falls back through flip-block → flip-inline → shift → clamp and exposes the resolved placement via the data-placement attribute.
Match Anchor Width
matchWidth makes the popup track the anchor's width — useful for select-like dropdowns. Combine with wFit removal (Popup defaults to wFit) is automatic when matchWidth is set.
const [open, setOpen] = useState(false);const anchorRef = useRef<HTMLButtonElement>(null);
<Button ref={anchorRef} onClick={() => setOpen(!open)} wFull>Select option</Button><Popup matchWidth open={open} onClose={() => setOpen(false)} anchorRef={anchorRef}> <Text>Same width as the anchor</Text></Popup>Rich Content
Children render directly inside the Popup surface — use layout primitives (Col, Row, Stack) to compose multi-element content. Avoid nesting another Card inside; Popup already provides the bordered, padded, shadowed surface.
const [open, setOpen] = useState(false);const anchorRef = useRef<HTMLButtonElement>(null);
<Button ref={anchorRef} onClick={() => setOpen(!open)}>Account</Button><Popup open={open} onClose={() => setOpen(false)} anchorRef={anchorRef}> <Text bold>User Menu</Text> <Divider /> <Button>Profile</Button> <Button danger>Sign Out</Button></Popup>Tooltip Pattern
Pair triggerOnHover with arrow for a classic tooltip. The arrow auto-rotates to match the resolved placement.
<PopupTrigger triggerOnHover openDelay={300} popupProps={{ top: true, arrow: true, role: "tooltip", sm: true }} popup={<Text sm>Save your changes to the server</Text>}> <Button>Save</Button></PopupTrigger>Dropdown via Popup
Popup is a low-level primitive. For action menus with keyboard navigation, focus management, and item semantics, prefer the dedicated Menu component — it wraps Popup and adds MenuItem/MenuLabel with full arrow-key navigation. Reach for raw Popup only when you need a non-menu surface (filter panels, custom autocompletes, info cards).
<PopupTrigger popup={ <> <Button transparent justifyStart>Edit</Button> <Button transparent justifyStart>Duplicate</Button> <Divider /> <Button transparent justifyStart danger>Delete</Button> </>}> <Button>Actions</Button></PopupTrigger>Arrow Indicator
Set arrow to render a pointer that visually links the popup to its anchor. The arrow is positioned automatically based on the resolved placement.
const [open, setOpen] = useState(false);const anchorRef = useRef<HTMLButtonElement>(null);
<Button ref={anchorRef} onClick={() => setOpen(!open)}>Toggle Arrow Popup</Button><Popup arrow open={open} onClose={() => setOpen(false)} anchorRef={anchorRef}> <Text sm>Arrow points at the anchor.</Text></Popup>Click-Outside Behavior
By default, clicking anywhere outside the popup or anchor closes it. Set closeOnClickOutside={false} to require an explicit dismiss (e.g., a close button inside the popup). closeOnEscape={false} similarly disables the keyboard dismiss.
const [open, setOpen] = useState(false);const anchorRef = useRef<HTMLButtonElement>(null);
<Button ref={anchorRef} onClick={() => setOpen(true)}>Open Sticky Popup</Button><Popup open={open} onClose={() => setOpen(false)} anchorRef={anchorRef} closeOnClickOutside={false} closeOnEscape={false}> <Text>Stays open until dismissed.</Text> <Button sm onClick={() => setOpen(false)}>Close</Button></Popup>Disabled Trigger
Set disabled on Popup (or PopupTrigger) to suppress opening regardless of open state. Useful for conditional UI where the trigger remains rendered but inactive.
<PopupTrigger disabled popup={<Text>Hidden</Text>}> <Button disabled>Cannot open</Button></PopupTrigger>Advanced Props
Popup supports additional configuration props:
| Prop | Default | Description |
|---|---|---|
offset | 4 | Distance from anchor in pixels |
closeOnEscape | true | Close on Escape key press |
closeOnClickOutside | true | Close when clicking outside the popup or anchor |
portal | true | Render via portal into document.body |
keepMounted | false | Keep DOM node mounted when closed |
noAnimation | false | Disable enter/exit transitions |
transitionDuration | 200 | Animation duration in ms |
disabled | false | Prevent popup from opening |
hideWhenDetached | false | Hide when anchor scrolls out of view (uses IntersectionObserver) |
role | "dialog" | ARIA role (set "tooltip" for tooltip patterns) |
arrow | false | Render arrow pointer |
onEnterComplete | — | Called when the enter transition finishes |
onExitComplete | — | Called when the exit transition finishes |
PopupTrigger additionally accepts:
| Prop | Default | Description |
|---|---|---|
triggerOnClick | true (when no other trigger set) | Toggle on click |
triggerOnHover | false | Open on mouse enter / focus |
triggerOnFocus | false | Open on focus only |
openDelay | 0 | ms before opening on hover |
closeDelay | 150 | ms before closing on hover-out |
popupProps | — | Props forwarded to the internal Popup |
popupId | auto | Override generated id for aria-controls |
const [open, setOpen] = useState(false);const anchorRef = useRef<HTMLButtonElement>(null);
<Button ref={anchorRef} onClick={() => setOpen(!open)}>Custom Config</Button><Popup open={open} onClose={() => setOpen(false)} anchorRef={anchorRef} offset={8} noAnimation closeOnEscape={false}> <Text>Custom config</Text></Popup>Popup Props
| Prop | Category | Default | Description |
|---|---|---|---|
accent | Appearance | Accent color appearance (rose) | |
brand | Appearance | Brand color appearance (blue) | |
danger | Appearance | Danger color appearance (red) | |
info | Appearance | Info color appearance (cyan) | |
inherit | Appearance | Inherit appearance from parent — suppresses own data-appearance/data-variant, uses parent's CSS variables | |
link | Appearance | Link color appearance (blue, for hyperlinks) | |
primary | Appearance | ✓ | Primary color appearance (gray) |
secondary | Appearance | Secondary color appearance (gray) | |
success | Appearance | Success color appearance (green) | |
tertiary | Appearance | Tertiary color appearance | |
warning | Appearance | Warning color appearance (amber) | |
flex1 | Flex | Take up remaining space (= `flex-1`, i.e. `flex: 1 1 0%`) | |
flexAuto | Flex | Grow but respect intrinsic size (= `flex-auto`, i.e. `flex: 1 1 auto`) | |
flexNone | Flex | Don't grow and don't shrink (= `flex-none`, i.e. `flex: none`) | |
heading | Font Family | Heading font family (defaults to sans, independently customizable via --font-heading CSS variable) | |
mono | Font Family | Monospace font family | |
sans | Font Family | Sans-serif font family (default) | |
serif | Font Family | Serif font family | |
italic | Font Style | Italic font style | |
notItalic | Font Style | Not italic (normal) font style | |
black | Font Weight | Black font weight (900) | |
bold | Font Weight | Bold font weight (700) | |
extrabold | Font Weight | Extra bold font weight (800) | |
extralight | Font Weight | Extra light font weight (200) | |
light | Font Weight | Light font weight (300) | |
medium | Font Weight | Medium font weight (500) | |
normal | Font Weight | Normal font weight (400) | |
semibold | Font Weight | Semibold font weight (600) | |
thin | Font Weight | Thin font weight (100) | |
noPadding | Padding | Disable internal padding | |
padding | Padding | ✓ | Enable internal padding |
paddingX | Padding | Enable only horizontal padding | |
paddingY | Padding | Enable only vertical padding | |
bottom | Placement | ✓ | Position below anchor, centered horizontally |
bottomEnd | Placement | Position below anchor, aligned to end (right) | |
bottomStart | Placement | Position below anchor, aligned to start (left) | |
left | Placement | Position to the left of anchor, centered vertically | |
leftEnd | Placement | Position to the left of anchor, aligned to bottom | |
leftStart | Placement | Position to the left of anchor, aligned to top | |
right | Placement | Position to the right of anchor, centered vertically | |
rightEnd | Placement | Position to the right of anchor, aligned to bottom | |
rightStart | Placement | Position to the right of anchor, aligned to top | |
top | Placement | Position above anchor, centered horizontally (default) | |
topEnd | Placement | Position above anchor, aligned to end (right) | |
topStart | Placement | Position above anchor, aligned to start (left) | |
pill | Shape | Fully rounded corners (circular) | |
pill | Shape | Fully rounded corners (circular) | |
rounded | Shape | ✓ | Medium rounded corners (default) |
rounded | Shape | ✓ | Medium rounded corners (default) |
sharp | Shape | No rounded corners (square) | |
sharp | Shape | No rounded corners (square) | |
noShrink | Shrink | Prevent the flex item from shrinking below its content size (= `shrink-0`) | |
lg | Size | Large size | |
md | Size | ✓ | Medium size (default) |
sm | Size | Small size | |
xl | Size | Extra large size | |
xs | Size | Extra small size | |
textCenter | Text Align | Align text to center | |
textJustify | Text Align | Justify text | |
textLeft | Text Align | Align text to left | |
textRight | Text Align | Align text to right | |
lineThrough | Text Decoration | Add strikethrough/line-through decoration across text | |
noUnderline | Text Decoration | Remove text decoration (no underline, strikethrough, etc.) | |
overline | Text Decoration | Add overline decoration above text | |
underline | Text Decoration | Add underline decoration below text | |
capitalize | Text Transform | Capitalize first letter of each word | |
lowercase | Text Transform | Transform text to lowercase | |
normalCase | Text Transform | Normal text case (no transformation) | |
uppercase | Text Transform | Transform text to uppercase | |
lineClamp2 | Truncate | Truncate at 2 lines with ellipsis | |
lineClamp3 | Truncate | Truncate at 3 lines with ellipsis | |
lineClamp4 | Truncate | Truncate at 4 lines with ellipsis | |
lineClamp5 | Truncate | Truncate at 5 lines with ellipsis | |
noTruncate | Truncate | Remove truncation | |
truncate | Truncate | Single line truncation with ellipsis | |
filled | Variant | Filled variant - solid background with contrasting text color | |
ghost | Variant | Ghost variant - transparent background, no border, appearance-colored text, tinted hover background | |
outline | Variant | ✓ | Outline variant - transparent background with border and colored text (default) |
Layout & utility props (gap, padding, hide, items, justify, ...) — documented on Common Props
| Prop | Category | Default | Description |
|---|---|---|---|
border | Border | ✓ | Enable border on all sides |
borderB | Border | Enable border on bottom | |
borderL | Border | Enable border on left | |
borderR | Border | Enable border on right | |
borderT | Border | Enable border on top | |
borderX | Border | Enable border on left and right | |
borderY | Border | Enable border on top and bottom | |
noBorder | Border | Disable all borders | |
block | Display | Block display - takes full width, new line | |
contents | Display | Contents display - element's box is removed, children display as if parent didn't exist | |
flex | Display | ✓ | Flex display - flexbox container |
grid | Display | Grid display - CSS grid container | |
hidden | Display | Hidden display - element is not visible | |
inline | Display | Inline display - flows with text | |
inlineBlock | Display | Inline-block display - inline but with block properties | |
inlineFlex | Display | Inline-flex display - inline flexbox container | |
inlineGrid | Display | Inline-grid display - inline grid container | |
table | Display | Table display - behaves like table element | |
tableCell | Display | Table-cell display - behaves like td element | |
column | Flex Direction | ✓ | Flex direction column (vertical) |
columnReverse | Flex Direction | Flex direction column-reverse | |
row | Flex Direction | Flex direction row (horizontal) | |
rowReverse | Flex Direction | Flex direction row-reverse | |
gap | Gap | ✓ | Enable gap spacing between children |
noGap | Gap | Disable gap spacing | |
hAuto | Height | Set height to auto | |
hFit | Height | Set height to fit-content | |
hFull | Height | Set height to 100% | |
hScreen | Height | Set height to 100vh (viewport height), removes max-height constraint | |
desktopHide | Hide | Hide element on desktop devices and below (max-desktop: 80rem) | |
mobileHide | Hide | Hide element on mobile devices and below (max-mobile: 48rem) | |
tabletHide | Hide | Hide element on tablet devices and below (max-tablet: 64rem) | |
itemsBaseline | Items | Align items to baseline | |
itemsCenter | Items | Align items to center | |
itemsEnd | Items | Align items to end (bottom/right) | |
itemsStart | Items | Align items to start (top/left) | |
itemsStretch | Items | Stretch items to fill container | |
justifyAround | Justify | Distribute items with space around them | |
justifyBaseline | Justify | Align items along their baseline on main axis | |
justifyBetween | Justify | Distribute items with space between them | |
justifyCenter | Justify | Center items along the main axis | |
justifyEnd | Justify | Pack items toward the end of the main axis | |
justifyEvenly | Justify | Distribute items with equal space around them | |
justifyStart | Justify | Pack items toward the start of the main axis | |
justifyStretch | Justify | Stretch items to fill the main axis | |
maxHeight | Max Height | ✓ | Apply size-dependent maximum height (uses --max-height CSS variable) |
minWidth | Min Width | Apply size-dependent minimum width (uses --popup-min-w CSS variable) | |
overflowAuto | Overflow | ✓ | Auto overflow - show scrollbars if needed |
overflowClip | Overflow | Clip overflow - hard clip without scrollbars | |
overflowHidden | Overflow | Hidden overflow - clip content without scrollbars | |
overflowScroll | Overflow | Scroll overflow - always show scrollbars | |
overflowVisible | Overflow | Visible overflow - content extends beyond bounds | |
overflowXAuto | Overflow | Auto overflow on X-axis only | |
overflowXClip | Overflow | Clip overflow on X-axis only | |
overflowXHidden | Overflow | Hidden overflow on X-axis only | |
overflowXScroll | Overflow | Scroll overflow on X-axis only | |
overflowXVisible | Overflow | Visible overflow on X-axis only | |
overflowYAuto | Overflow | Auto overflow on Y-axis only | |
overflowYClip | Overflow | Clip overflow on Y-axis only | |
overflowYHidden | Overflow | Hidden overflow on Y-axis only | |
overflowYScroll | Overflow | Scroll overflow on Y-axis only | |
overflowYVisible | Overflow | Visible overflow on Y-axis only | |
pointerEventsAuto | Pointer Events | Enable pointer events (default browser behavior) | |
pointerEventsNone | Pointer Events | Disable pointer events - clicks pass through the element | |
absolute | Position | Absolute positioning | |
fixed | Position | ✓ | Fixed positioning |
relative | Position | Relative positioning | |
static | Position | Static positioning | |
sticky | Position | Sticky positioning | |
responsive | Responsive | Enable responsive sizing - uses breakpoint-specific classes for font size, padding, and gap | |
reverse | Reverse | Reverse the order of children | |
noRing | Ring | Disable focus ring | |
ring | Ring | Enable focus ring | |
noShadow | Shadow | Disable drop shadow | |
shadow | Shadow | ✓ | Enable drop shadow |
noTransition | Transition | Disable transitions for instant state changes | |
transition | Transition | Enable smooth transitions between states | |
transparent | Transparent | Disable background color - makes component background transparent | |
wAuto | Width | Set width to auto | |
wFit | Width | ✓ | Set width to fit-content |
wFull | Width | Set width to 100% | |
wScreen | Width | Set width to 100vw (viewport width), removes max-width constraint | |
flexNoWrap | Wrap | Force flex items to stay on single line (may overflow) | |
flexWrap | Wrap | Allow flex items to wrap to new lines when container is too narrow | |
flexWrapReverse | Wrap | Wrap flex items in reverse order (last items wrap first) |