HyPopover
Usage examples on this page are written for Lit / plain HTML (
<hy-popover>). The same component ships asHyPopoverin@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 menusrole="listbox"- Selection dropdownsrole="tree"/role="grid"- Hierarchical or tabular pickersrole="tooltip"- Tooltips and toggletips (never takes focus)role="dialog"- The default, stated explicitlyrole="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-controlsandaria-expanded
- For tooltips:
- 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
focusSettledpromise 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-buttonorhy-icon-buttonas the trigger, never a barediv.
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
foror thetriggerslot.
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)
<hy-popover>
<button slot="trigger">Open</button>
<p>Popover content</p>
</hy-popover>Fallback Usage (for attribute)
<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
<hy-popover placement="top">
<button slot="trigger">Open</button>
Top positioned content
</hy-popover>Without Arrow
<hy-popover no-arrow>
<button slot="trigger">Open</button>
<p>No arrow indicator</p>
</hy-popover>Menu Popover
<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
<hy-popover role="tooltip" no-arrow>
<span slot="trigger" tabindex="0">?</span>
Press Enter to submit the form
</hy-popover>Preventing Close
<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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
open | open | boolean | false | Controls the open state of the popover. |
for | for | string | '' | 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). |
anchor | anchor | string | '' | 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. |
placement | placement | '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. |
distance | distance | number | 8 | Distance between the popover and its anchor element in pixels. This creates a gap between the anchor and the popover. |
skidding | skidding | number | 0 | Offset along the anchor's edge in pixels. Positive values move the popover in the direction of the text flow. |
noArrow | no-arrow | boolean | false | Hides the arrow indicator when true. |
label | label | string | '' | Accessible label for the popover. Applied as aria-label on the popover container for screen readers. |
popoverId | popover-id | string | '' | Auto-generated unique identifier for the popover element. Used to establish ARIA relationships between trigger and popover. |
announceStateChanges | announce-state-changes | boolean | false | Whether to announce state changes to screen readers via ARIA live regions. When enabled, announces "opened" and "closed" states. |
announcementPriority | announcement-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 |
openedLabel | opened-label | string | '{label} opened' | The announcement read when the popover opens (with announce-state-changes); {label} is the popover's label. Use for internationalization. |
closedLabel | closed-label | string | '{label} closed' | The announcement read when the popover closes (with announce-state-changes); {label} is the popover's label. Use for internationalization. |
fallbackLabel | fallback-label | string | 'Popover' | The name put in place of {label} in the opened / closed announcements when label is unset. Use for internationalization. |
blockedLabel | blocked-label | string | 'Action blocked' | The announcement read when a close is canceled through request-hide (with announce-state-changes). Use for internationalization. |
role | role | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'tooltip' | 'none' | null | ARIA role for the popover element. Should match the semantic purpose of the content. |
describedBy | described-by | string | 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
| Event | Detail | Description |
|---|---|---|
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
| Slot | Description |
|---|---|
trigger | The element that triggers the popover (automatically toggles on click) |
default | The content to display inside the popover. |
CSS Parts
| Part | Description |
|---|---|
base | The popover container element (has popover attribute). |
body | The visible content wrapper with background and border. |
arrow | The arrow indicator element. |
live-region | The 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.
| Property | Default | Description |
|---|---|---|
--hy-popover-surface-width-md | var(--hy-surface-width-md) | Maximum width of the popover |
--hy-popover-padding-overlay-floating-md-block | var(--hy-padding-overlay-floating-md-block) | Block (top and bottom) padding of the popover body |
--hy-popover-padding-overlay-floating-md-inline | var(--hy-padding-overlay-floating-md-inline) | Inline (start and end) padding of the popover body |
--hy-popover-radius-overlay-floating-md | var(--hy-radius-overlay-floating-md) | Border radius |
--hy-popover-stroke-sm | var(--hy-stroke-sm) | Border width |
--hy-popover-border-elevated-rest | var(--hy-border-elevated-rest) | Border color (the rim of the floating surface) |
--hy-popover-ornament-bullet | var(--hy-ornament-bullet) | Size of the arrow indicator |
--hy-popover-shadow-elevation-4 | var(--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.