---
title: Popover
description: Popovers anchor rich content to a trigger element (filters, mini forms, or contextual details) without modal focus trapping.
category: Overlays
---
import { meta as PopoverMeta, Default } from '@/content/stories/popover';
import Canvas from '@/components/component-docs/canvas';
import Demo from '@/components/component-docs/demo';
export const config = {
dependencies: {
components: [Canvas, Demo],
scripts: [
'../../components/component-docs/demo.script.tsx',
'../../components/component-docs/canvas.script.tsx',
'../../components/component-docs/controls.script.tsx',
],
},
};
# Popover
Popovers anchor rich content to a trigger element (filters, mini forms, or contextual details) without modal focus trapping.
## Try it
## Usage
Wrap a child `RuiPopover` in `RuiPopoverTrigger`. Control `open`, `placement`, and `portal` on the trigger or popover.
```tsx
import { RuiPopover, RuiPopoverTrigger, RuiPopoverContent } from '@ecopages/radiant-ui/popover';
import { RuiButton } from '@ecopages/radiant-ui/button';
Filter}>
Show items from the last 7 days.
```
## Custom markup
`` coordinates any light-DOM tree that matches its query contract. Pair with
`` for click-to-toggle, or pass `anchor` for an external selector.
```tsx
import '@ecopages/radiant-ui/popover';
Show items from the last 7 days.
;
```
For a standalone popover anchored elsewhere, omit `rui-popover-trigger` and set `anchor="#my-button"` on ``.
## Listbox variant
Set `variant="listbox"` when the popover hosts a `RuiListbox`; width and focus behavior adapt automatically.
## Match anchor width
Enable `matchAnchorWidth` for select-style popovers so the panel aligns with the trigger.
## Theming
The popover surface maps to **semantic surface roles**, never Tailwind palette steps. The panel is often portaled; override `--rui-popover-*` on `.rui-popover`.
| Part | Default | Override |
| --- | --- | --- |
| Fill / border / radius / shadow | `--background`, `--border`, `--radius-container`, `--shadow-overlay` | `--rui-popover-surface`, `--rui-popover-border-color`, `--rui-popover-radius`, `--rui-popover-shadow` |
| Listbox variant | Stripped padding for embedded listboxes | class `rui-popover--listbox` |
| Z-order | `popover` | theme z-index |
```css
.rui-popover {
--rui-popover-radius: var(--radius-control);
}
```
## Accessibility
- Triggers expose `aria-expanded` when the popover is open.
- Content is focusable and dismissible with Escape.
- Do not trap focus; users should reach surrounding content without closing first.
## API
`RuiPopover` is a custom element (``) positioning a floating `role="dialog"` surface. `RuiPopoverTrigger` (``) coordinates open state with the trigger click.
### Attributes (``)
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | `false` | Whether the popover is open (controlled). |
| `placement` | `RuiPlacement` | `bottom` | Placement relative to the anchor. |
| `portal` | `boolean` | `true` | Teleport the surface to `document.body`. |
| `match-anchor-width` | `boolean` | `false` | Match the anchor width (dropdown menus). |
| `offset` | `number` | `8` | Gap between anchor and surface in px. |
| `anchor` | `string` | `''` | CSS selector for an external anchor. |
| `variant` | `default` · `listbox` | `default` | Surface variant; `listbox` strips padding. |
### Attributes (``)
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | `false` | Whether the popover starts open. |
### Light-DOM contract (``)
| Target | Required | Host writes | Author owns |
| --- | --- | --- | --- |
| `[data-ref="host"]` | yes | — | anchor + surface wrapper |
| `[data-ref="surface"]` | yes | `id`, portal position | floating panel content |
| `[data-popover-trigger]` | yes* | `aria-controls`, `aria-expanded` on anchor | pressable anchor wrapper |
\* Or use the `anchor` attribute with a CSS selector instead of `[data-popover-trigger]`.
Nested hosts: `rui-popover-trigger` (parent provides the trigger target).
### Light-DOM contract (``)
| Target | Required | Host writes | Author owns |
| --- | --- | --- | --- |
| `[data-ref="root"]` | yes | — | click delegate root |
| `[data-popover-trigger]` | yes | — | pressable anchor |
| `rui-popover` | yes | syncs `open` both ways | child popover element |
### Events
| Event | Detail | Description |
| --- | --- | --- |
| `rui-open-change` | `{ open: boolean }` | Emitted when open state changes. |
### Methods (``)
| Method | Description |
| --- | --- |
| `setOpen(next, emit?)` | Toggle open state. Pass `emit = false` to sync without firing `rui-open-change`. |
### View helpers
| Component | Target stamped | Notes |
| --- | --- | --- |
| `RuiPopoverTrigger` | `[data-ref="root"]`, `[data-popover-trigger]` | Wraps child `rui-popover`. Requires `trigger` prop. |
| `RuiPopover` | `[data-ref="host"]`, `[data-ref="surface"]`, optional `[data-popover-trigger]` | `trigger` prop for inline anchor; `children` are surface content. |
| `RuiPopoverContent` | — | Body inside `[data-ref="surface"]`; not a query target. |
### CSS classes
Public BEM classes (documented via `@cssclass`):
| Class | Description |
| --- | --- |
| `.rui-popover-host` | Anchor + surface wrapper. |
| `.rui-popover` | Floating surface (`role="dialog"`); `background` + `rounded-container` + `shadow-overlay`. |
| `.rui-popover--listbox` | Stripped padding for embedded listboxes. |
| `.rui-popover-trigger` | Trigger + popover wrapper. |
### Theme roles
| Part | CSS variables consumed |
| --- | --- |
| Surface | `--background`, `--on-background`, `--border`, `--radius-container`, `--shadow-overlay` |
| Z-order | `--z-popover` |