Skip to content

Hover card

A preview that opens on hover or focus, for records behind a link.

Category
Overlays
Adoption
Not used yet
import { HoverCard } from "@oration/canon/components/hover-card";
packages/canon/src/components/hover-card.tsx
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.

When not to use

  • To name an icon or show a shortcut. Use Tooltip
  • To explain a term. Use Info tip
  • For anything with buttons or fields. Keyboard users can't tab into a hover card. Use Popover
  • For the full record while keeping the list in view. Use Sheet

The Hairline-and-Lift Rule

The card lifts with the overlay shadow and a 1px ink ring at 10%. Inside, split regions with hairlines or a Well Gray tint well, never a nested card.

The Option Hue Rule

A stage or tier inside the preview is a Tag in its option hue, and the monogram tile takes the record's identity tint. Nothing else in the card is colored.

Anatomy#

Northwind Freight

Northwind Freight

northwindfreight.com

Stage
Negotiation
Open balance
$212,400
  1. Trigger. A real link to the record. It opens the card on hover after 600ms, or on keyboard focus.
  2. Surface. Popover White, 10px corners, the overlay shadow and ring, 16rem wide with 10px padding.
  3. Identity. A monogram tile or avatar with the record's name and one line of meta.
  4. 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#

States
StateTreatment
ClosedOnly the link is drawn.
OpeningAfter 600ms of hover, or at once on focus, the card grows from a 0.97 scale and slides 8px from the link over 160ms.
OpenStays open while the pointer is on the link or the card, so you can move onto it to select text.
Closing300ms after the pointer leaves both, it fades out over 110ms toward a 0.95 scale.

Behavior#

  • Built on Base UI Preview Card. HoverCardTrigger renders an <a>, so pass href or render={<Link href="…" />} and it navigates like any link.
  • Opens after delay (600ms) of hover, or immediately when the link takes keyboard focus. Closes closeDelay (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 alignOffset is 4.
  • Shares the menu and popover motion: 160ms in from 0.97, 110ms out, a fade only under reduced motion.

Do and don't#

Do. Show two or three facts that answer why someone hovered, and keep the link as the way to the record.
Don't. Put buttons, menus or forms in the card. Keyboard and touch users can't reach them.
Do. Wait the full 600ms so the card doesn't flash as the pointer crosses a list of names.
Don't. Set 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.
Keyboard interactions
KeysAction
TabFocusing the link opens the card; moving focus away closes it.
EnterFollows the link.
EscCloses the card.

Design tokens#

Design tokens
TokenUsed for
--popoverSurface fill
shadow-mdThe overlay shadow
--foregroundThe 1px ring at 10%
--muted-foregroundMeta lines
--radius-lg10px corners
--ease-out160ms open and 110ms close

API reference#

HoverCard

The root.

Other props spread onto Base UI PreviewCard.Root.

Props of HoverCard
PropTypeDefaultDescription
openbooleanNo defaultControls whether the card is open.
defaultOpenbooleanfalseWhether it starts open when uncontrolled.
onOpenChange(open: boolean, eventDetails) => voidNo defaultCalled when it opens or closes.

HoverCardTrigger

The link that opens the card.

Other props spread onto Base UI PreviewCard.Trigger (<a>).

Props of HoverCardTrigger
PropTypeDefaultDescription
hrefstringNo defaultWhere the link goes.
renderReactElement | (props, state) => ReactElementNo defaultRender a Next Link instead of a plain anchor.
delaynumber600Milliseconds of hover before the card opens.
closeDelaynumber300Milliseconds before it closes after the pointer leaves.

HoverCardContent

The card, with its portal and positioner.

Other props spread onto Base UI PreviewCard.Popup.

Props of HoverCardContent
PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end""bottom"Which side of the link it opens on.
sideOffsetnumber4Distance from the link in pixels.
align"start" | "center" | "end""center"Alignment along the link.
alignOffsetnumber4Offset along the alignment axis.
classNamestringNo defaultMerged 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.