Hover Card
stimeo--hover-card
A preview card on hover and focus. Moving the pointer onto its links does not dismiss it.
A card with real content, opened on hover and on focus. Unlike a hint it may hold links. Opening and closing each have their own delay, so a cursor passing over does not open it. Moving the pointer from the trigger onto the card keeps it up, so its links can be pressed, and tabbing into them keeps it open too. Esc closes it while it shows. Opening never steals focus. Its content is expected to be reachable from the trigger as well, so nobody who cannot open the card is left behind. Placement here is this demo's CSS.
Keyboard
| Key | Action |
|---|---|
| Esc | Close the open card (watched document-wide, regardless of focus). |
<%# Markup for the hover_card demo.
Hover/focus on the link opens a rich supplementary card after a delay. The card
has no role="dialog" (it's purely supplementary), and its contents are assumed to
be reachable from the trigger's target too. The open/close delay, the hoverable
bridge, and Escape-to-dismiss are the library's. %>
<span class="hover-card" data-controller="stimeo--hover-card">
<a
class="hover-card__trigger"
href="#jane"
data-stimeo--hover-card-target="trigger"
aria-expanded="false"
aria-controls="hover-card-panel"
data-action="
mouseenter->stimeo--hover-card#open
mouseleave->stimeo--hover-card#close
focusin->stimeo--hover-card#open
focusout->stimeo--hover-card#close">
<%= t("components.hover_card.demo.trigger") %>
</a>
<%# The card has no role="dialog" (supplementary display, not an interrupting dialog). %>
<div
class="hover-card__panel"
id="hover-card-panel"
data-stimeo--hover-card-target="card"
data-action="
mouseenter->stimeo--hover-card#open
mouseleave->stimeo--hover-card#close
focusin->stimeo--hover-card#open
focusout->stimeo--hover-card#close"
hidden>
<p class="hover-card__name"><%= t("components.hover_card.demo.name") %></p>
<p class="hover-card__role"><%= t("components.hover_card.demo.role") %></p>
<a class="hover-card__link" href="#follow"><%= t("components.hover_card.demo.follow") %></a>
</div>
</span>
/*
* Presentation-only styles for the hover_card demo.
* The library opens/closes the card by toggling its hidden attribute. Placement
* is static — the consumer's CSS responsibility.
*/
.hover-card {
position: relative;
display: inline-block;
}
/* The theme-aware accent *text* token: the raw brand accent measures ~2.9:1 on the
light canvas, under the 4.5:1 floor. It resolves back to the brand value in dark. */
.hover-card__trigger {
color: var(--accent-700);
text-decoration: underline;
}
.hover-card__panel {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
z-index: 10;
width: 18rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem;
background: var(--surface, var(--surface-card));
border: 1px solid var(--border-strong);
border-radius: 0.5rem;
box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
/* Setting display: flex overrides the hidden attribute's default display:none, so
re-declare display:none while closed to honor the library's hidden toggle. */
.hover-card__panel[hidden] {
display: none;
}
.hover-card__name {
font-weight: 600;
}
.hover-card__role {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.hover-card__link {
margin-top: 0.5rem;
color: var(--accent-700);
}
// hover_card opt-in positioning demo (consumer-side JS).
//
// The core controller (stimeo--hover-card) only handles open/close, delay, and
// dismiss. Viewport-edge flip/shift is optionally connected via the opt-in
// stimeo-ui/positioning module. We watch the card's hidden and, while open, track
// the trigger's coordinates.
import { attachPositioning } from 'stimeo-ui/positioning';
document.querySelectorAll('[data-controller~="stimeo--hover-card"]').forEach((root) => {
const trigger = root.querySelector('[data-stimeo--hover-card-target="trigger"]');
const card = root.querySelector('[data-stimeo--hover-card-target="card"]');
if (!trigger || !card) return;
let detach = null;
const sync = () => {
if (!card.hidden && !detach) {
detach = attachPositioning(trigger, card, {
placement: 'bottom-start',
offset: 8,
padding: 8,
});
} else if (card.hidden && detach) {
detach();
detach = null;
card.style.position = card.style.left = card.style.top = '';
}
};
new MutationObserver(sync).observe(card, { attributes: true, attributeFilter: ['hidden'] });
sync();
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--hover-card"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
required
|
The element (e.g. a link) that opens the card on hover/focus; its aria-expanded reflects state and focus is never stolen. |
data-stimeo--hover-card-target="trigger" |
card
required
|
The non-modal popover content shown/hidden via hidden and data-state; binding open/close on it forms a hoverable bridge. |
data-stimeo--hover-card-target="card" |
Values
| Name | Description | Attribute |
|---|---|---|
openDelay
|
Milliseconds to wait before opening on hover/focus to prevent flicker (default 300). | data-stimeo--hover-card-open-delay-value |
closeDelay
|
Milliseconds to wait before closing on leave/blur (re-checks focus); prevents flicker (default 200). | data-stimeo--hover-card-close-delay-value |
closeOnScroll
|
Whether to close on window or scrollable-ancestor scroll while open (default false). | data-stimeo--hover-card-close-on-scroll-value |
Actions
| Name | Description | Action |
|---|---|---|
close
|
Schedules close after closeDelay; cancels a pending open and aborts if focus is inside the card. |
stimeo--hover-card#close |
open
|
Opens the card after openDelay (or immediately at 0); cancels a pending close. |
stimeo--hover-card#open |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-expanded |
Trigger | "true" while the card is open, "false" when closed. |
hidden |
Card | Removed when open, present when closed. |
data-state |
Card | "open" / "closed" — an optional hook for fade transitions. |