Skip to content

Popover

OverlaysSince 0.2.0
import { Popover } from '@absoluteui/components/popover';
import '@absoluteui/components/popover/style.css';

Using Angular? See Popover for Angular.

A non-modal dialog anchored to a trigger button: the WAI-ARIA APG Disclosure pattern for the trigger, plus a non-modal dialog for the popup. The popup renders in the top layer through the Popover API, so overflow containers never clip it. It sits immediately after the trigger in the DOM, so Tab moves from the trigger into the popup and then on through the page. Escape and outside-pointer dismissal go through the shared overlay stack, so only the topmost overlay responds.

  • Contextual content or controls tied to a trigger: filters, quick settings, details, a small form.
  • Content the user may interact with (links, buttons, inputs) that should not block the rest of the page.
  • Tasks that must be completed or dismissed before continuing: use Dialog.
  • Short, non-interactive hints on hover or focus: use Tooltip.
  • A list of commands or choices: use Menu or Select.
  • Essential content users must not miss: keep it inline on the page.
import { Popover } from '@absoluteui/components/popover';
import { Button } from '@absoluteui/components/button';
import { Checkbox } from '@absoluteui/components/checkbox';
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.textContent = 'Filters';
Button.enhance(trigger);
document.body.append(trigger);
const form = document.createElement('div');
const onlyActive = document.createElement('div');
form.append(onlyActive);
Checkbox.create(onlyActive, { label: 'Only active' });
Popover.create(trigger, { title: 'Filters', content: form, showCloseButton: true });
import { Popover, type PopoverCloseDetail } from '@absoluteui/components/popover';
import { Button } from '@absoluteui/components/button';
import { Textarea } from '@absoluteui/components/textarea';
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.textContent = 'Add note';
Button.enhance(trigger);
document.body.append(trigger);
const noteHost = document.createElement('div');
const note = Textarea.create(noteHost, { label: 'Note' }).textarea;
const popover = Popover.create(trigger, { label: 'Add note', content: noteHost, placement: 'right-start' });
popover.element.addEventListener('aui-cancel', (event) => {
const { reason } = (event as CustomEvent<PopoverCloseDetail>).detail;
if (reason === 'outside-pointer' && note.value !== '') event.preventDefault();
});
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { popover: { placement: 'bottom-end', showCloseButton: true } } });
import { Popover, type PopoverCloseDetail } from '@absoluteui/components/popover';
import '@absoluteui/components/popover/style.css';
const trigger = document.querySelector<HTMLButtonElement>('#share');
if (trigger) {
const link = document.createElement('input');
link.readOnly = true;
link.value = location.href;
link.setAttribute('aria-label', 'Link');
const share = Popover.create(trigger, { title: 'Share', content: link, placement: 'bottom-end' });
share.element.addEventListener('aui-close', (event) => {
const { reason } = (event as CustomEvent<PopoverCloseDetail>).detail;
if (reason === 'escape') trigger.dataset.dismissed = 'true';
});
}
import { Popover } from '@absoluteui/components/popover';
const trigger = document.querySelector<HTMLButtonElement>('#filters');
if (trigger) Popover.create(trigger, { title: 'Filters', content: 'No filters applied.', showCloseButton: true });
<button aria-haspopup="dialog" aria-expanded aria-controls=popup> (consumer trigger)
<div class="aui-popover" role="dialog" popover="manual" tabindex="-1" aria-labelledby=title | aria-label>
<div class="aui-popover__header"> (title and/or close button)
<h2 class="aui-popover__title">Title</h2>
<button class="aui-button aui-popover__close" aria-label="Close"> (showCloseButton)
</div>
<div class="aui-popover__body">content</div>
</div>
Popover.create(trigger: HTMLElement, options: PopoverOptions): Popover

Global configuration key: popover (see Configuration).

Option Type Default Description
content Node | string Popup content; strings render as text. Required.
title string Visible heading; names the popup through aria-labelledby. Either title or label is required.
label string aria-label when there is no title.
icons Partial<{ close: IconRef }> Icon slot overrides.
placement Placement "bottom-start" Preferred side and logical alignment; flips and shifts to fit. Globally configurable.
offset number (0..64) 8 Gap between trigger and popup, px. Globally configurable.
closeOnEscape boolean true Escape requests close (cancelable). Escape is consumed while the popover is topmost either way. Globally configurable.
closeOnOutsidePointer boolean true Pointer down outside the popup and trigger requests close (cancelable). Globally configurable.
initialFocus 'first' | 'container' | 'none' "first" Where focus goes on open: first tabbable element (falls back to the container), the popup container, or nowhere. Globally configurable.
showCloseButton boolean false Render a localized close button in the header. Globally configurable.
direction 'ltr' | 'rtl' "ltr" Resolves logical start/end placements. Globally configurable. Inherited from common configuration.
Property Type Description
element HTMLDivElement The popup (div.aui-popover[role=dialog]).
body HTMLDivElement Content container.
trigger HTMLElement The trigger element.
isOpen boolean Whether the popup is open.
options Readonly<PopoverOptions> Current options.
Method Description
open(): void Show, position, and move focus per initialFocus.
close(): void Close programmatically (not cancelable). Focus inside the popup returns to the trigger.
toggle(): void Open or close programmatically.
update(options: Partial<PopoverOptions>): void Merge and apply options; repositions when open.
destroy(): void Close if open; remove the popup, trigger ARIA attributes, listeners, and subscriptions.
Event Detail Bubbles Cancelable Description
aui-open null No No After the popup opened (dispatched on the popup element).
aui-cancel { reason: 'escape' | 'outside-pointer' | 'focus-out' | 'trigger' | 'close-button' } No Yes Before a user-initiated close; preventDefault keeps it open.
aui-close { reason: PopoverCloseReason } No No After the popup closed.
Area Description
body Popover.body (or the content option).

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-popover Popup element (manual popover, top layer).
aui-popover--static Set when animation is disabled globally.
aui-popover__header Title row with the optional close button.
aui-popover__title Heading (h2).
aui-popover__close Close button.
aui-popover__body Content container.
Slot Default icon
close close

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-focus-ring | --aui-color-surface | --aui-color-text | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-weight-semibold | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-space-4

type Placement = 'top' | 'bottom' | 'left' | 'right' | `${'top' | 'bottom' | 'left' | 'right'}-${'start' | 'end'}`;
type PopoverInitialFocus = 'first' | 'container' | 'none';
type PopoverCloseReason = 'escape' | 'outside-pointer' | 'focus-out' | 'trigger' | 'close-button' | 'programmatic';
interface PopoverCloseDetail { reason: PopoverCloseReason }
interface PopoverOptions extends Partial<PopoverConfig> { content: Node | string; title?: string; label?: string; direction?: 'ltr' | 'rtl'; icons?: Partial<PopoverIcons> }

Pattern: WAI-ARIA APG Disclosure (trigger) with a non-modal Dialog (popup).

Key Context Action
Enter / Space Trigger focused Toggles the popup; on open, focus moves per initialFocus.
Tab / Shift+Tab Popup open The popup follows the trigger in the tab order; tabbing past its last control (or back before the trigger) closes it (focus-out).
Escape Popup open and topmost, closeOnEscape=true Emits cancelable aui-cancel, closes, and returns focus to the trigger.
Escape Popup open inside a Dialog Closes only the popover; a second Escape closes the dialog.
Enter / Space Close button focused Closes and returns focus to the trigger.
Element Role
popup div dialog (non-modal)
title heading level 2
trigger button (consumer-provided, native <button> recommended)
Attribute Element Value
aria-haspopup trigger dialog
aria-expanded trigger true while open, false otherwise
aria-controls trigger id of the popup
aria-labelledby popup id of the title heading (when title is set)
aria-label popup label option (when there is no title)
aria-label close button Localized ‘Close’

On open, focus moves to the first tabbable element in the popup (initialFocus ‘first’, falling back to the popup), to the popup itself (‘container’), or stays on the trigger (‘none’). Escape and the close button return focus to the trigger; an outside pointer or focus moving elsewhere leaves focus where the user put it; a programmatic close moves focus to the trigger only if it was inside the popup.

Criterion Level Note
1.3.1 Info and Relationships A
1.3.2 Meaningful Sequence A
2.1.1 Keyboard A
2.1.2 No Keyboard Trap A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • The popup is placed immediately after the trigger in the DOM so reading and tab order match the visual relationship (1.3.2, 2.4.3).
  • Non-modal: the rest of the page stays interactive and is not inert.
  • Top-layer rendering avoids clipping by overflow containers.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.

closed, open, closing (aui-cancel pending).

From Trigger To
closed trigger click, Enter, or Space; open() open; focus per initialFocus; aui-open
open trigger click aui-cancel, then closed (trigger)
open Escape while topmost aui-cancel, then closed (escape); focus to trigger
open pointer down outside popup and trigger aui-cancel, then closed (outside-pointer); focus not moved
open focus moves outside popup and trigger aui-cancel, then closed (focus-out)
open close button aui-cancel, then closed (close-button); focus to trigger
open close(), toggle(), destroy() closed (programmatic), not cancelable
  • A missing accessible name is a configuration error.
  • DOM order matches the visual relationship, so reading and tab order continue naturally (WCAG 1.3.2, 2.4.3).
  • The background stays interactive: this is not a focus trap.
  • With initialFocus: 'first' and no focusable content, focus moves to the popup itself (tabindex="-1").
  • The trigger should be a native <button>.
  • Manual screen-reader verification is pending (see Known limitations).

Content width up to min(24rem, 100vw - 2rem). The block size is capped at the space available on the chosen side, and the popup scrolls. The positioning engine flips and shifts to stay in the viewport.

start and end placements are logical. With direction: 'rtl', bottom-start aligns the popup to the trigger’s right edge. Header spacing uses logical properties.

Colors come from role tokens only. Verified in the light, dark, and high-contrast themes. In forced-colors mode the popup has a CanvasText border. The open fade uses @starting-style and is removed under reduced motion or animation: 'none'.

configureAbsoluteUI({ components: { popover: { placement: 'bottom-end', showCloseButton: true } } });

Changes apply to existing instances immediately (open popovers are repositioned).

Instance options override global and theme defaults. All options are dynamic.

icons.close (default close).

Title, label, and string content use textContent; they are never parsed as HTML. Node content is inserted as given, so the consumer owns its safety. CSP-safe: positioning uses the CSSOM.

No persistence, network access, or telemetry.

Three listeners while closed (trigger click, two focusout) and one configuration subscription. While open: one overlay-stack entry plus resize observers and scroll/resize listeners from the positioning engine, all released on close. Budget: open to positioned within one frame.

Any number of closed instances per page. Keep one popover open at a time; nesting one level (a popover in a Dialog, or a tooltip in a popover) is tested.

  • Clicking the trigger while open closes it with reason trigger (the pointer is treated as inside, so no outside-pointer close happens first).
  • closeOnEscape: false still consumes Escape while the popover is topmost, so an enclosing Dialog does not close.
  • Removing the close button (update({ showCloseButton: false })) while it has focus moves focus to the popup.
  • Focus leaving the browser window (no relatedTarget) does not close the popup.
Code Category Cause
POPOVER_TRIGGER configuration The trigger is not a connected HTMLElement.
POPOVER_OPTIONS configuration Missing or invalid content, or neither title nor label.
CFG_* configuration Invalid placement, offset, initialFocus, or boolean options.

A failed construction leaves no rendering, trigger attributes, listeners, or subscriptions behind. A failed update() keeps the previous options.

  • Enter and Space activation relies on the trigger being a native <button>.
  • The popup is not portaled; it inherits the trigger’s container styles (such as dir and font).
  • The heading is always an h2.
  • The close animation is not played.
  • Canceling aui-cancel for a focus-out close leaves the popup open while focus is elsewhere.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • Modal behavior (inert background, focus trap): use Dialog.
  • HTML strings as content.
  • Opening on hover.