Skip to content
txbrendan/ui

Hover Card

beta

A peek: hover, focus, or tap a person, status, count, or position to see the story behind it, without leaving the page.

Preview

Effect

underline

Design review on Tuesday: , Room 1 at .

rows

control

side

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/hover-card

Use it for

  • Showing the story behind a person, a status, a count, or a position, like who the 21 going are.
  • Put a PeekCard in content. Its title names the peek, and in a peek it shows a glance: up to 3 facts and 2 actions.
  • Mark each row of a list with data-peek-row. The peek then opens beside the row, so the rows above and below stay in reach.
  • Set control when the trigger is itself a control, like an event card you press to select. It keeps its role and its Enter.
  • Set underline on words in a sentence, so people can see there is more behind them.
  • While a menu, list, or popover is open, hover opens no peek outside it. The peek waits, and opens once that closes if the pointer is still there.

Avoid it for

  • Anything people must see to finish a task. Peeks are extra. Put the rest on the page the peek links to.
  • A short name for an icon button. Use a Tooltip.
  • A menu of choices, or a form. Use a Menu, a Popover, or a Dialog.
  • Opening one on click. A peek opens on hover, focus, and tap. Use a Popover for a click.

Props

  • content

    Required. What the peek shows. A PeekCard is the standard content.

    ReactNode

  • children

    The trigger: a person, a status, a count, or a position.

    ReactNode

  • delay

    How long the pointer rests on the trigger before the peek opens, in milliseconds.

    number · default 320

  • glance

    A PeekCard inside shows up to 3 facts and 2 actions. False shows it whole.

    boolean · default true

  • width

    The peek's width in pixels. It never grows past the screen.

    number · default 320

  • side

    auto opens beside a row marked data-peek-row, level with the row, or else below the trigger, lined up with its start. It flips when there is no room.

    "auto" | "right" | "left" | "top" | "bottom" · default "auto"

  • inline

    A span trigger that sits in a line of text. False makes it a block.

    boolean · default true

  • control

    The child is itself a control. It keeps its role and its Enter, and the peek follows its hover and focus.

    boolean · default false

  • underline

    A faint dotted underline under the whole trigger, on every line when it wraps.

    boolean · default false

  • label

    The peek's name for screen readers, when its content is not a PeekCard with a title.

    string

  • open

    Controlled. The peek shows while true. Hover, focus, Esc, and a press outside only ask, through onOpenChange.

    boolean

  • defaultOpen

    Starts open, without taking focus.

    boolean · default false

  • onOpenChange

    Hears every open and close, or what the peek asks for when controlled.

    (open: boolean) => void

  • effect

    How the peek opens and closes. rise fades in and moves out from the trigger. grow fades in and grows from the trigger, and closes the same way. Left out, it follows an effect-<name> class on <body>, where popups open, and rises with none.

    "rise" | "grow" · default "rise"

  • className

    Extra classes on the trigger. Later classes win.

    string

  • useHoverCard

    Hook. Content inside a peek reads it to name the peek, to trim to a glance, and to close it.

    () => { titleId; registerTitle; glance; close } | null

Accessibility

  • The trigger is a focusable role="button" with aria-haspopup="dialog", aria-expanded, and aria-controls while open. With control, the child keeps its own role instead.
  • The peek opens on keyboard focus at once, and on hover after the delay. A tap opens it on touch, and a second tap on the trigger closes it.
  • The peek is a non-modal role="dialog". A PeekCard's title names it. Without one, label names it, or else the trigger's words.
  • Enter or Space on the trigger moves focus into the peek. Tab moves through it, and past either end focus is back on the trigger. Esc closes it and puts focus back.
  • ↑ and ↓ step to the peek above or below, while the keyboard is on a trigger or in its peek.
  • A trigger with no words, like an icon or a swatch, needs an aria-label. The console says so until it has one.
  • It opens with a quick fade from its trigger's side, and closes with a quicker fade and a slight shrink.

Examples

Words in a sentence

<p>
  Design review on Tuesday:{" "}
  <HoverCard underline label="Who is going" content={<p>Sam, Kate, Josh, and 18 more.</p>}>
    21 going
  </HoverCard>
</p>

Rows in a list

const people = [
  { name: "Sam Carter", role: "Design lead" },
  { name: "Kate Clarke", role: "Support lead" },
];

<ul>
  {people.map((person) => (
    <li key={person.name} data-peek-row>
      <HoverCard label={person.name} content={<p>{person.role}</p>}>
        {person.name}
      </HoverCard>
    </li>
  ))}
</ul>

Around a control

<HoverCard control label="Thursday's review" content={<p>Starts 15:00 in the Boardroom.</p>}>
  <button type="button">Quarterly review</button>
</HoverCard>

Grows from its trigger

<p>
  Design review on Tuesday:{" "}
  <HoverCard underline effect="grow" label="Who is going" content={<p>Sam, Kate, Josh, and 18 more.</p>}>
    21 going
  </HoverCard>
</p>