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 });