registry:ui · NativeHTML
popover — declarative, zero script
The native Popover API does everything: the panel carries popover="auto" and the trigger is wired with popovertarget, so light dismiss, Escape, aria-expanded, and top-layer rendering come from the browser. The default trigger path runs zero listeners and zero positioning script — the browser owns open/close, light dismiss, and the top layer; the component's script is one aria toggle seam plus the shared surface-motion kernel (view source and count).
$npx jixoai-ui add popover
https://ui.jixoai.com/r/popover.json
usage
Usage
Menu rows repeat popovertarget to close on select — still zero JavaScript on the close path.
<script lang="ts">
import Popover from '@ui/popover.svelte';
</script>
<!-- menu rows repeat popovertarget to close on select — still zero JS -->
<Popover id="actions" triggerLabel="Actions">
<div class="flex flex-col">
<button type="button" class="pop-row" popovertarget="actions">Rename…</button>
<button type="button" class="pop-row" popovertarget="actions">Archive</button>
</div>
</Popover>popover
popover="auto" + popovertarget: light dismiss, Escape, aria-expanded, and top-layer rendering are the browser's — the panel anchors to the trigger through CSS Anchor Positioning. Relabel the trigger from the Playground.
<script lang="ts">
import Popover from '@ui/popover.svelte';
</script>
let triggerLabel = $state("Actions");
</script>
<!-- rows repeat popovertarget to close on select — still zero JS -->
<Popover id="actions" {triggerLabel} variant="auto">
<div class="flex w-52 flex-col">
<button type="button" class="pop-row" popovertarget="actions">Rename…</button>
<button type="button" class="pop-row" popovertarget="actions">Archive</button>
</div>
</Popover>demo
Card type
The children snippet is the whole panel body, so richer content — status facts and a link out — composes with the same one-element base.
registry status
Components are served as same-source files from this domain — nothing here is a runtime dependency.
- items
- 12
- runtime deps
- 0
- license
- MIT
<Popover id="status" triggerLabel="Registry status">
<p class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">
registry status
</p>
<p>Any content — the snippet is the whole panel body.</p>
</Popover>W3C foundation
What the platform gives, what we add
The component is styling and structure over two native attributes. Everything behavioral is the browser's; the one design decision we own is the placement strategy — anchored placement SHIPPED 2026-08-21 (the placement prop, nine positions, demoed above).
platform-native, free
-
popover="auto"— light dismiss: outside click closes (Tab-out does NOT close on Chromium — an interop gap; Escape and click-out are the reliable exits); only one auto popover stays open at a time -
popovertarget={id}— declarative trigger wiring; Enter/Space toggle, and the browser exposesaria-expandedon the button - top layer — the panel renders above sticky headers, transforms, and open dialogs (engine-dependent at the top layer's hit-testing edge: true on system Chrome, not on Chrome for Testing 153 — the z-order seam is the platform's, re-verify per engine)
- Escape closes natively; focus returns to the trigger
-
::backdropships with popovers too — kept transparent so dismissing never dims the page
jixoai strategy & extensions
- zero positioning script — the open/close path is native; the component's script is the aria toggle seam + the motion kernel (no effects on the trigger path)
- placement v1 — authored centering:
inset-area: centerwith an inset + margin fallback (the UA default centers viamargin: auto) - SHIPPED: anchored placement next to the trigger — CSS Anchor Positioning (
anchor-name+position-anchor/inset-area) behind the liveplacementprop (nine positions, demoed above) - floating-surface law: the hard offset shadow is a REAL
::afterlayer;@starting-style+allow-discreterun the open/close pull-apart — layers press together, then separate into elevation - surface variants:
variant="solid | acrylic | auto"— solid keeps the opaque surface with the original-color translucent shadow (black in light mode, white in dark); acrylic is a dual-layerbackdrop-filter(surface blur + shadow-layer brightness); auto picks acrylic unless the environment asks for reduced transparency - extension:
popover="manual"variant for toast-like panels that ignore light dismiss
types
Popover variants
Panel content is free; the variant axes are surface paint, placement, and the trigger.
variant
solid | acrylic | auto (default) — acrylic is a dual-layer backdrop-filter; auto defers to the environment's transparency preference.
placement — nine positions
The six classic sides plus left | right | center; default bottom-end — under the trigger, right edges aligned.
trigger
The default button carries popovertarget; a custom trigger snippet renders your own control (drive it via the imperative handle).
a11y
Accessibility
Light dismiss, Escape, and top-layer focus order are the browser's; one native toggle seam mirrors aria-expanded.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Toggle the panel from the popovertarget trigger (native button) |
| Escape | Close the panel; focus returns to the trigger |
| Tab | Moves through the panel body content while open |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-expanded | true/false | On the default trigger — mirrored live from :popover-open by the toggle seam. |
| popover | "auto" | Light dismiss (outside click / focus loss) and one-at-a-time are native. |
| position-visibility | anchors-visible | A panel whose anchor scrolled away hides instead of floating stale. |
theming
Density and tokens
The anchored panel rides the shared motion kernel; the scroll ring's padding is token-overridable.
Density-FLAT by family mechanism (measured): the scope stamps re-derive on the wrapper (the --jx-hit arithmetic 28/32/40/48) but this family's trigger and panel consume no density lane — the trigger stays 43px and the panel pad 12px 14px across every rung; each rung's panel is its OWN instance (density-pop-xs/sm/default/lg — per-rung ids, LAW #19).
Density-FLAT by family mechanism (measured): the scope stamps re-derive on the wrapper (the --jx-hit arithmetic 28/32/40/48) but this family's trigger and panel consume no density lane — the trigger stays 43px and the panel pad 12px 14px across every rung; each rung's panel is its OWN instance (density-pop-xs/sm/default/lg — per-rung ids, LAW #19).
Density-FLAT by family mechanism (measured): the scope stamps re-derive on the wrapper (the --jx-hit arithmetic 28/32/40/48) but this family's trigger and panel consume no density lane — the trigger stays 43px and the panel pad 12px 14px across every rung; each rung's panel is its OWN instance (density-pop-xs/sm/default/lg — per-rung ids, LAW #19).
Density-FLAT by family mechanism (measured): the scope stamps re-derive on the wrapper (the --jx-hit arithmetic 28/32/40/48) but this family's trigger and panel consume no density lane — the trigger stays 43px and the panel pad 12px 14px across every rung; each rung's panel is its OWN instance (density-pop-xs/sm/default/lg — per-rung ids, LAW #19).
| Token | Default | Source |
|---|---|---|
| --jx-pop-{id} | anchor-name | component |
| --jx-pop-gap | 0px (gap prop) | component |
| --jx-pop-pad / -inline | 12px 14px / 14px | component |
| --jx-surface-in-x/y · -ox/-oy | direction vectors | component |
| --jx-scrollbar-thin | thin lane | component |
| --jx-text | 11 / 12 / 13 / 15px | density |
| --jx-hit | 28 / 32 / 40 / 48px | density |
axes
Universal props
The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The anchored panel carries its OWN elevation — level2 (3dp, M3's menu rung): the theme's level table pairs the shadow recipe with the ladder rung; an explicit lane or a dp number steps both. The carriers stamp the promoted panel root — self-carried across the top layer.
Popover · universal props
3dp + the surface-container-low rung.
6dp + the surface-container rung.
Exact dp snaps down — 8dp IS level4.
The concave: inset shadow + the deepest rung.
<Popover id="p-default" triggerLabel="level2 · default">…</Popover>
<Popover id="p-l3" triggerLabel="level3" elevation="level3">…</Popover>
<Popover id="p-dp" triggerLabel="8dp" elevation={8}>…</Popover>
<Popover id="p-concave" triggerLabel="level-1" elevation="level-1">…</Popover>api
API
Ten props plus the bind:this imperative handle — no controlled state model.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| id * | string | — | Popover id: popovertarget association + the CSS anchor name. |
| triggerLabel | string | '' | Default trigger button label; ignored when a trigger snippet is given. |
| placement | 'bottom' | 'bottom-end' | 'bottom-start' | 'top' | 'top-end' | 'top-start' | 'left' | 'right' | 'center' | 'bottom-end' | The INITIAL anchored position; chosen once at open, never re-evaluated while open. |
| variant | 'solid' | 'acrylic' | 'auto' | 'auto' · Own default, not ambient | Floating-surface paint. |
| tryFallbacks | string | '' | Raw position-try value — custom @position-try idents replace the default flip series. |
| gap | number | string | — | Anchor gap, margin semantics — number = uniform px; shorthand gaps only the facing side. Invalid input is ignored. |
| trigger | Snippet | — | Custom trigger control; anchoring stays component-owned. |
| panelClass | string | '' | Appended to the panel (width, grid, tokens — never anchoring). |
| onToggle | (open: boolean) => void | — | Mirrors the native toggle event; the only open-state source of truth. |
| children * | Snippet | — | The whole panel body. |
| bind:this | { show, hide, toggle } | — | Imperative handle — thin native passthroughs for exceptional triggers. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |