Anchored content that opens near its trigger.
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Controlled visibility (or uncontrolled via trigger) |
| onOpenChange | (open) => void | — | Fires on trigger, outside click, or Escape |
| placement | bottom · top · right | bottom | Side the panel opens toward |
| trigger | ReactNode | — | The anchoring element; gets aria-expanded |
| title | string | — | Optional header label |
| dismissOnOutside | boolean | true | Click outside closes; the page stays live |
| Trigger | Result |
|---|---|
| Click trigger | Toggles open; a second click closes |
| Click outside | Closes; unlike a modal, no scrim blocks the page |
| Escape | Closes and returns focus to the trigger |
| Tab | Moves through the panel, then out — focus is not trapped |
A popover is non-blocking: it never dims the page and never traps focus. If the content is a list of actions, that is a menu; if a single word of help, a tooltip; if the choice must block, a modal.
Use a popover for contextual content that shouldn't block the page — filters, pickers, previews.
Don't confirm the irreversible in a popover. A stray outside click dismisses it — use a modal.
Keep the arrow. It ties the panel to its anchor, so origin is obvious at a glance.
Don't nest popovers. Layers of floating panels lose the anchor and the user with it.
Faceted filters live in a popover so results update without leaving the view.
Inline edits — one or two fields — commit right where the trigger sits.
Hover-card previews — a profile, a link unfurl — belong in a light, dismissable popover.
| Token | Value | Role |
|---|---|---|
| --pop-w | 260px | Default panel width |
| --pop-bg | #141519 | Panel surface (arrow inherits it) |
| --pop-border | #262A30 | Panel and arrow hairline |
| --pop-radius | 12px | Corner radius |
| --pop-shadow | 0 18px 48px rgba(0,0,0,.55) | Floating elevation |
| --pop-arrow | 10px, rotate 45° | Square that points to the anchor |
| --pop-offset | 10px | Gap between anchor and panel |
| --pop-ease | 0.18s ease | Fade + lift on open |
<div class="o-pop-anchor">
<button class="o-btn o-btn--primary" aria-expanded="true" aria-haspopup="dialog">
Filter
</button>
<div class="o-pop o-pop--bottom is-open" role="dialog">
<span class="o-pop__arrow"></span>
<div class="o-pop__head">Filter by status</div>
<div class="o-pop__body"><!-- checkboxes --></div>
<div class="o-pop__foot">
<button class="o-btn o-btn--quiet">Reset</button>
<button class="o-btn o-btn--primary">Apply</button>
</div>
</div>
</div>