Popover
Popover: anatomy, keyboard behavior, state, and theming in Askr.
Example
import { Button, Popover, PopoverContent, PopoverTrigger } from '@askrjs/themes/components';
<Popover>
<PopoverTrigger asChild><Button variant="outline">Filters</Button></PopoverTrigger>
<PopoverContent><ProjectFilters /></PopoverContent>
</Popover>Open contextual content anchored to a trigger.
Published props
Generated from the TypeScript declarations shipped by the installed package. Named types in the Type column define the accepted values.
PopoverCloseAsChildProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
type | type?: undefined; | — | — |
PopoverCloseProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
PopoverContentAsChildProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
align | align?: OverlayAlign | undefined; | — | — |
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
side | side?: OverlaySide | undefined; | — | — |
sideOffset | sideOffset?: number | undefined; | — | — |
width | width?: PopoverContentWidth | undefined; | — | — |
PopoverContentOwnProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
align | align?: OverlayAlign | undefined; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
side | side?: OverlaySide | undefined; | — | — |
sideOffset | sideOffset?: number | undefined; | — | — |
width | width?: PopoverContentWidth | undefined; | — | — |
PopoverContentProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
align | align?: OverlayAlign | undefined; | — | — |
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
side | side?: OverlaySide | undefined; | — | — |
sideOffset | sideOffset?: number | undefined; | — | — |
width | width?: PopoverContentWidth | undefined; | — | — |
PopoverOwnProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
defaultOpen | defaultOpen?: boolean | undefined; | — | — |
id | id?: string | undefined; | — | — |
onOpenChange | onOpenChange?: ((open: boolean) => void) | undefined; | — | — |
open | open?: boolean | undefined; | — | — |
PopoverPortalProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
PopoverProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
defaultOpen | defaultOpen?: boolean | undefined; | — | — |
id | id?: string | undefined; | — | — |
onOpenChange | onOpenChange?: ((open: boolean) => void) | undefined; | — | — |
open | open?: boolean | undefined; | — | — |
PopoverTriggerAsChildProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
type | type?: undefined; | — | — |
PopoverTriggerProps
Import from @askrjs/ui/popover.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
Purpose
Popover is a non-modal floating panel anchored to a trigger element — think a filter panel, a color picker, or a small settings menu that appears next to what you clicked. Unlike Dialog it doesn't block the rest of the page or trap focus behind a backdrop; it just opens near its trigger and closes on outside interaction or Escape.
Install and import
Import behavior from `@askrjs/ui/popover` and the themed version from `@askrjs/themes/popover`. The root package re-exports the same set — `Popover`, `PopoverTrigger`, `PopoverContent`, `PopoverClose` — so use whichever import path fits your bundling setup.
Live examples
`Popover` wraps a `PopoverTrigger` and a `PopoverPortal` containing `PopoverContent`; put a `PopoverClose` inside the content if you want an explicit dismiss button rather than relying on outside-click. Pass `side` and `align` on `PopoverContent` to control which edge of the trigger it opens from and how it lines up against it.
Anatomy
The tree is `Popover` > `PopoverTrigger` and `PopoverPortal` > `PopoverContent`, with `PopoverClose` optionally nested inside the content. There's no separate overlay component — popovers don't dim the background, since they're meant to be lightweight and non-blocking compared to Dialog.
State model
Open state works exactly like Dialog's — `open`, `defaultOpen`, `onOpenChange` on the root — but there's no `modal` prop, because a popover never blocks the rest of the page by design. Positioning state (which side it actually rendered on, once it's flipped to avoid the viewport edge) is tracked internally and exposed through `data-side`/`data-align` attributes rather than anything you manage yourself.
Keyboard and accessibility
`PopoverContent` renders with `role="dialog"` and is labelled from the trigger by default, with `aria-labelledby`/`aria-label` as your override if the trigger's own text isn't descriptive enough. It uses `FocusScope` for focus management, but not in trapped mode — Tab can move focus back out to the rest of the page while the popover is open, unlike Dialog's hard trap; what it does give you is restoring focus to the trigger on close. It sits behind a dismissable layer so outside clicks and Escape close it the way users expect from any transient overlay.
Styling and tokens
`PopoverContent` accepts `side`, `align`, `sideOffset`, and a `width` of `sm`/`md`/`lg`, all of which show up as `data-side`/`data-align` attributes the default theme keys its positioning CSS off of. It sits at `--ak-z-popover` (`1500`) in the shared z-index scale — tied with `--ak-z-dropdown` at the same layer, above modals (`1400`) but below toast (`1550`) and tooltip (`1600`).
API
`PopoverContent` layers `forceMount`, `side`, `align`, `sideOffset`, and `width` on top of standard div props. `PopoverTrigger` and `PopoverClose` are the usual button-like components with `onPress`/`disabled`. `PopoverPortal` just wraps `children` — it doesn't have positioning props of its own, since that all lives on `PopoverContent`.
Edge cases
If the popover is anchored near a viewport edge, the positioning logic will flip `side` automatically to keep it visible — don't hardcode layout assumptions based on the `side` prop you passed in, read the resolved `data-side` attribute if you need to react to where it actually landed. Popovers containing form inputs need care around outside-click dismissal — a click that starts inside the content and ends outside (e.g. a native `<select>` dropdown) can register as an outside interaction if you're not careful with `onInteractOutside`.
Related pages
Hover Card is a close cousin for content that should appear on hover rather than click. If you need to block the rest of the page and trap focus for something more significant than a small panel, use Dialog instead.