Popover
Displays rich content in a portal, triggered by a button.
Set the dimensions for the layer.
Popover is a floating panel anchored to a trigger that opens on click. Unlike a Dialog (centered modal, blocking backdrop), the popover is non-modal: the rest of the page stays interactive and clicking outside closes the popover.
Popover is the foundation for several other components that inherit its pattern:
- DropdownMenu: a popover with a list of menu items.
- HoverCard: opens on hover instead of click.
- Tooltip: hover + minimal text content.
- Combobox: a popover with a search input + filterable list.
- DatePicker: a popover around a Calendar.
PRISM architecture: the trigger carries data-ui="toggle-pop" data-target="#popover-id", and the content is a <div data-overlay data-light hidden> with the matching id. data-light distinguishes popovers ("light") from dialogs ("heavy"): popovers do not block body scroll, close on outside click, and do not stack overflow locks.
Use it for: advanced filters, inline editors (color, dimensions), contextual details, short forms, entity previews.
Installation
$ bext ui add popover
Copy and paste the following into src/components/ui/popover.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="popover" hooks.
Usage
Anatomy
<Popover>
├── <PopoverTrigger for="pop-id"> // data-ui="toggle-pop"
│ └── <Button>Open popover</Button>
└── <PopoverContent id="pop-id"> // data-overlay data-light hidden
└── ...free-form content... // form, list, textAPI Reference
Popover
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Extra classes on the wrapper, a with relative inline-block. Contains trigger + content. |
PopoverTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| for | string | — | Matches the id of the PopoverContent to open. Generates data-target="#for". |
| children | ReactNode | — | Trigger element (button, link, icon). The click is dispatched to the toggle. |
PopoverContent
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | — | Matches PopoverTrigger.for. |
| align | "start" | "center" | "end" | "start" | Horizontal alignment of the panel relative to the trigger. start→left-0, center→center, end→right-0. |
| ariaLabel | string | — | Accessible name (aria-label) of the panel. Without it the island names the panel after its trigger button (aria-labelledby). |
| className | string | — | CSS classes. Default: absolute top-full mt-2 w-72 rounded-md border bg-popover p-4 shadow-md. |
Data Attributes
Attributes you can target in custom CSS to override the style for each state.
| Attribute | On | Values | Description |
|---|---|---|---|
| data-ui="toggle-pop" | PopoverTrigger | data-target="#id" | Click opens/closes the popover. |
| data-state | PopoverTrigger | "open" | "closed" | Set by the island, together with aria-haspopup="dialog", aria-expanded and aria-controls on the button inside. |
| data-overlay data-light | PopoverContent | hidden by default | The data-light attribute distinguishes popovers from dialogs: the island closes them on outside click without stacking the body overflow lock. |
Keyboard
| Key | Action |
|---|---|
| Enter / Space | On the trigger: opens the popover and moves focus to its first focusable element (or to the panel), or closes it. On an inner button: activates. |
| Tab / Shift+Tab | Moves through the content, then on to the rest of the page: focus is not trapped. |
| Escape | Closes the popover and returns focus to the trigger. |
Accessibility
PopoverTrigger is a <span> around your button. The island sets aria-haspopup="dialog", aria-controls and aria-expanded on that button and keeps aria-expanded in sync on every open and close, including a close by outside click or Escape. Put a real <button> or link inside the trigger: plain text gets no ARIA state and cannot be reached with the keyboard.
PopoverContent has role="dialog" without aria-modal. It is named by its trigger button through aria-labelledby, or by the ariaLabel prop.
Focus: on open, focus moves to the first focusable element of the panel, or to the panel itself when it has none. The popover does not trap focus: Tab can leave it. This is intentional, since the popover is non-modal. Escape closes it and returns focus to the trigger; an outside click closes it and leaves focus where the click put it.
Use Dialog instead when: the content is a critical form (2+ required fields), an action must be blocking, or the content needs a backdrop.
Examples
Minimal content (info)
This is a short piece of contextual information that lives in a popover.
Aligned end (right of the trigger)
align="end": the popover aligns to the right edge of the trigger.
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page