Skip to content

cv-popover

Non-modal overlay anchored to a trigger element for contextual content such as menus, tooltips, or forms.

Headless: createPopover

Usage

View source
html
<div class="popover-demo-shell" data-demo="popover" data-live-demo-inline>
  <section class="popover-demo-hero" aria-labelledby="popover-demo-title">
    <div class="popover-demo-copy">
      <span class="popover-demo-kicker">Anchored non-modal overlay</span>
      <h3 id="popover-demo-title">Expose dense controls without leaving the current task</h3>
      <p>
        Use popovers for contextual actions, short forms, route details, or guidance that must stay tied to a
        visible trigger.
      </p>
    </div>

    <div class="popover-demo-stage" aria-label="Vault route toolbar example">
      <div class="popover-demo-toolbar">
        <span class="popover-demo-status">Visible route: public vault</span>

        <cv-popover
          class="popover-demo-primary"
          arrow
          placement="bottom-start"
          offset="10"
          aria-label="Exposure route controls"
        >
          <span slot="trigger">Exposure route</span>
          <div class="popover-demo-panel">
            <div class="popover-demo-panel-header">
              <span class="popover-demo-label">Route state</span>
              <strong>Decoy surface active</strong>
            </div>
            <dl class="popover-demo-readout">
              <div>
                <dt>Anchor</dt>
                <dd>trigger</dd>
              </div>
              <div>
                <dt>Dismiss</dt>
                <dd>Escape, outside pointer, outside focus</dd>
              </div>
              <div>
                <dt>Placement</dt>
                <dd>bottom-start + arrow</dd>
              </div>
            </dl>
            <div class="popover-demo-actions">
              <cv-button variant="primary">Confirm route</cv-button>
              <cv-button variant="secondary">Inspect policy</cv-button>
            </div>
          </div>
        </cv-popover>

        <cv-popover arrow placement="bottom-end" offset="10" aria-label="Audit controls">
          <span slot="trigger">Audit</span>
          <div class="popover-demo-compact">
            <strong>Last check passed</strong>
            <p>Native popover support is used when available; the headless model still owns state.</p>
          </div>
        </cv-popover>
      </div>

      <div class="popover-demo-vault">
        <div>
          <span>Public layer</span>
          <strong>2 files visible</strong>
        </div>
        <div>
          <span>Hidden layer</span>
          <strong>not mounted</strong>
        </div>
        <div>
          <span>Hardware core</span>
          <strong>paired</strong>
        </div>
      </div>
    </div>
  </section>

  <section class="popover-demo-matrix" aria-labelledby="popover-demo-matrix-title">
    <div class="popover-demo-section-header">
      <span class="popover-demo-kicker">Placement variants</span>
      <h4 id="popover-demo-matrix-title">Same contract, different anchor geometry</h4>
    </div>

    <div class="popover-demo-cases">
      <article>
        <span class="popover-demo-label">Top</span>
        <p>Useful for bottom toolbars and compact inspector controls.</p>
        <cv-popover arrow placement="top" offset="8">
          <span slot="trigger">Open top</span>
          <div class="popover-demo-compact">
            <strong>Policy note</strong>
            <p>Arrow position follows the resolved placement.</p>
          </div>
        </cv-popover>
      </article>

      <article>
        <span class="popover-demo-label">Right</span>
        <p>Use beside lists, tree items, and narrow rail controls.</p>
        <cv-popover arrow placement="right-start" offset="8">
          <span slot="trigger">Open right</span>
          <div class="popover-demo-compact">
            <strong>Quick action</strong>
            <p>Panel size is clamped to the viewport.</p>
          </div>
        </cv-popover>
      </article>

      <article>
        <span class="popover-demo-label">Host anchor</span>
        <p>Use when the panel should align to the component box instead of the trigger button.</p>
        <cv-popover anchor="host" placement="bottom-end" offset="8">
          <span slot="trigger">Host aligned</span>
          <div class="popover-demo-compact">
            <strong>Aligned to host</strong>
            <p>The <code>anchor</code> attribute changes the positioning reference.</p>
          </div>
        </cv-popover>
      </article>
    </div>
  </section>
</div>

Anatomy

<cv-popover> (host)
└── <div part="base">
    ├── <button part="trigger" type="button">
    │   └── <slot name="trigger">
    ├── <div part="content" role="dialog">
    │   ├── <slot>
    │   └── <span part="arrow">
    │       └── <slot name="arrow">
    └── (document-level listeners for outside dismiss)

Attributes

AttributeTypeDefaultDescription
openBooleanfalseWhether the popover content is visible
placementString"bottom-start"Content placement relative to the anchor: top | top-start | top-end | bottom | bottom-start | bottom-end | left | left-start | left-end | right | right-start | right-end
anchorString"trigger"Positioning reference: trigger | host
offsetNumber4Distance (in px) between anchor and content
arrowBooleanfalseShow an arrow pointing toward the anchor
close-on-escapeBooleantrueWhether Escape key closes the popover
close-on-outside-pointerBooleantrueWhether clicking outside closes the popover
close-on-outside-focusBooleantrueWhether focusing outside closes the popover
aria-labelString""Accessible label for content panel
aria-labelledbyString""Id of element labelling the content panel

Slots

SlotDescription
(default)Popover content
triggerTrigger element (defaults to a styled button)
arrowCustom arrow content (replaces default arrow)

CSS Parts

PartElementDescription
base<div>Root layout wrapper with position: relative
trigger<button>Trigger element that opens/closes the popover
content<div>Popover content panel with role="dialog"
arrow<span>Arrow element pointing toward the anchor

CSS Custom Properties

PropertyDefaultDescription
--cv-popover-offsetvar(--cv-space-1, 4px)Distance between anchor and content
--cv-popover-min-inline-sizemax(220px, 100%)Minimum width of content panel
--cv-popover-max-inline-sizemin(560px, calc(100vw - 32px))Maximum width of content panel
--cv-popover-paddingvar(--cv-space-3, 12px)Content panel padding
--cv-popover-border-radiusvar(--cv-radius-md, 10px)Content panel border radius
--cv-popover-z-index20Content panel stacking order
--cv-popover-arrow-size8pxWidth and height of the arrow element

Additionally, component styles depend on theme tokens through fallback values:

Theme PropertyDefaultDescription
--cv-color-border#2a3245Border color for trigger and content
--cv-color-surface#141923Trigger background color
--cv-color-surface-elevated#1d2432Content panel background color
--cv-color-text#e8ecf6Default text color
--cv-color-primary#65d7ffFocus ring color
--cv-shadow-10 2px 8px rgba(0, 0, 0, 0.24)Content panel box shadow
--cv-radius-sm6pxTrigger border radius fallback
--cv-radius-md10pxContent border radius fallback
--cv-space-14pxSmall spacing scale fallback
--cv-space-28pxMedium spacing scale fallback
--cv-space-312pxMedium-large spacing scale fallback

Visual States

Host selectorDescription
:host([open])Content panel is visible
:host(:not([open]))Content panel is hidden
:host([placement="top"])Content positioned above anchor, centered
:host([placement="top-start"])Content positioned above anchor, start-aligned
:host([placement="top-end"])Content positioned above anchor, end-aligned
:host([placement="bottom"])Content positioned below anchor, centered
:host([placement="bottom-start"])Content positioned below anchor, start-aligned (default)
:host([placement="bottom-end"])Content positioned below anchor, end-aligned
:host([placement="left"])Content positioned to the left, centered
:host([placement="left-start"])Content positioned to the left, start-aligned
:host([placement="left-end"])Content positioned to the left, end-aligned
:host([placement="right"])Content positioned to the right, centered
:host([placement="right-start"])Content positioned to the right, start-aligned
:host([placement="right-end"])Content positioned to the right, end-aligned
:host([anchor="host"])Content positioned relative to host instead of trigger
:host([arrow])Arrow element is visible

Events

EventDetailCancelableDescription
beforetoggle{open: boolean, openedBy: string | null, dismissIntent: string | null}Yes (on open)Fires before the open state changes. Canceling (via preventDefault()) when opening prevents the popover from opening. Not cancelable on close.
toggle{open: boolean, openedBy: string | null, dismissIntent: string | null}NoFires after the open state has changed

Event detail fields:

  • open — the new visibility state (true when opening, false when closing)
  • openedBy — how the popover was opened: "keyboard" | "pointer" | "programmatic" | null
  • dismissIntent — why the popover was closed: "escape" | "outside-pointer" | "outside-focus" | "programmatic" | null

Reactive State Mapping

cv-popover is a visual adapter over headless createPopover.

UIKit PropertyDirectionHeadless Binding
openattr -> actionactions.open(source) / actions.close(intent) based on boolean value
close-on-escapeattr -> optioncloseOnEscape passed to createPopover(options)
close-on-outside-pointerattr -> optioncloseOnOutsidePointer passed to createPopover(options)
close-on-outside-focusattr -> optioncloseOnOutsideFocus passed to createPopover(options)
aria-labelattr -> optionariaLabel passed to createPopover(options)
aria-labelledbyattr -> optionariaLabelledBy passed to createPopover(options)
Headless StateDirectionDOM Reflection
state.isOpen()state -> attr[open] host attribute
state.openedBy()state -> eventIncluded in beforetoggle / toggle event detail
state.lastDismissIntent()state -> eventIncluded in beforetoggle / toggle event detail
state.restoreTargetId()state -> DOMFocus restored to trigger element after close
state.useNativePopover()state -> DOMControls native showPopover() / hidePopover() calls
  • contracts.getTriggerProps() is spread onto the inner [part="trigger"] element to apply role, aria-haspopup, aria-expanded, aria-controls, tabindex, popovertarget (when native), and keyboard/click handlers.
  • contracts.getContentProps() is spread onto the inner [part="content"] element to apply role, aria-modal, aria-label, aria-labelledby, tabindex, hidden (when manual mode), popover="manual" (when native), and keyboard/outside-dismiss handlers.
  • UIKit dispatches beforetoggle (cancelable on open) and toggle events by observing isOpen changes from user activation.
  • UIKit does not own open/close, keyboard, or dismiss logic; headless state is the source of truth.
  • When beforetoggle is canceled on open, UIKit calls actions.close() to revert headless state.

Native Popover API Progressive Enhancement

UIKit auto-detects native Popover API support via feature check (typeof HTMLElement.prototype.showPopover === 'function'). When supported:

  1. useNativePopover: true is passed to createPopover(options).
  2. Content element receives popover="manual" attribute (from headless contract) instead of hidden.
  3. UIKit calls contentElement.showPopover() when state.isOpen() transitions to true.
  4. UIKit calls contentElement.hidePopover() when state.isOpen() transitions to false.
  5. UIKit listens for native toggle events on the content element and calls actions.handleNativeToggle(newState) to synchronize headless state.

When the native Popover API is not available, the component falls back to hidden attribute-based visibility management. Behavior is identical in both modes; the headless layer manages all open/close logic regardless.

Placement (UIKit-only)

Placement is CSS-only (no Floating UI). The placement attribute maps to data-placement on the content element, which drives absolute positioning rules via CSS selectors. The anchor attribute controls whether the content panel is positioned relative to the trigger button or the host element. The offset attribute maps to --cv-popover-offset.

Arrow (UIKit-only)

When the arrow boolean attribute is present, the [part="arrow"] element is rendered inside the content panel. It is positioned via CSS to point toward the anchor, with its direction derived from the current placement. The arrow slot allows custom arrow content. Arrow size is controlled by --cv-popover-arrow-size.

ChromVoid UIKit documentation