Skip to content

Hover Card

OverlaysSince 0.3.0
import { HoverCard } from '@absoluteui/components/hover-card';
import '@absoluteui/components/hover-card/style.css';

Using Angular? See Hover Card for Angular.

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.

  • 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.
  • 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.
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;
});
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';
});
}
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.' });
<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>
HoverCard.create(trigger: HTMLElement, options: HoverCardOptions): HoverCard

Global configuration key: hoverCard (see Configuration).

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.
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.
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.
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.
Area Description
content The card element’s children (the content option). Framework-owned nodes appended to element survive updates that do not pass content.

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.

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

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';
}

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).

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)
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.

Criterion Level Note
1.3.1 Info and Relationships A
1.3.2 Meaningful Sequence A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
1.4.13 Content on Hover or Focus AA
2.1.1 Keyboard 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.

  • 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.

hidden, pending (show delay running), visible, hiding (hide delay running), dismissed (after Escape; stays hidden until the pointer or focus leaves and returns).

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
  • 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. Use describe: false for 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).

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.

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.

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.

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.

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

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.

No persistence, network access, or telemetry. The component never fetches content; load it before create() or pass it through update({ content }).

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).

Any number of closed instances per page; one visible at a time by design. Keep content short enough to scan in a glance.

  • showDelay: 0 shows synchronously on hover or focus; hideDelay: 0 still 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 (reason programmatic); if focus was inside that other card, it moves to that card’s trigger.
  • update({ content: '<b>text</b>' }) renders the literal text.
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.

  • Touch pointers never show the card.
  • Keyboard focus uses the same showDelay as 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 div into 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.
  • Opening on tap, long press, or click (use Popover for click).
  • HTML strings as content.
  • Modal behavior or focus trapping (use Dialog).