# Popover

> Rich content on click, built on the native Popover API — top layer and light dismiss for free, anchored with CSS anchor positioning.

URL: https://apogee.avaui.dev/docs/popover
Last updated: 2026-10-15

Rich content on click, built on the native Popover API — top layer and light dismiss for free, anchored with CSS anchor positioning.

Last updated October 15, 2026

```astro
<Popover>
<span slot="trigger">More info</span>
<p>Extra detail shown in the panel.</p>
</Popover>
```

## Add

```sh
npx @ava-ui/apogee add popover
```

Depends on `utils` (the anchored-panel helper). No icons. Zero JS — the native Popover API (`popover`, `popovertarget`) plus CSS anchor positioning do the work.

## Usage

```astro
---
import Popover from '@/components/apogee/Popover/Popover.astro'
---
<Popover placement="bottom-start" width="trigger">
  <span slot="trigger">Filters</span>
  <p>Panel content goes here.</p>
</Popover>
```

## Anatomy

-   **Trigger** — the built-in `<button popovertarget>`; its label is the `trigger` slot (defaults to “Open”).
-   **Anchor** — alternatively, your own element in the `anchor` slot. Give it `popovertarget={id}` and pass the same `id` to the Popover.
-   **Panel** — the `<div popover>` anchored to the trigger; its content is the default slot. It paints in the top layer, so no ancestor can clip it.

## API

`@composable` — exposed to Content Editors in AVA. Any other attribute passes through to the trigger (the built-in button, or the wrapper around your `anchor` element).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | auto-generated | Panel id. Set it when an `anchor` element needs `popovertarget`. |
| `placement` | `'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'start' \| 'start-top' \| 'start-bottom' \| 'end' \| 'end-top' \| 'end-bottom'` | `'bottom'` | Side and alignment. `start`/`end` are logical and mirror in RTL. The panel flips to the opposite side when there is no room. |
| `gap` | `number` | `2` | Space between trigger and panel, in spacing steps (2 = 0.5rem). |
| `width` | `'trigger' \| string` | — | `'trigger'` matches the trigger’s width; otherwise any CSS length. |
| `maxHeight` | `string` | — | Caps the panel height; content scrolls past it. |
| `padding` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | `4` | Panel padding in spacing steps. |
| `elevation` | `'raised' \| 'overlay'` | `'overlay'` | Surface and shadow of the panel. |
| `panelClass` | `string` | — | Extra classes on the panel. |
| `class` | `string` | — | Extra classes on the trigger, merged last. |

## Placement

All twelve values. Each panel flips to the opposite side when it would leave the viewport (`position-try-fallbacks`).

```astro
<Popover placement="top-start"><span slot="trigger">top-start</span>…</Popover>
<Popover placement="top"><span slot="trigger">top</span>…</Popover>
<Popover placement="top-end"><span slot="trigger">top-end</span>…</Popover>
<Popover placement="bottom-start"><span slot="trigger">bottom-start</span>…</Popover>
<Popover placement="bottom"><span slot="trigger">bottom</span>…</Popover>
<Popover placement="bottom-end"><span slot="trigger">bottom-end</span>…</Popover>
<Popover placement="start-top"><span slot="trigger">start-top</span>…</Popover>
<Popover placement="start"><span slot="trigger">start</span>…</Popover>
<Popover placement="start-bottom"><span slot="trigger">start-bottom</span>…</Popover>
<Popover placement="end-top"><span slot="trigger">end-top</span>…</Popover>
<Popover placement="end"><span slot="trigger">end</span>…</Popover>
<Popover placement="end-bottom"><span slot="trigger">end-bottom</span>…</Popover>
```

### Edge flip

A trigger at the end of the row with `placement="end"` has no room on that side, so the panel opens on the start side instead.

```astro
<div class="flex w-full justify-end">
<Popover placement="end">
  <span slot="trigger">At the edge</span>
  <p>Flipped to the start side.</p>
</Popover>
</div>
```

## Width and max height

`width="trigger"` matches the trigger; `maxHeight` scrolls the content. The panel is in the top layer, so an `overflow: hidden` ancestor cannot clip it.

```astro
<div class="w-64 overflow-hidden rounded-md border border-neutral p-4">
<Popover width="trigger" maxHeight="8rem" placement="bottom-start">
  <span slot="trigger">Pick a region (long list)</span>
  <ul class="flex flex-col gap-1">
    <li>Africa</li><li>Antarctica</li><li>Asia</li><li>Europe</li>
    <li>North America</li><li>Oceania</li><li>South America</li>
  </ul>
</Popover>
</div>
```

## Custom trigger

Put your own element in the `anchor` slot with `popovertarget` set to the Popover’s `id`. The element must be a `<button>` (or any element with invoker support) so it works with zero JS.

```astro
<Popover id="pop-profile" placement="bottom-end">
<Button slot="anchor" popovertarget="pop-profile" variant="ghost" color="neutral">Jane Doe</Button>
<p class="font-medium">Jane Doe</p>
<p class="text-neutral-faded">jane@example.com</p>
</Popover>
```

## Padding, elevation and gap

```astro
<Popover padding={0}><span slot="trigger">padding 0</span><div class="h-24 w-56 rounded-lg bg-neutral-faded" /></Popover>
<Popover padding={2}><span slot="trigger">padding 2</span><p>Tight panel.</p></Popover>
<Popover elevation="raised"><span slot="trigger">raised</span><p>Lighter shadow.</p></Popover>
<Popover gap={0}><span slot="trigger">gap 0</span><p>Touches the trigger.</p></Popover>
<Popover gap={4}><span slot="trigger">gap 4</span><p>One rem away.</p></Popover>
```

## States

-   **Closed → open** — a fade + slight scale (`--duration-fast`, `--ease-standard`), using `@starting-style` for the entry transition; driven entirely by the browser’s `:popover-open` pseudo-class.
-   **Out of room** — the panel flips to the opposite side, then the opposite alignment, with no JS.
-   **Unsupported anchor positioning** — the panel falls back to the popover API’s default centered position, still fully usable. Anchor positioning is Baseline since January 2026.

## Examples

A short list inside the panel:

```astro
<Popover placement="bottom-start" padding={1}>
  <span slot="trigger">Share</span>
  <ul class="flex flex-col gap-0.5">
    <li><a href="/copy-link" class="block rounded px-2 py-1.5 hover:bg-neutral-faded-highlighted">Copy link</a></li>
    <li><a href="/email" class="block rounded px-2 py-1.5 hover:bg-neutral-faded-highlighted">Email</a></li>
  </ul>
</Popover>
```

## Accessibility

| Key | Action |
| --- | --- |
| `Enter` `Space` | On the trigger, toggle the panel. |
| `Esc` | Closes the panel and returns focus to the trigger (native). |

-   The trigger is a real `<button popovertarget>`; the browser sets `aria-expanded` on it.
-   Clicking outside closes the panel — the browser’s native light-dismiss behaviour (`popover="auto"`, the implicit default), no JS involved.
-   The panel paints in the native top layer, above everything else, with no `z-index` needed.
-   The panel has no implicit ARIA role — if its content behaves like a dialog or menu, add `role="dialog"` / `role="menu"` (and a label) yourself.
-   Opening doesn’t move focus into the panel — there’s no `autofocus`. Add one to a focusable child if the panel needs immediate keyboard interaction.
-   The open/close fade is disabled under `prefers-reduced-motion: reduce`.

## Tokens

| Token | Used for |
| --- | --- |
| `--border-neutral` | Trigger + panel border |
| `--background-page` | Trigger background |
| `--background-elevation-overlay` / `-raised` | Panel background (`elevation`) |
| `--foreground-neutral` | Trigger + panel text |
| `--background-neutral-faded-highlighted` | Trigger hover background |
| `--outline-color-focus` | Focus ring |
| `--duration-fast` / `--ease-standard` | Open/close transition |
| `--radius-md` | Trigger corner radius |
| `--radius-lg` | Panel corner radius |

## Do / Don’t

-   **Do** keep the panel’s first interactive element reachable by `Tab` — there’s no focus trap, so keyboard order just follows DOM order.
-   **Do** use `width="trigger"` for panels that act like a picker under a field.
-   **Don’t** put a page’s primary content only in a Popover — a stray click or `Esc` dismisses it.
-   **Don’t** forget `popovertarget` on an `anchor` element — without it the panel never opens.

## Content

-   Trigger label is a short verb or noun for the panel’s purpose (“Filters”, “Share”, “More info”).
-   Panel copy stays short — it’s a lightweight aside, not a page.

## Related

[

TooltipA short hint shown on hover and keyboard focus, anchored to its trigger in the top layer with hover intent.

](https://apogee.avaui.dev/docs/tooltip)[

DropdownA menu on the native Popover API — the trigger opens a list of DropdownItems in the top layer, with sections and submenus.

](https://apogee.avaui.dev/docs/dropdown)[

DialogA native <dialog> opened and closed with invoker commands — modal, edge panel, or fullscreen.

](https://apogee.avaui.dev/docs/dialog)
