Hover card
A preview that opens on hover or focus, for records behind a link.
- Status
- Experimental
- Level
- Molecule
- Category
- Overlays
- Adoption
- Not used yet
import { HoverCard } from "@oration/canon/components/hover-card";packages/canon/src/components/hover-card.tsx- 9:42 AMPriya Raman approved INV-20931 from Northwind Freight
- 9:15 AMTomás Ferreira replied to the W-9 request.
import { Avatar, AvatarFallback } from "@oration/canon/components/avatar";import { HoverCard, HoverCardContent, HoverCardTrigger } from "@oration/canon/components/hover-card";import { MonogramTile } from "@oration/canon/components/monogram-tile";import { Tag } from "@oration/canon/components/tag";import Link from "next/link";export function Hero() { return ( <ul className="flex w-full max-w-xl flex-col gap-3 rounded-xl bg-card p-4 text-left text-13 shadow-border"> <li className="flex gap-2"> <span className="shrink-0 text-muted-foreground tabular-nums"> 9:42 AM </span> <span> Priya Raman approved INV-20931 from{" "} <HoverCard> <HoverCardTrigger render={<Link href="/companies" />} className="font-medium underline decoration-border-strong underline-offset-[3px] hover:decoration-foreground" > Northwind Freight </HoverCardTrigger> <HoverCardContent className="flex flex-col gap-3"> <div className="flex items-center gap-2.5"> <MonogramTile name="Northwind Freight" color="teal" size="lg" /> <div className="min-w-0"> <p className="truncate font-medium"> Northwind Freight </p> <p className="truncate text-xs text-muted-foreground"> northwindfreight.com </p> </div> </div> <dl className="grid grid-cols-2 gap-x-3 gap-y-2 rounded-[10px] bg-muted/70 px-3 py-2 text-xs"> <div> <dt className="text-muted-foreground"> Stage </dt> <dd className="mt-0.5"> <Tag color="amber" dot> Negotiation </Tag> </dd> </div> <div> <dt className="text-muted-foreground"> Open balance </dt> <dd className="mt-0.5 text-13 font-medium tabular-nums"> $212,400 </dd> </div> </dl> <p className="text-xs text-muted-foreground"> Owned by Priya Raman </p> </HoverCardContent> </HoverCard> </span> </li> <li className="flex gap-2"> <span className="shrink-0 text-muted-foreground tabular-nums"> 9:15 AM </span> <span> <HoverCard> <HoverCardTrigger render={<Link href="/people" />} className="font-medium underline decoration-border-strong underline-offset-[3px] hover:decoration-foreground" > Tomás Ferreira </HoverCardTrigger> <HoverCardContent className="flex items-center gap-2.5"> <Avatar> <AvatarFallback>TF</AvatarFallback> </Avatar> <div className="min-w-0"> <p className="truncate font-medium"> Tomás Ferreira </p> <p className="truncate text-xs text-muted-foreground"> Billing lead at Halcyon </p> </div> </HoverCardContent> </HoverCard>{" "} replied to the W-9 request. </span> </li> </ul> );}Usage#
Hover card is a preview of the record behind a link: hover or focus Northwind Freight in an activity line and a 16rem card shows its stage, balance and owner without leaving the page. It is a peek, not a place to work. The link still goes to the record, and nothing inside the card is required, because touch users never see it and keyboard users can't reach into it. It is experimental and not used in the product yet.
When to use
- On links to records inside running text or activity: a supplier, a person, an invoice.
- When a glance at two or three facts saves opening the record: stage and balance, role and email.
- In dense places where adding those facts inline would crowd the row.
The Hairline-and-Lift Rule
The Option Hue Rule
Anatomy#
Northwind Freight
northwindfreight.com
- Stage
- Negotiation
- Open balance
- $212,400
- Trigger. A real link to the record. It opens the card on hover after 600ms, or on keyboard focus.
- Surface. Popover White, 10px corners, the overlay shadow and ring, 16rem wide with 10px padding.
- Identity. A monogram tile or avatar with the record's name and one line of meta.
- Facts. Two or three values that answer why you hovered: stage, balance, owner.
Examples#
Person
An avatar, name, role and email. The trigger is a real link to the person, rendered through Next's Link.
Assigned to Maya Okafor
import { Avatar, AvatarFallback } from "@oration/canon/components/avatar";import { HoverCard, HoverCardContent, HoverCardTrigger } from "@oration/canon/components/hover-card";import Link from "next/link";export function Person() { return ( <p className="text-13"> Assigned to{" "} <HoverCard> <HoverCardTrigger render={<Link href="/settings" />} className="font-medium underline decoration-border-strong underline-offset-[3px] hover:decoration-foreground" > Maya Okafor </HoverCardTrigger> <HoverCardContent className="flex items-center gap-2.5"> <Avatar size="lg"> <AvatarFallback>MO</AvatarFallback> </Avatar> <div className="min-w-0"> <p className="truncate font-medium">Maya Okafor</p> <p className="truncate text-xs text-muted-foreground"> VP of Revenue </p> <p className="truncate text-xs text-muted-foreground"> maya@cedarline.com </p> </div> </HoverCardContent> </HoverCard> </p> );}Placement and delay
side and align place the card; set alignOffset={0} when you align it, because the default is 4. delay on the trigger shortens the 600ms wait for places where people hover on purpose.
Paid in the Friday payment run
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@oration/canon/components/hover-card";import Link from "next/link";export function Placement() { return ( <p className="text-13"> Paid in the{" "} <HoverCard> <HoverCardTrigger render={<Link href="/reports" />} delay={400} className="font-medium underline decoration-border-strong underline-offset-[3px] hover:decoration-foreground" > Friday payment run </HoverCardTrigger> <HoverCardContent side="top" align="start" alignOffset={0}> <p className="font-medium">Payment run, Friday, Oct 2</p> <p className="text-xs text-muted-foreground tabular-nums"> 212 invoices, $1,842,310.40 </p> </HoverCardContent> </HoverCard> </p> );}States#
| State | Treatment |
|---|---|
| Closed | Only the link is drawn. |
| Opening | After 600ms of hover, or at once on focus, the card grows from a 0.97 scale and slides 8px from the link over 160ms. |
| Open | Stays open while the pointer is on the link or the card, so you can move onto it to select text. |
| Closing | 300ms after the pointer leaves both, it fades out over 110ms toward a 0.95 scale. |
Behavior#
- Built on Base UI Preview Card.
HoverCardTriggerrenders an<a>, so passhreforrender={<Link href="…" />}and it navigates like any link. - Opens after
delay(600ms) of hover, or immediately when the link takes keyboard focus. ClosescloseDelay(300ms) after the pointer leaves the link and card, or when focus leaves the link. - The card portals to the body. Tab from the link moves on through the page, not into the card, so it must not hold anything interactive.
- It never opens on touch; a tap follows the link.
- Opens below the link and centered, 4px away, and flips near the viewport edge. Its default
alignOffsetis 4. - Shares the menu and popover motion: 160ms in from 0.97, 110ms out, a fade only under reduced motion.
Do and don't#
delay to 0 on names in a feed. Moving the pointer down the page sets off a card on every row.Content#
- Lead with the record's name exactly as the link says it.
- Meta is facts with units, not sentences: Negotiation, $212,400 open, Owned by Priya Raman.
- Don't repeat what the surrounding line already says.
Accessibility#
- The trigger is a real link with its own accessible name; the card adds nothing a screen reader needs.
- Keyboard focus opens the card, but its content isn't in the tab order. Everything in it must also be on the record page.
- The card has no dialog role and isn't announced. Treat it as a visual convenience.
- Touch devices never show it.
| Keys | Action |
|---|---|
| Tab | Focusing the link opens the card; moving focus away closes it. |
| Enter | Follows the link. |
| Esc | Closes the card. |
Design tokens#
| Token | Used for |
|---|---|
--popover | Surface fill |
shadow-md | The overlay shadow |
--foreground | The 1px ring at 10% |
--muted-foreground | Meta lines |
--radius-lg | 10px corners |
--ease-out | 160ms open and 110ms close |
API reference#
HoverCard
The root.
Other props spread onto Base UI PreviewCard.Root.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | No default | Controls whether the card is open. |
defaultOpen | boolean | false | Whether it starts open when uncontrolled. |
onOpenChange | (open: boolean, eventDetails) => void | No default | Called when it opens or closes. |
HoverCardTrigger
The link that opens the card.
Other props spread onto Base UI PreviewCard.Trigger (<a>).
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | No default | Where the link goes. |
render | ReactElement | (props, state) => ReactElement | No default | Render a Next Link instead of a plain anchor. |
delay | number | 600 | Milliseconds of hover before the card opens. |
closeDelay | number | 300 | Milliseconds before it closes after the pointer leaves. |
HoverCardContent
The card, with its portal and positioner.
Other props spread onto Base UI PreviewCard.Popup.
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end" | "bottom" | Which side of the link it opens on. |
sideOffset | number | 4 | Distance from the link in pixels. |
align | "start" | "center" | "end" | "center" | Alignment along the link. |
alignOffset | number | 4 | Offset along the alignment axis. |
className | string | No default | Merged after the defaults (w-64 p-2.5). |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
Hover card isn't used anywhere in the product. Record names in activity, tables and transcripts are plain links with no preview.
HoverCardContent defaults alignOffset to 4 while every other popup uses 0, so a centered card sits 4px off center.
Like Popover, it enters from 0.97 but exits with zoom-out-95 to 0.95.