Skip to content

HoverCard

  • A profile behind an @mention or an author’s name
  • A summary of a linked repository, issue or document, before following the link

Not for anything people need: a hover card never opens on touch, so on a phone it doesn’t exist. Put essential content on the page, or in a Popover opened by a press.

Choosing between messages and popups:

ComponentUse it forFor example
DialogSomething that needs a decision or input before the user carries on; the page is blocked meanwhileConfirm a delete, edit a record in a form
DrawerA panel that slides in from an edge and blocks the page; the first choice on a phoneShare options, a mobile nav, a filter panel
PopoverExtra content or controls next to a button, while the rest of the page stays usableFilter panel, share options, details card
MenuA list of actions or settings opened from a button“More actions ⋯”, a View menu
TooltipA short text label on hover or focus; nothing in it can be clickedName of an icon button, a keyboard shortcut
HoverCardA preview of where a link goes, on hover or focus; nothing essential in itA profile behind an @mention, a repository’s summary
ToastA brief confirmation after an action, which goes away by itself“Saved”, “Archived — Undo”
AlertA message that stays in the page until the situation changes“Your trial ends in 3 days”, a form error summary

Reviewed by @ada

AL
Ada LovelaceAnalyst. Writes the notes everyone else reads.12 repositories
on 4 October.

import { Avatar, HoverCard, Link, Paragraph, Stack, Text } from '@arun-dev/ui';
const photo =
'data:image/svg+xml,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" fill="#7c3aed"/><circle cx="20" cy="16" r="7" fill="#ede9fe"/><rect x="8" y="26" width="24" height="14" rx="7" fill="#ede9fe"/></svg>',
);
export default function HoverCardBasics() {
return (
<Paragraph>
Reviewed by{' '}
<HoverCard.Root>
{/* The trigger is a real link: a tap follows it, and the card only adds detail. */}
<HoverCard.Trigger render={<Link href="#ada" />}>@ada</HoverCard.Trigger>
<HoverCard.Popup>
<Stack direction="row" gap="sm" align="start">
<Avatar.Root>
<Avatar.Image src={photo} alt="" />
<Avatar.Fallback>AL</Avatar.Fallback>
</Avatar.Root>
<Stack gap="3xs">
<Text weight="semibold">Ada Lovelace</Text>
<Text color="secondary">Analyst. Writes the notes everyone else reads.</Text>
<Link href="#ada-repos">12 repositories</Link>
</Stack>
</Stack>
</HoverCard.Popup>
</HoverCard.Root>{' '}
on 4 October.
</Paragraph>
);
}
import { HoverCard, Link } from '@arun-dev/ui';
<HoverCard.Root>
<HoverCard.Trigger render={<Link href="/ada" />}>@ada</HoverCard.Trigger>
<HoverCard.Popup />
</HoverCard.Root>;
PartRendersDoes
Rootnothingholds open, and the hover timers
Trigger<a>the link it previews; the card is anchored to it
Popup<div popover="manual">the card

Trigger is an unstyled link: render a Link to style it, or your router’s link.

InputOpensCloses
Mouseafter resting on the link for delay (600ms)closeDelay (300ms) after leaving both the link and the card
Keyboardat once, on focuswhen focus leaves both the link and the card, or on Esc
Touchnever: a tap follows the link—
  • The pointer can move from the link onto the card: it stays open while either is hovered.
  • After the link, Tab moves into the card, wherever the card is in the page, and on through its links. Esc closes it and puts focus back on the link.
  • Pressing the link closes the card, since following the link is what you came to do.
  • It doesn’t close an open Popover or Dialog, and Esc closes only the card.
Hover or focus me
Card content
<HoverCard.Popup
  side="bottom"
  align="center"
>
  Card content
</HoverCard.Popup>
  • The link must make sense without the card. The card adds detail; it doesn’t replace the link’s text.
  • Nothing in it may be the only way to do something: a touch user never sees it.
  • It has no role, so it isn’t announced as a dialog. Screen reader users reach its content by tabbing on from the link.
HoverCard.Root props
PropTypeDefaultDescription
delaynumber600Milliseconds a pointer rests on the Trigger before the card opens. Keyboard focus opens it at once.
closeDelaynumber300Milliseconds between the pointer leaving the Trigger or the card and the card closing — time to cross the gap onto the card.
openbooleanControlled state. Provide onOpenChange alongside it. Never pass undefined — the mode is fixed at mount.
defaultOpenbooleanfalseInitial state when uncontrolled. Read once, at mount.
onOpenChange(open: boolean) => voidCalled on every open and close, after any delay has run.
childrenReactNodeThe other parts. Root renders no element of its own.
HoverCard.Popup props
PropTypeDefaultDescription
side'top' | 'bottom' | 'left' | 'right''bottom'Which side of the Trigger to show on. Flips to the opposite side when there is no room.
align'start' | 'center' | 'end''center'Flush with the Trigger's start or end edge, or centred on it.
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM.

HoverCard.Trigger takes the props of an <a>, plus render and ref.

  • Restyle with the --hover-card-* tokens, such as --hover-card-width or --hover-card-padding.
  • Placement is CSS anchor positioning, as for Popover: it flips to the other side when there’s no room.
  • Types: import type { HoverCardRootProps } from '@arun-dev/headless/hover-card', or ComponentProps<typeof HoverCard.Root>.