Hover Card

A preview that opens while the pointer rests on a link: the profile behind a mention, the page behind a link, the commit behind a hash. Popover's surface, grown out of the link it previews.

The new form controls shipped this morning, designed by @katiep and reviewed by the whole team.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add hover-card

Usage#

usage.tsxTSX
import {  HoverCard,  HoverCardTrigger,  HoverCardContent,} from "@/components/ui/hover-card"export function Example() {  return (    <HoverCard>      <HoverCardTrigger asChild>        <a href="/people/katiep">@katiep</a>      </HoverCardTrigger>      <HoverCardContent>Katie Park, Design Systems</HoverCardContent>    </HoverCard>  )}

Anatomy#

import {  HoverCard,  HoverCardTrigger,  HoverCardContent,  HoverCardMedia,  HoverCardTitle,  HoverCardDescription,  HoverCardFooter,} from "@/components/ui/hover-card"<HoverCard>  <HoverCardTrigger />  <HoverCardContent>    <HoverCardMedia />    <HoverCardTitle />    <HoverCardDescription />    <HoverCardFooter />  </HoverCardContent></HoverCard>

The page behind a link. HoverCardMedia sets the picture 8px in from the card's edges instead of bleeding to them, with concentric corners (the card's 16px radius minus 8px of inset leaves 8px on the picture). The copy under it keeps the card's own 16px. It is aspect-video until you give it another ratio.

Everything that changed is in the release notes for 2.4.

Commit preview#

A reference in running text, opened into what it points at: the subject, the author, and the size of the change. The counts sit in HoverCardFooter, a muted well set in from the card's edges like the media, where a divider line would otherwise go. They are tabular-nums, so a column of them lines up. align="start" opens the card from the hash's left edge.

Fixed in 8f3a2c1, released with 2.4.1.

Placement#

side, align and sideOffset are Radix's, and the card flips when the side you asked for has no room. showArrow adds a pointer toward the link. The card grows out of the trigger it previews, from Radix's transform origin, rather than from its own centre. These open after 150ms so you can sweep across them.

Timing#

The card waits openDelay (700ms) before it opens, so a pointer passing over a paragraph full of links doesn't set off a string of cards, and closeDelay (300ms) after the pointer leaves, which is the time it has to cross the gap into the card. Both are props on HoverCard. Shorten the open delay where the preview is the point (a list of people), and keep the close delay: without it the card vanishes on the way to it.

Accessibility#

A hover card is for sighted pointer users. It opens when the trigger has focus, but the card never takes focus and it never opens on touch, so everything in it must also be reachable where the link goes. Make the trigger a real link to that place, and never put the only way to do something inside the card. When the content has to be operated, use a Popover, which opens on a click and moves focus into itself.

API reference#

HoverCard

The root, Radix HoverCard.Root; renders no DOM. Every part that renders an element accepts className, merged last.

PropTypeDefaultDescription
openDelaynumber700Milliseconds the pointer rests on the trigger before the card opens.
closeDelaynumber300Milliseconds after the pointer leaves before the card closes: the time it has to cross into the card.
openboolean-Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the card opens or closes.

HoverCardTrigger

The link being previewed, Radix HoverCard.Trigger. Renders an <a> and forwards all its props.

PropTypeDefaultDescription
asChildbooleanfalseMerge onto your own link (a Link, a Next.js link) instead of rendering an <a>.
Data attributeDescription
data-state"open" or "closed".

HoverCardContent

The card, Radix HoverCard.Content portalled to the body; forwards all its props. Declares --surface, so a nested control blends with the card.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"Preferred side of the trigger; the card flips when there is no room.
align"start" | "center" | "end""center"Alignment against the trigger.
sideOffsetnumber8Gap in pixels between the trigger and the card.
collisionPaddingnumber8Distance in pixels kept from the viewport edge.
showArrowbooleanfalseRender a small pointer toward the trigger.
density"comfortable" | "compact""compact"Padding: 16px or 12px. Falls back to the nearest DensityProvider.
Data attributeDescription
data-state"open" or "closed".
data-sideThe side the card actually opened on, after collisions.
data-align"start", "center" or "end".
CSS variableDescription
--radix-hover-card-content-transform-originThe trigger-side origin the card grows from.
--radix-hover-card-content-available-widthWidth left between the trigger and the viewport edge.
--radix-hover-card-content-available-heightHeight left between the trigger and the viewport edge.
--surfaceSet to var(--popover), read by nested Inputs and Selects.

HoverCardTitle

The card's heading. Forwards all native <h4> props.

HoverCardDescription

Muted supporting copy under the title. Forwards all native <p> props.

HoverCardMedia

One <img> or <video>, aspect-video, set 8px in from the card's edges (4px compact) with concentric corners. Put it first or last. Forwards all native <div> props.

HoverCardFooter

A muted flex row inset like the media; always the last part. Forwards all native <div> props.

FAQ#

A Tooltip names a control in a few words and never takes the pointer. A Hover Card previews where a link goes, and the pointer can move into it. A Popover opens on a click and takes focus, so it is the one for anything that has to be operated: forms, pickers, confirmations.