Hover Card
A preview of what is behind a link — the Popover's machinery, a different gesture.
src/components/rahti_ui/hover_card.rs
Basic
A link, and a preview of where it goes.
shadcn's own demo. The trigger is an `<a href>` and the card is a `popover` in the top layer — so it is never clipped by an ancestor, dismisses on an outside click, and closes on `Escape`, none of which is code here.
What *is* code here is the opening. `popovertarget` is a click, and the platform has no hover equivalent — so unlike every other component in this library, this one does nothing before the runtime loads. That matters less than it sounds: the trigger is a link, and without the script the user follows it and gets the real page instead of the preview.
Focus opens it too, and the card never takes focus. It is supplementary content beside a link — shadcn's own description is `for sighted users` — so it carries no `role`, and a `role="dialog"` nobody can reach would be worse than none.
`delay="10"` and `close_delay="100"` are shadcn's own demo, and they sit on the trigger because that is where shadcn's Base UI version puts them. They are also this library's defaults, so the tag above could leave both out: Base UI nominally defaults to 600ms, but no shadcn demo ships that number, and 600ms is a very different thing to hover.
The two delays
How long before it opens, and how long before it gives up.
`delay` and `closeDelay`, and neither is decoration. Without an open delay every link brushed on the way somewhere else flashes a card; without a close delay the four pixels of nothing between the trigger and the card are enough to shut it before the pointer gets across.
Entering the card cancels the pending close, which is what makes that gap crossable at all — the same arrangement the Dropdown Menu's submenus need, for the same reason.
They are props of the trigger, which is where shadcn's Base UI version puts them, and they are rendered as `data-delay` and `data-close-delay`. The script reads its timings off the elements it is already searching from rather than having them baked into it, so the page can be read for what it will do.
A `HoverCard` root takes the same two props, and every trigger under it that says nothing inherits them — trigger, then root, then this library's 10 and 100. That part is this port's rather than shadcn's: it is what a list of twenty previewable links wants instead of twenty copies of the same two numbers.
Hover the middle one first and it takes its 600ms. Then move straight to another — it opens at once. The delay guards the *first* card only: someone going from one previewable link to the next is already reading previews rather than passing by, and making them wait it out again on every link is the thing that feels broken. Neither Base UI nor Radix does this; it is the tooltip world's skip delay.
A trigger with no link
The one shape of this component that keeps a no-script path.
A trigger without an `href` is a `<button>`, and it gets a fallback the link cannot have: `popovertarget` with `popovertargetaction="show"`. So a click — or a tap, on a device with no hover at all — opens this card with no script.
`show` rather than the default toggle, because a pointer that already opened the card on its way to the click must not have the click close it again. That is the same trap the Dropdown Menu's submenus hit when hover and click both drive one popover.
Sides
Where the card opens, said twice.
Radix's `side` and `align`, placed twice over exactly as the Popover places them: an inline `position-anchor` and `position-area` where the browser has CSS anchor positioning, and the script measuring the trigger everywhere else. Both flip when the card would run off screen.
The offset that separates the card from its trigger is a margin on one axis only — `margin-block` above or below, `margin-inline` beside. All four margins would also push a `start`-aligned card four pixels off the edge it is supposed to line up with.
What the platform does
Most of Radix's HoverCard, minus the one part it has no attribute for.
Two rows down from the top is where this component parts company with the Popover and the Dropdown Menu. Everywhere else in this library the opening is a declarative association and the script is a garnish; here the script *is* the gesture.
It also asks for `popover="hint"` at mount, which is the platform's own answer to this component: showing a hint does not dismiss an open `auto` popover the way another `auto` would, so a card hovered inside an open menu no longer shuts the menu. It is newer than `auto` and an unsupported value would silently fall back to `manual` — losing light dismiss and `Escape` — so the script reads the value back and reverts rather than serving it and hoping.
The port
What crossed, and what the popover made unnecessary.
The card at the top of this page carries `id="hover-card-nextjs"`, and the pairing between the two halves is `data-hover-card` on both. The id exists for the `popovertarget` fallback, so a component that renders its children twice — as the Sidebar does for its mobile drawer — would otherwise put two cards in one document under one id. The script renames the second at mount.
A trigger that says nothing, under a root that says nothing, lands on a delay of 10ms and a close delay of 100ms. Those are shadcn's own demo numbers rather than Base UI's 600 and 300 — a library whose job is to feel like shadcn should default to what shadcn shows, not to a number none of its demos ship.
