sveltelean Primitives
Versionv0.2.0 GitHub

Example

A reviewer’s card
Reviewed by Ada Lovelace

Ada Lovelace · Analyst, London

Notes on the analytical engine, 1843.

Read the notes
on 28 September.

The trigger’s destination: a hover card only previews it.

<div>
	Reviewed by
	<span data-slean="hover-card">
		<a href="/people/ada" data-slean-part="trigger">Ada Lovelace</a>
		<div popover="hint" data-slean-part="content">
			<p><strong>Ada Lovelace</strong> · Analyst, London</p>
			<p>Notes on the analytical engine, 1843.</p>
			<a href="/people/ada/notes">Read the notes</a>
		</div>
	</span>
	on 28 September.
</div>
Rest the pointer on the name, then move onto the card: it stays. With the keyboard, Tab to the name and Tab again into the card; Escape closes it and returns focus to the name.

Why this implementation exists

A tooltip describes its trigger in a few words; a hover card previews where its trigger leads, with a picture, details and links of its own. That makes it interactive content, so it cannot be a description (aria-describedby) and it must be reachable by keyboard.

The platform provides the pieces: a hint popover in the top layer, which does not close an open menu, and showPopover({ source }), which makes the trigger the invoker so the card follows it in the focus order. The behavior adds what has no trigger in HTML: a hover delay, keeping the card open while the pointer crosses onto it or focus is inside it, and Escape that returns focus without reopening the card.

The browser owns

  • the top layer; with popover="hint", closing other hints without closing an open menu
  • the trigger as the invoker: the card is next in the focus order after it
  • placement next to the trigger with anchor positioning, where supported

Svelte Lean owns

  • showing after a hover delay, at once on keyboard focus, never on touch
  • keeping the card open while the pointer or focus is on it; one card at a time
  • Escape: closing, returning focus from inside the card, not reopening on that focus
  • hover-card.css: the card surface, the sides, the entry transition

Usage

Install @svelte-lean/primitives for the behavior and @svelte-lean/styles for the stylesheet. With @svelte-lean/vite the registration is injected for every static data-slean="hover-card"; without it, import the register module once.

npm install @svelte-lean/primitives
+layout.svelte or any client module
import '@svelte-lean/primitives/hover-card/register';
stylesheets
import '@svelte-lean/styles/hover-card.css';

Put the trigger and, right after it, the content in the root. The trigger works on its own: the card only previews its destination, since touch users never see it.

Fill the card on the server, or on the popover’s toggle event when its content is expensive.

Anatomy

PartElementdata-slean-partRequiredNotes
root<span data-slean="hover-card">–yesOptions: data-slean-side (top, bottom, left, right), data-slean-delay in milliseconds.
trigger<a href>triggeryesWorks on its own: it goes where the card previews.
content<div popover="hint">contentyesRight after the trigger; may hold links and buttons.

Runtime profile

The hover card registers five handlers (pointerover, pointerout, focusin, focusout, keydown) with the shared router and keeps three variables for the page: the open card, one timer and the card Escape dismissed. A thousand cards keep one listener per type (tests/hover-card.test.ts).

Accessibility contract

  • The card is supplementary: what it shows is also where the trigger leads.
  • Keyboard focus on the trigger shows the card at once; Tab moves into it because the trigger is its invoker, and the card follows the trigger in the DOM as well.
  • Escape closes the card; from inside it, focus returns to the trigger and the card stays closed until focus leaves.
  • Touch pointers are ignored: a tap follows the link.

Keyboard

KeyWhenResult
Tabonto the triggerShows the card at once
Tabon the trigger, card openMoves into the card
Escapethe card is openHides it; from inside, focus returns to the trigger

Platform features

FeatureBaselineOutside the target
Popover APIBaseline 2024 (Chrome 114, Safari 17, Firefox 125)The card renders in place, hidden by the stylesheet until shown
popover="hint"Chrome 133; not BaselineFalls back to manual; the behavior hides the card itself
CSS anchor positioningChrome 125, Safari 26; not BaselineThe card keeps the platform default placement

Without JavaScript

The card never shows; the trigger is a working link.

Server rendering

Render the markup as is; the card is hidden until shown.

Before hydration

Before the behavior loads, hovering shows nothing. The first hover after hydration works on the server's markup.

Styling

hover-card.css draws the card surface, anchors it below the trigger's inline start (or on another side), slides it in, and keeps the platform's centered placement where anchor positioning is missing. The tokens it reads:

TokenDefault (light)Applies to
--slean-space-20.5remmargin-block, margin-inline
--slean-space-41remmax-inline-size, padding
--slean-bordervar(--slean-neutral-6)border
--slean-radius-lg0.875remborder-radius
--slean-surfaceoklch(100% 0 0)background
--slean-fgvar(--slean-neutral-12)color
--slean-shadow-lg0 16px 40px oklch(0% 0 0 / 0.18), 0 2px 6px oklch(0% 0 0 / 0.08)box-shadow
--slean-text-sm0.875remfont-size
--slean-leading1.5line-height
--slean-space-10.25remtranslate
--slean-duration-normal160mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition

State selectors the stylesheet targets, all from the platform or ARIA: :popover-open.

Variant attributes: data-slean-side (top, left, right).

Compatibility notes

The Popover API is Baseline 2024. popover="hint" and the source option of showPopover() are Chromium-first; elsewhere the card is a manual popover the behavior hides itself, still after its trigger in the focus order. Anchor positioning is in Chrome and Safari.

Testing

Source