API
Popover
Sides, alignment, and animation for any `[popover]` element.
Installation
pnpm dlx zazz-ui add popovernpx zazz-ui add popoveryarn dlx zazz-ui add popoverbunx zazz-ui add popoverRun zazz-ui init first if the project has no zazz/ folder yet.
Examples
Shipping
Orders placed before noon ship the same day.
<div>
<button data-ui="button" type="button" popovertarget="popover-example">Show details</button>
<div id="popover-example" popover="auto" data-popover-side="bottom" data-popover-align="start">
<div
style="
--display: flex-col;
--p: 3;
--gap: 2;
--max-w: 72;
"
>
<strong>Shipping</strong>
<p style="--text: var(--color-muted-foreground)">
Orders placed before noon ship the same day.
</p>
</div>
</div>
</div>/**
* popover.css: Shared [popover] primitive (Tier 3 owner for --ui-popover-*)
*
* @layer variables, reset, components
* @requires _layers.css, _variables.css (--color-popover, --color-popover-foreground), _properties.css (--_*-resolved)
* @uses [popover], popovertarget, :popover-open: Popover API (native, no polyfill)
* @uses @starting-style + allow-discrete: enter/exit opacity/transform
* @uses anchor-name, position-anchor, position-area, position-try-fallbacks:
* anchor positioning (gated behind @supports; Safari 26.x falls
* back to the UA-centered popover)
* @tokens --ui-popover-* (Tier 3 owner; @layer variables)
* @presets data-popover-side="top | bottom | left | right", data-popover-align="start | center | end"
* on the [popover] element (the identity is the native attribute)
* @consumedby select.css (picker placement), menu.css, navigation-menu.css, tooltip.css, toaster.css,
* autocomplete.css, combobox.css, command.css
*/
@layer variables {
:root {
/* Styles */
--ui-popover-border-color: var(--color-border);
--ui-popover-rounded: var(--radius-md);
--ui-popover-button-rounded: var(--radius-sm);
--ui-popover-shadow: var(--shadow-md);
--ui-popover-p: calc(var(--spacing) * 1.5);
--ui-popover-min-w: var(--article-xs);
--ui-popover-max-w: 100vi;
/* Reveal transitions/animations */
--ui-popover-scale--start: 0.98;
--ui-popover-translate--start: 0, 0, 0;
--ui-popover-opacity--start: 0;
--ui-popover-scale--end: 1;
--ui-popover-translate--end: 0, 0, 0;
--ui-popover-opacity--end: 1;
/* Positioning */
--ui-popover-origin: top center;
--ui-popover-my: calc(var(--spacing) * 1.5);
--ui-popover-mx: 0;
--ui-popover-position-area: block-end inline-start;
--ui-popover-position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
}
@layer reset {
/* ===========================================================================
POPOVER ELEMENT ENHANCEMENTS
Wires native open/close motion and anchor positioning; surface chrome is in
@layer ui.components below.
=========================================================================== */
:where([popover]) {
color: var(--color-popover-foreground);
background-color: var(--color-popover);
align-content: start;
transform-origin: var(--ui-popover-origin);
border-radius: var(--_rounded-resolved, var(--ui-popover-rounded));
box-shadow: var(--_shadow-resolved, var(--ui-popover-shadow));
opacity: var(--ui-popover-opacity--start);
transform: scale(var(--ui-popover-scale--start)) translate3d(var(--ui-popover-translate--start));
transition:
opacity var(--default-transition-duration) var(--default-transition-timing-function),
transform var(--default-transition-duration) var(--default-transition-timing-function),
padding var(--default-transition-duration) var(--default-transition-timing-function),
block-size var(--default-transition-duration) var(--default-transition-timing-function),
inline-size var(--default-transition-duration) var(--default-transition-timing-function),
overlay var(--default-transition-duration) var(--default-transition-timing-function)
allow-discrete,
display var(--default-transition-duration) var(--default-transition-timing-function)
allow-discrete;
}
/* Anchor positioning tethers the popover to its trigger and flips it away
from viewport edges. The gate keeps Safari 26.x on the UA-centered popover
(inset: 0 + margin: auto) instead of a mispositioned top-left box.
Detection key is anchor-name, the API's entry property. */
@supports (anchor-name: --zazz) {
:where([popover]) {
position: fixed;
inset: auto;
--_my-len: var(--_my-resolved, var(--ui-popover-my));
--_my-num: var(--_my-resolved, var(--ui-popover-my));
margin-block: calc(var(--_my-len) + var(--_my-num) * var(--spacing));
--_mx-len: var(--_mx-resolved, var(--ui-popover-mx));
--_mx-num: var(--_mx-resolved, var(--ui-popover-mx));
margin-inline: calc(var(--_mx-len) + var(--_mx-num) * var(--spacing));
position-area: var(--ui-popover-position-area);
position-try-fallbacks: var(--ui-popover-position-try-fallbacks);
}
}
/* Closed popovers keep the UA's display: none; allow-discrete animates the switch. */
[popover]:popover-open {
display: grid;
align-content: start;
opacity: var(--ui-popover-opacity--end);
transform: scale(var(--ui-popover-scale--end)) translate3d(var(--ui-popover-translate--end));
@starting-style {
opacity: var(--ui-popover-opacity--start);
transform: scale(var(--ui-popover-scale--start))
translate3d(var(--ui-popover-translate--start));
}
}
}
@layer ui.components {
/* ===========================================================================
POPOVER SURFACE — shared chrome for every [popover] consumer
=========================================================================== */
[popover] {
border: 1px solid var(--_border-color-resolved, var(--ui-popover-border-color));
--_p-len: var(--_p-resolved, var(--ui-popover-p));
--_p-num: var(--_p-resolved, var(--ui-popover-p));
padding: calc(var(--_p-len) + var(--_p-num) * var(--spacing));
--_min-w-len: var(--_min-w-resolved, var(--ui-popover-min-w));
--_min-w-num: var(--_min-w-resolved, var(--ui-popover-min-w));
min-inline-size: calc(var(--_min-w-len) + var(--_min-w-num) * var(--spacing));
--_max-w-len: var(--_max-w-resolved, var(--ui-popover-max-w));
--_max-w-num: var(--_max-w-resolved, var(--ui-popover-max-w));
max-inline-size: calc(var(--_max-w-len) + var(--_max-w-num) * var(--spacing));
}
[popover] [data-ui~="button"] {
--ui-button-rounded: var(--ui-popover-button-rounded);
}
/* ===========================================================================
POSITIONING — side × align
The single placement matrix for every anchored surface. It assigns the
--ui-popover-position-* tokens; each surface applies them itself (popover
above, ::picker(select) in select.css) because the properties are gated on
anchor support. Zero specificity (:where) so components can override a cell.
A <select> joins from select.css: custom properties set on it inherit into
::picker(select).
=========================================================================== */
:where([popover])[data-popover-side="bottom"]:not([data-popover-align]),
:where([popover])[data-popover-side="bottom"][data-popover-align="start"] {
--ui-popover-position-area: block-end span-inline-end;
--ui-popover-origin: top center;
--ui-popover-my: calc(var(--spacing) * 1.5);
--ui-popover-mx: 0;
}
:where([popover])[data-popover-side="bottom"][data-popover-align="center"] {
--ui-popover-position-area: block-end span-all;
--ui-popover-origin: top center;
--ui-popover-my: calc(var(--spacing) * 1.5);
--ui-popover-mx: 0;
}
:where([popover])[data-popover-side="bottom"][data-popover-align="end"] {
--ui-popover-position-area: block-end span-inline-start;
--ui-popover-origin: top center;
--ui-popover-my: calc(var(--spacing) * 1.5);
--ui-popover-mx: 0;
}
:where([popover])[data-popover-side="top"]:not([data-popover-align]),
:where([popover])[data-popover-side="top"][data-popover-align="start"] {
--ui-popover-position-area: block-start span-inline-end;
--ui-popover-origin: bottom center;
--ui-popover-my: calc(var(--spacing) * 1.5);
--ui-popover-mx: 0;
}
:where([popover])[data-popover-side="top"][data-popover-align="center"] {
--ui-popover-position-area: block-start span-all;
--ui-popover-origin: bottom center;
--ui-popover-my: calc(var(--spacing) * 1.5);
--ui-popover-mx: 0;
}
:where([popover])[data-popover-side="top"][data-popover-align="end"] {
--ui-popover-position-area: block-start span-inline-start;
--ui-popover-origin: bottom center;
--ui-popover-my: calc(var(--spacing) * 1.5);
--ui-popover-mx: 0;
}
:where([popover])[data-popover-side="right"]:not([data-popover-align]),
:where([popover])[data-popover-side="right"][data-popover-align="start"] {
--ui-popover-position-area: inline-end span-block-end;
--ui-popover-origin: bottom left;
--ui-popover-my: 0;
--ui-popover-mx: calc(var(--spacing) * 1.5);
}
:where([popover])[data-popover-side="right"][data-popover-align="center"] {
--ui-popover-position-area: inline-end span-all;
--ui-popover-origin: center left;
--ui-popover-my: 0;
--ui-popover-mx: calc(var(--spacing) * 1.5);
}
:where([popover])[data-popover-side="right"][data-popover-align="end"] {
--ui-popover-position-area: inline-end span-block-start;
--ui-popover-origin: top left;
--ui-popover-my: 0;
--ui-popover-mx: calc(var(--spacing) * 1.5);
}
:where([popover])[data-popover-side="left"]:not([data-popover-align]),
:where([popover])[data-popover-side="left"][data-popover-align="start"] {
--ui-popover-position-area: inline-start span-block-end;
--ui-popover-origin: bottom right;
--ui-popover-my: 0;
--ui-popover-mx: calc(var(--spacing) * 1.5);
}
:where([popover])[data-popover-side="left"][data-popover-align="center"] {
--ui-popover-position-area: inline-start span-all;
--ui-popover-origin: center right;
--ui-popover-my: 0;
--ui-popover-mx: calc(var(--spacing) * 1.5);
}
:where([popover])[data-popover-side="left"][data-popover-align="end"] {
--ui-popover-position-area: inline-start span-block-start;
--ui-popover-origin: top right;
--ui-popover-my: 0;
--ui-popover-mx: calc(var(--spacing) * 1.5);
}
}Attributes
| Attribute | Kind | Values | Notes |
|---|---|---|---|
data-popover-align | Preset | center end start | Preset: a named bundle of values. |
data-popover-side | Preset | bottom left right top | Preset: a named bundle of values. |
Hooks
Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.
| Hook | Default | State |
|---|---|---|
--ui-popover-border-color | var(--color-border) | — |
--ui-popover-button-rounded | var(--radius-sm) | — |
--ui-popover-max-w | 100vi | — |
--ui-popover-min-w | 0 | — |
--ui-popover-mx | calc(var(--spacing) * 1.5) | — |
--ui-popover-my | calc(var(--spacing) * 1.5) | — |
--ui-popover-opacity--end | 1 | end |
--ui-popover-opacity--start | 0 | start |
--ui-popover-origin | top center | — |
--ui-popover-p | 0 | — |
--ui-popover-position-area | block-end span-inline-end | — |
--ui-popover-position-try-fallbacks | flip-inline, flip-block, flip-inline flip-block | — |
--ui-popover-rounded | var(--radius-md) | — |
--ui-popover-scale--end | 1 | end |
--ui-popover-scale--start | 1 | start |
--ui-popover-shadow | var(--ui-navigation-menu-popover-shadow) | — |
--ui-popover-translate--end | 0, 0, 0 | end |
--ui-popover-translate--start | 0, 0, 0 | start |