Popover
Rich content in a floating panel, triggered by a button. Uses the native Popover API.
Default
Click the trigger to open a popover below (default side). Uses native Popover API — no JS for open/close.
Dimensions
Set the dimensions for the layer.
Width: 100% • Height: auto
<button class="btn" data-variant="outline" popovertarget="my-popover">Open Popover</button>
<div class="popover" id="my-popover" popover>
<div class="popover-header">
<p class="popover-title">Dimensions</p>
<p class="popover-description">Set the dimensions for the layer.</p>
</div>
<div class="popover-content">...</div>
</div>
With Form
Popover containing form fields — the canonical shadcn popover pattern.
Dimensions
Set the dimensions for the layer.
Align
Use data-align on the popover to control horizontal alignment: start, center (default), or end.
Start aligned
Popover aligned to the start edge.
Center aligned
Default center alignment.
End aligned
Popover aligned to the end edge.
Side
Use data-side to position the popover on a specific side of the trigger: top, right, bottom (default), or left.
Top side
Positioned above the trigger.
Right side
Positioned to the right.
Bottom side
Default bottom positioning.
Left side
Positioned to the left.
CSS view file
Uses position-area for anchor positioning, @starting-style for enter animations, and position-try-fallbacks for automatic viewport-overflow flipping. Supports data-side and data-align attributes.
@layer components {
.popover {
position: fixed;
inset: auto;
margin: 0;
border: 1px solid var(--border);
border-radius: var(--radius-xl);
background-color: var(--popover);
color: var(--popover-foreground);
padding: 1rem;
box-shadow: var(--shadow-md);
width: 20rem;
opacity: 0;
translate: 0 -4px;
transition: opacity 150ms ease, translate 150ms ease, display 150ms allow-discrete;
/* -- Default: bottom center -- */
position-area: bottom;
margin-top: 4px;
position-try-fallbacks: flip-block;
&:popover-open {
opacity: 1;
translate: 0 0;
}
/* ----- Side variants ----- */
&[data-side="top"] {
position-area: top;
margin-top: 0;
margin-bottom: 4px;
translate: 0 4px;
&:popover-open { translate: 0 0; }
}
&[data-side="left"] {
position-area: left;
margin-top: 0;
margin-right: 4px;
translate: 4px 0;
position-try-fallbacks: flip-inline;
&:popover-open { translate: 0 0; }
}
&[data-side="right"] {
position-area: right;
margin-top: 0;
margin-left: 4px;
translate: -4px 0;
position-try-fallbacks: flip-inline;
&:popover-open { translate: 0 0; }
}
/* ----- Align variants (combined with side) ----- */
&[data-align="start"] {
&:not([data-side="left"]):not([data-side="right"]) { position-area: bottom left; }
&[data-side="top"] { position-area: top left; }
&[data-side="left"] { position-area: left top; }
&[data-side="right"] { position-area: right top; }
}
&[data-align="end"] {
&:not([data-side="left"]):not([data-side="right"]) { position-area: bottom right; }
&[data-side="top"] { position-area: top right; }
&[data-side="left"] { position-area: left bottom; }
&[data-side="right"] { position-area: right bottom; }
}
}
@starting-style {
.popover:popover-open { opacity: 0; translate: 0 -4px; }
.popover[data-side="top"]:popover-open { opacity: 0; translate: 0 4px; }
.popover[data-side="left"]:popover-open { opacity: 0; translate: 4px 0; }
.popover[data-side="right"]:popover-open { opacity: 0; translate: -4px 0; }
}
.popover-header { margin-bottom: 0.75rem; }
.popover-title { margin: 0; font-size: 0.875rem; font-weight: 500; color: var(--foreground); }
.popover-description { margin: 0.25rem 0 0; font-size: 0.8125rem; color: var(--muted-foreground); }
.popover-content { font-size: 0.875rem; }
@media (prefers-reduced-motion: reduce) {
.popover {
transition: none;
}
}
}
JavaScript view file
Assigns unique CSS anchor names per trigger–popover pair. Opening, closing, and light-dismiss are handled entirely by the native popovertarget attribute.
// -- Popover --------------------------------------------------
// CSS anchor positioning for popover components.
function init() {
document.querySelectorAll('[popovertarget]:not([data-init])').forEach((trigger) => {
trigger.dataset.init = '';
const id = trigger.getAttribute('popovertarget');
const popover = document.getElementById(id);
if (!popover || !popover.classList.contains('popover')) return;
// CSS anchor positioning - unique name per trigger-popover pair
const anchorId = `--popover-${id}`;
trigger.style.anchorName = anchorId;
popover.style.positionAnchor = anchorId;
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });