Skip to content

HyPopover ​

Usage examples on this page are written for Lit / plain HTML (<hy-popover>). The same component ships as HyPopover in @whitespaceux/harmony-react (native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.

A non-modal overlay for contextual content, built on the Native Popover API (using popover="manual" mode) with Floating UI for automatic placement. Light dismiss (click outside, Escape key) is handled by the component itself for reliable cross-browser behavior.

Trigger Patterns ​

Slotted trigger (preferred): The trigger element is passed via the trigger slot, ensuring ARIA relationships work correctly across shadow DOM boundaries. The trigger automatically toggles the popover on click.

for attribute (fallback): When the trigger needs to be outside the popover element (e.g., in SSR frameworks), use the for attribute to reference a trigger element by ID. The popover wires the click-to-toggle on that element itself, exactly as it does for a slotted trigger — do not add a click handler of your own that calls toggle(), or every click toggles twice and the popover never opens.

anchor attribute (a separate positioning element): By default the popover is placed against its trigger. When it should be placed against a larger element that holds the trigger — a field row whose button opens a calendar — give anchor that element's ID. The trigger keeps the click-to-toggle, the ARIA and the focus return; the anchor only positions the popover, and a click on it is not a light dismiss. Resolved like for.

Positioning Technology ​

This component uses Floating UI for positioning instead of CSS Anchor Positioning.

Popover vs Dialog ​

A popover is NOT a dialog. Use hy-popover for small, non-modal content beside the control that opened it, such as a short form or details. Use hy-dialog for modal interactions that require user attention and response.

Semantic Role ​

The panel is a dialog unless role says otherwise: a popover takes focus when it opens, and focus moving into an unnamed, roleless region tells a screen reader nothing. An explicit role always wins:

  • role="menu" - Action menus, context menus
  • role="listbox" - Selection dropdowns
  • role="tree" / role="grid" - Hierarchical or tabular pickers
  • role="tooltip" - Tooltips and toggletips (never takes focus)
  • role="dialog" - The default, stated explicitly
  • role="none" - No role: the content provides its own semantics (e.g., when wrapping a component that already has proper ARIA)

The role sits on the inner panel only. The host never carries it: a role attribute written on <hy-popover> is read as the requested role and then taken off the host, so the accessibility tree meets one node (a host role too put a second, unnamed menu / dialog around the named panel). Read the current role from the role property; to go back to the default, set the property to null (there is no host attribute to remove).

The trigger gets aria-haspopup (the role, or true for none), aria-controls and aria-expanded — each only when the trigger does not already carry it: an attribute the consumer wrote is never overwritten.

Accessibility: built in ​

  • Full keyboard navigation (Escape to close)
  • Light dismiss (click outside to close)
  • ARIA live regions for screen reader announcements (opt-in)
  • Automatic ARIA relationships with trigger element:
    • For tooltips: aria-describedby (tooltip describes the trigger)
    • For other roles: aria-haspopup, aria-controls and aria-expanded
  • Role-aware focus management:
    • Tooltips: Focus stays on trigger (tooltips are supplementary)
    • Other roles: on open, focus moves to the slotted [autofocus] marker, else the first focusable content, else the panel; it returns on close
    • The public focusSettled promise resolves once the open's initial-focus sequence has settled (deterministic await for tests)
  • High contrast mode support
  • Reduced motion support

Accessibility: your part ​

  • Give a popover that holds controls a label.
  • Use a focusable control such as hy-button or hy-icon-button as the trigger, never a bare div.

When to use

  • Extra content that belongs to the control that opened it: details, a short form of one or two fields, a picker.
  • A quick look at more information that people dismiss by moving on.

Do

  • Keep its content short enough to read without scrolling.
  • Open it from the control it belongs to, with for or the trigger slot.

When not to use

  • Short text shown on hover or focus, with nothing to interact with: hy-tooltip.
  • A multi-field form, scrolling content, or a popover opened from inside another: hy-drawer.
  • Brief feedback such as saved or copied: hy-toast.
  • A button that opens a list of actions: hy-menu-button.

Examples ​

Basic Usage (Slotted Trigger - Preferred) ​

html
<hy-popover>
  <button slot="trigger">Open</button>
  <p>Popover content</p>
</hy-popover>

Fallback Usage (for attribute) ​

html
<button id="trigger">Open</button>
<!-- `for` wires the trigger's click, ARIA and focus return, like a slotted trigger. -->
<hy-popover for="trigger">
  <p>Popover content</p>
</hy-popover>

Placement Options ​

html
<hy-popover placement="top">
  <button slot="trigger">Open</button>
  Top positioned content
</hy-popover>

Without Arrow ​

html
<hy-popover no-arrow>
  <button slot="trigger">Open</button>
  <p>No arrow indicator</p>
</hy-popover>
html
<hy-popover role="menu" label="User actions">
  <button slot="trigger">Menu</button>
  <button role="menuitem">Edit Profile</button>
  <button role="menuitem">Settings</button>
  <button role="menuitem">Logout</button>
</hy-popover>

Tooltip Popover ​

html
<hy-popover role="tooltip" no-arrow>
  <span slot="trigger" tabindex="0">?</span>
  Press Enter to submit the form
</hy-popover>

Preventing Close ​

html
<hy-popover id="popover">
  <button slot="trigger">Open</button>
  Content
</hy-popover>
<script>
  document.getElementById('popover').addEventListener('request-hide', (e) => {
    if (hasUnsavedChanges) {
      e.preventDefault();
    }
  });
</script>

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
openopenbooleanfalseControls the open state of the popover.
forforstring''The id of the element that anchors this popover. Use this as a fallback when the slotted trigger pattern isn't available (e.g., when the trigger needs to be outside the popover element).
anchoranchorstring''The id of the element the popover is positioned against, when it is not the trigger — e.g. a field row whose calendar button is the trigger. The trigger keeps the click-to-toggle, the ARIA and the focus return; a click on the anchor is not a light dismiss. Resolved like for (through enclosing shadow roots, then the document). Empty: the trigger positions.
placementplacement'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end''bottom'Where to place the popover relative to the anchor. Uses Floating UI with automatic fallback (flip, shift) when there isn't enough space.
distancedistancenumber8Distance between the popover and its anchor element in pixels. This creates a gap between the anchor and the popover.
skiddingskiddingnumber0Offset along the anchor's edge in pixels. Positive values move the popover in the direction of the text flow.
noArrowno-arrowbooleanfalseHides the arrow indicator when true.
labellabelstring''Accessible label for the popover. Applied as aria-label on the popover container for screen readers.
popoverIdpopover-idstring''Auto-generated unique identifier for the popover element. Used to establish ARIA relationships between trigger and popover.
announceStateChangesannounce-state-changesbooleanfalseWhether to announce state changes to screen readers via ARIA live regions. When enabled, announces "opened" and "closed" states.
announcementPriorityannouncement-priority'polite' | 'assertive''polite'ARIA live region politeness level for screen reader announcements. - polite: Announcements wait for current reading to complete (recommended) - assertive: Announcements interrupt current reading
openedLabelopened-labelstring'{label} opened'The announcement read when the popover opens (with announce-state-changes); {label} is the popover's label. Use for internationalization.
closedLabelclosed-labelstring'{label} closed'The announcement read when the popover closes (with announce-state-changes); {label} is the popover's label. Use for internationalization.
fallbackLabelfallback-labelstring'Popover'The name put in place of {label} in the opened / closed announcements when label is unset. Use for internationalization.
blockedLabelblocked-labelstring'Action blocked'The announcement read when a close is canceled through request-hide (with announce-state-changes). Use for internationalization.
rolerole'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'tooltip' | 'none'nullARIA role for the popover element. Should match the semantic purpose of the content.
describedBydescribed-bystring | undefined—ID of an element that describes the popover content. Applied as aria-describedby on the popover container.
focusSettled—Promise<void>—Resolves once the current open's initial-focus sequence has settled — discovery ran and focus was applied, or the sequence was skipped (tooltip role, prevented request-show / initial-focus, closed before the deferred work ran). Before the first open it is already resolved.

Events ​

EventDetailDescription
request-show—Emitted before opening; cancelable to prevent open. Detail: { source } — click, programmatic.
show—Emitted after the popover opens. Detail: { source }.
request-hide—Emitted before closing; cancelable to prevent close. Detail: { source } — click, escape-key, light-dismiss (outside click), programmatic.
hide—Emitted after the popover closes. Detail: { source }.
initial-focus—Fired on show (non-tooltip role) before the autofocus discovery runs. Cancelable: call event.preventDefault() to skip the default focus and apply your own. Detail: HyInitialFocusDetail

Slots ​

SlotDescription
triggerThe element that triggers the popover (automatically toggles on click)
defaultThe content to display inside the popover.

CSS Parts ​

PartDescription
baseThe popover container element (has popover attribute).
bodyThe visible content wrapper with background and border.
arrowThe arrow indicator element.
live-regionThe ARIA live region for screen reader announcements (visually hidden).

CSS Custom Properties ​

The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.

PropertyDefaultDescription
--hy-popover-surface-width-mdvar(--hy-surface-width-md)Maximum width of the popover
--hy-popover-padding-overlay-floating-md-blockvar(--hy-padding-overlay-floating-md-block)Block (top and bottom) padding of the popover body
--hy-popover-padding-overlay-floating-md-inlinevar(--hy-padding-overlay-floating-md-inline)Inline (start and end) padding of the popover body
--hy-popover-radius-overlay-floating-mdvar(--hy-radius-overlay-floating-md)Border radius
--hy-popover-stroke-smvar(--hy-stroke-sm)Border width
--hy-popover-border-elevated-restvar(--hy-border-elevated-rest)Border color (the rim of the floating surface)
--hy-popover-ornament-bulletvar(--hy-ornament-bullet)Size of the arrow indicator
--hy-popover-shadow-elevation-4var(--hy-shadow-elevation-4)Box shadow for the popover body

Methods ​

show() ​

Shows the popover. Emits request-show before opening (cancelable). Emits show after opened.

hide() ​

Hides the popover. Emits request-hide before closing (cancelable). Emits hide after closed.

toggle() ​

Toggles the popover open/closed state. Emits the appropriate request event (cancelable) before toggling.

reposition() ​

Manually triggers a position update. Useful after content changes that affect popover size.

focus() ​

Focuses the popover's first focusable content — slotted content included, walked in rendered order (through nested slots and open shadow roots, so a slotted hy-button resolves to its inner button) — or, when nothing inside can take focus, the panel itself (given tabindex="-1" so it stays out of the Tab order). This is also the default initial focus on open when no [autofocus] marker is set.

blur() ​

Removes focus from the popover.

Built with Lit. Documented with VitePress.