Hover Card
import { HoverCard } from '@absoluteui/components/hover-card';import '@absoluteui/components/hover-card/style.css';Using Angular? See Hover Card for Angular.
What it does
Section titled “What it does”A rich, non-modal preview shown while a trigger (usually a link) is hovered or has keyboard focus. It meets WCAG 1.4.13 (Content on Hover or Focus): it is dismissible with Escape, hoverable (the pointer can move onto the card), and persistent until pointer and focus leave both trigger and card. The card is inserted right after the trigger and shown in the top layer, so Tab moves from the trigger into interactive card content and then on through the page; focus inside the card keeps it open. The trigger references the card with aria-describedby by default. It never opens on touch (a tap follows the link), and only one hover card is visible at a time. Hover cards are supplementary: never put essential information or actions only in a hover card.
When to use
Section titled “When to use”- Supplementary previews of a linked object: a user profile, a repository, a document summary.
- Richer content than a Tooltip allows (headings, images, links), where the same information is reachable by following the trigger.
When not to use
Section titled “When not to use”- Essential information or actions: touch users never see hover cards; put them on the page or behind the link.
- Short text hints: use Tooltip.
- Content the user must open deliberately or interact with at length (forms): use Popover.
- Triggers that are not focusable: keyboard users could never open the card.
Examples
Section titled “Examples”Profile preview on a link
Section titled “Profile preview on a link”import { HoverCard } from '@absoluteui/components/hover-card';
const link = document.querySelector<HTMLAnchorElement>('a[href="/u/ada"]');if (link) { const content = document.createElement('div'); const name = document.createElement('p'); name.textContent = 'Ada Lovelace'; const role = document.createElement('p'); role.textContent = 'Mathematician, London'; content.append(name, role); HoverCard.create(link, { content, placement: 'bottom-start' });}Interactive card that is not used as the trigger’s description
Section titled “Interactive card that is not used as the trigger’s description”import { HoverCard, type HoverCardCloseDetail } from '@absoluteui/components/hover-card';import { Button } from '@absoluteui/components/button';
const trigger = document.createElement('a');trigger.href = '/repos/atlas';trigger.textContent = 'atlas';document.body.append(trigger);
const content = document.createElement('div');const summary = document.createElement('p');summary.textContent = 'Mapping toolkit. 1.2k stars.';const star = document.createElement('button');star.type = 'button';star.textContent = 'Star';Button.enhance(star, { size: 'sm' });content.append(summary, star);
// Tab moves from the trigger into the card, so the Star button is keyboard reachable.const card = HoverCard.create(trigger, { content, describe: false, showDelay: 400, hideDelay: 400 });card.element.addEventListener('aui-close', (event) => { const { reason } = (event as CustomEvent<HoverCardCloseDetail>).detail; trigger.dataset.cardClosed = reason;});Global defaults
Section titled “Global defaults”import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { hoverCard: { placement: 'top', showDelay: 800, hideDelay: 200 } } });import { HoverCard, type HoverCardCloseDetail } from '@absoluteui/components/hover-card';import '@absoluteui/components/hover-card/style.css';
const link = document.querySelector<HTMLAnchorElement>('a[href="/u/ada"]');if (link) { const content = document.createElement('div'); const bio = document.createElement('p'); bio.textContent = 'Mathematician, London.'; const follow = document.createElement('button'); follow.type = 'button'; follow.textContent = 'Follow'; content.append(bio, follow); const card = HoverCard.create(link, { content, describe: false, placement: 'bottom-start' }); card.element.addEventListener('aui-close', (event) => { const { reason } = (event as CustomEvent<HoverCardCloseDetail>).detail; if (reason === 'escape') link.dataset.cardDismissed = 'true'; });}More usage
Section titled “More usage”import { HoverCard } from '@absoluteui/components/hover-card';
const link = document.querySelector<HTMLAnchorElement>('a[href="/u/ada"]');if (link) HoverCard.create(link, { content: 'Ada Lovelace, mathematician. Follow the link for the full profile.' });Anatomy
Section titled “Anatomy”<a href="/u/ada" aria-describedby=card>Ada</a> (consumer trigger)<div class="aui-hover-card" id=card popover="manual"> (inserted right after the trigger, top layer) content (the content option: text or a Node)</div>API reference
Section titled “API reference”Factory
Section titled “Factory”HoverCard.create(trigger: HTMLElement, options: HoverCardOptions): HoverCardGlobal configuration key: hoverCard (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
content |
Node | string |
Card content. Strings render as text (non-empty); Nodes are inserted as given. Inserted at construction and again only when update() passes content. Required. | |
describe |
boolean |
true |
The trigger references the card with aria-describedby (added to existing ids). Set false for long or highly interactive cards. |
placement |
Placement |
"bottom" |
Preferred side and logical alignment; flips and shifts to fit. Globally configurable. |
showDelay |
integer (0..5000) |
600 |
Delay before showing on hover or keyboard focus, ms. 0 shows synchronously. Globally configurable. |
hideDelay |
integer (0..5000) |
300 |
Grace period after the pointer leaves trigger and card before hiding, ms. Always asynchronous, so the pointer can move onto the card. Globally configurable. |
direction |
'ltr' | 'rtl' |
"ltr" |
Resolves logical start/end placements. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
element |
HTMLDivElement |
The card (div.aui-hover-card, popover=manual), inserted right after the trigger. |
trigger |
HTMLElement |
The trigger element. |
isOpen |
boolean |
Whether the card is visible. |
options |
Readonly<HoverCardOptions> |
Current options. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
show(): void |
Show immediately, clearing an Escape dismissal; hides any other visible hover card. |
hide(): void |
Hide immediately (reason ‘programmatic’); focus inside the card moves to the trigger. |
update(options: Partial<HoverCardOptions>): void |
Merge and apply options; content is replaced only when passed; repositions when open. A failed update keeps the previous options. |
destroy(): void |
Hide; remove the card, the trigger aria-describedby reference, listeners, and subscriptions. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-open |
null |
No | No | After the card became visible (dispatched on the card element). |
aui-close |
{ reason: 'escape' | 'pointer-leave' | 'focus-out' | 'outside-pointer' | 'programmatic' } |
No | No | After the card was hidden. ‘programmatic’ covers hide(), destroy(), and another hover card showing. |
Content areas
Section titled “Content areas”| Area | Description |
|---|---|
content |
The card element’s children (the content option). Framework-owned nodes appended to element survive updates that do not pass content. |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-hover-card |
Card element (manual popover, top layer); data-placement holds the final placement. |
aui-hover-card--static |
Set on show when animation is disabled globally or by reduced motion. |
Design tokens
Section titled “Design tokens”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-link | --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-sm | --aui-line-height-normal | --aui-popover-max-height | --aui-radius-lg | --aui-shadow-lg | --aui-space-2 | --aui-space-4
TypeScript definitions
Section titled “TypeScript definitions”interface HoverCardConfig { placement: Placement; showDelay: number; hideDelay: number }type HoverCardCloseReason = 'escape' | 'pointer-leave' | 'focus-out' | 'outside-pointer' | 'programmatic';interface HoverCardCloseDetail { reason: HoverCardCloseReason }interface HoverCardOptions extends Partial<HoverCardConfig> { content: Node | string; describe?: boolean; direction?: 'ltr' | 'rtl';}Accessibility
Section titled “Accessibility”Pattern: Supplementary preview disclosed on hover or keyboard focus, meeting WCAG 1.4.13 Content on Hover or Focus; the card is a generic container in DOM order right after the trigger (not a dialog, not a tooltip).
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Tab / Shift+Tab | Focus moves to the trigger (:focus-visible) | Shows the card after showDelay. |
| Tab | Trigger focused, card visible | Moves into focusable card content (the card follows the trigger in the DOM); the card stays open. |
| Tab / Shift+Tab | Focus leaves trigger and card | Hides the card (reason focus-out) unless the pointer is over it. |
| Escape | Card visible and topmost, focus on the trigger or elsewhere | Hides the card without moving focus (reason escape); it stays hidden until pointer or focus leaves and returns. |
| Escape | Focus inside the card | Hides the card and moves focus to the trigger; it does not reopen until focus leaves and returns. |
| Escape | Card visible inside a Dialog | Closes only the card; the next Escape reaches the dialog. |
| Element | Role |
|---|---|
| trigger | consumer-provided link or button (unchanged) |
| card | none added (generic div, popover=manual) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-describedby |
trigger | id of the card, added to existing ids while describe is true (default); removed on describe false and destroy() |
Showing never moves focus. Focus inside the card or keyboard focus on the trigger keeps it open. Escape or hide() with focus inside the card returns focus to the trigger; focus-out, pointer-leave, and outside-pointer closes leave focus where the user put it.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- 1.4.13 dismissible: Escape hides the card without moving pointer or focus off the trigger, and it stays dismissed until the pointer or focus leaves and returns.
- 1.4.13 hoverable: the pointer can move from the trigger onto the card within hideDelay; the card stays open while hovered.
- 1.4.13 persistent: stays visible while trigger or card is hovered or focused, until Escape, an outside press, or another hover card opens.
- DOM order places the card right after the trigger, so reading and Tab order follow the visual relationship (1.3.2, 2.4.3).
- Only one hover card is visible at a time; touch pointers never open it, so it must not hold essential information or actions.
- With describe true the whole card text becomes the trigger’s description; use describe false for long or interactive cards.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
- The card has no role or name; screen-reader users discover it only through aria-describedby (describe true) or by tabbing into it.
Behavior
Section titled “Behavior”States
Section titled “States”hidden, pending (show delay running), visible, hiding (hide delay running), dismissed (after Escape; stays hidden until the pointer or focus leaves and returns).
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| hidden | mouse or pen pointer enters the trigger | pending; visible after showDelay (aui-open) |
| hidden | keyboard focus (:focus-visible) on the trigger |
pending; visible after showDelay |
| pending | pointer leaves the trigger or focus leaves trigger and card | hidden (timer cancelled) |
| visible | pointer leaves trigger and card, no focus inside | hiding; hidden after hideDelay (pointer-leave) unless the pointer returns |
| visible | focus leaves trigger and card while the pointer is not over them | hidden (focus-out) |
| visible | Escape while topmost | dismissed (escape); focus moves to the trigger only if it was inside the card |
| dismissed | pointer leaves the trigger, or focus leaves trigger and card | hidden (can open again) |
| visible | pointer down outside trigger and card | hidden (outside-pointer); focus not moved |
| visible | another hover card shows, hide(), destroy() |
hidden (programmatic) |
| any | show() |
visible |
Accessibility notes
Section titled “Accessibility notes”- WCAG 1.4.13: dismissible (Escape, without moving pointer or focus off the trigger), hoverable (the pointer can cross onto the card within
hideDelay), and persistent (stays while trigger or card is hovered or focused). - DOM order matches the visual relationship, so reading and Tab order continue naturally from the trigger into the card (1.3.2, 2.4.3).
- With
describe: true, the whole card text is announced as the trigger’s description on focus. Usedescribe: falsefor long or highly interactive cards. - Showing never moves focus; only keyboard focus (
:focus-visible) opens the card, not focus from a pointer click. - Touch pointers never open it; the trigger must work on its own (for example the link goes to the full profile).
- Manual screen-reader verification is pending (see Known limitations).
Responsive behavior
Section titled “Responsive behavior”Width is max-content up to min(22rem, 100vw - 2 x space-2); text wraps anywhere. Height is capped by popover-max-height and the available space on the chosen side, then the card scrolls with contained overscroll. The positioning engine flips and shifts to stay in the viewport.
Right-to-left
Section titled “Right-to-left”start and end placements are logical. With direction: 'rtl' (option or global direction), bottom-start aligns the card to the trigger’s right edge. An inherited dir="rtl" alone does not change placement resolution; set direction. Padding uses logical properties.
Theming
Section titled “Theming”Colors come from role tokens only (color-surface, color-border, color-text, color-link, shadow-lg). The open fade uses @starting-style and is removed under reduced motion or animation: 'none'. In forced-colors mode the card has a CanvasText border.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { hoverCard: { placement: 'top', showDelay: 800 } } });Changes apply to existing instances: an open card is repositioned; delays apply to the next show or hide.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic.
Security
Section titled “Security”String content uses textContent; it is never parsed as HTML. Node content is inserted as given, so the consumer owns its safety. CSP-safe: positioning uses the CSSOM.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. The component never fetches content; load it before create() or pass it through update({ content }).
Performance
Section titled “Performance”Four listeners on the trigger, three on the card, and one configuration subscription per instance. While visible: one overlay-stack entry, one capture-phase pointerdown listener on the window, and the positioning engine’s observers, all released on hide. Budget: HoverCard.show() under 16 ms (one frame).
Supported data scale
Section titled “Supported data scale”Any number of closed instances per page; one visible at a time by design. Keep content short enough to scan in a glance.
Edge cases
Section titled “Edge cases”showDelay: 0shows synchronously on hover or focus;hideDelay: 0still hides asynchronously, so a direct move from the trigger onto the card keeps it open.- A card shown under a stationary pointer stays open until the pointer moves out of it.
- Keyboard focus on the trigger keeps the card open even when the pointer leaves.
- After Escape with the pointer still on the trigger, the card stays hidden until the pointer leaves the trigger and enters again.
show()on one card hides any other visible card (reasonprogrammatic); if focus was inside that other card, it moves to that card’s trigger.update({ content: '<b>text</b>' })renders the literal text.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
HOVER_CARD_TRIGGER |
configuration | The trigger is not a connected HTMLElement with a parent node (elements from another realm fail the instanceof check). |
HOVER_CARD_OPTIONS |
configuration | content missing, not a string or Node, or an empty string; non-boolean describe. |
CFG_INVALID_VALUE |
configuration | Invalid placement, showDelay, hideDelay (integer 0 to 5000), or direction. |
A failed construction leaves no card, trigger attributes, listeners, or subscriptions behind. A failed update() keeps the previous options.
Known limitations
Section titled “Known limitations”- Touch pointers never show the card.
- Keyboard focus uses the same
showDelayas hover. - Only one hover card is visible at a time per loaded copy of the module. When another card replaces it while focus is inside, focus returns to its trigger (it is never lost) without reopening it.
- The card is inserted right after the trigger, not portaled: it inherits the trigger’s container styles, and a trigger inside inline text puts a
divinto that inline context. - The gap between trigger and card is fixed at 8px. The close animation is not played.
- Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Unsupported scenarios
Section titled “Unsupported scenarios”- Opening on tap, long press, or click (use Popover for click).
- HTML strings as content.
- Modal behavior or focus trapping (use Dialog).