Skip to content

Hover Card

A richer preview that opens when a link is hovered or reached with the keyboard, like a profile summary on a user name. It only repeats information that is available elsewhere.

Overlayv0.1.0experimentalWCAG 2.2 AAView spec

Reviewed by Ada Lovelace on Friday.

import { Avatar, HoverCard, HoverCardContent, HoverCardTrigger } from "@rdloom/react";

export default function HoverCardBasicExample() {
  return (
    <div className="flex justify-center p-10">
      <p className="text-sm">
        Reviewed by{" "}
        <HoverCard>
          <HoverCardTrigger href="#ada-lovelace">Ada Lovelace</HoverCardTrigger>
          <HoverCardContent label="Ada Lovelace, profile preview">
            <div className="flex gap-3">
              <Avatar name="Ada Lovelace" decorative />
              <div className="flex flex-col gap-1">
                <p className="font-semibold">Ada Lovelace</p>
                <p className="text-[var(--rd-color-text-muted)]">Staff engineer, Platform</p>
                <p className="text-[var(--rd-color-text-muted)]">Joined March 2021 · 48 reviews</p>
              </div>
            </div>
          </HoverCardContent>
        </HoverCard>{" "}
        on Friday.
      </p>
    </div>
  );
}

Installation

npx rdloom add hover-card

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/hover-card.json

Works, but without upgrade tracking.

Usage

import { Avatar, HoverCard, HoverCardContent, HoverCardTrigger } from "@rdloom/react";

<div className="flex justify-center p-10">
  <p className="text-sm">
    Reviewed by{" "}
    <HoverCard>
      <HoverCardTrigger href="#ada-lovelace">Ada Lovelace</HoverCardTrigger>
      <HoverCardContent label="Ada Lovelace, profile preview">
        <div className="flex gap-3">
          <Avatar name="Ada Lovelace" decorative />
          <div className="flex flex-col gap-1">
            <p className="font-semibold">Ada Lovelace</p>
            <p className="text-[var(--rd-color-text-muted)]">Staff engineer, Platform</p>
            <p className="text-[var(--rd-color-text-muted)]">Joined March 2021 · 48 reviews</p>
          </div>
        </div>
      </HoverCardContent>
    </HoverCard>{" "}
    on Friday.
  </p>
</div>

Assigned to Grace Hopper.

import { Avatar, HoverCard, HoverCardContent, HoverCardTrigger } from "@rdloom/react";

// The same links exist on the profile page, so the card is a shortcut and never the only way in.
export default function HoverCardWithLinksExample() {
  return (
    <div className="flex justify-center p-10">
      <p className="text-sm">
        Assigned to{" "}
        <HoverCard>
          <HoverCardTrigger href="#grace-hopper">Grace Hopper</HoverCardTrigger>
          <HoverCardContent label="Grace Hopper, profile preview">
            <div className="flex flex-col gap-3">
              <div className="flex items-center gap-3">
                <Avatar name="Grace Hopper" decorative />
                <div>
                  <p className="font-semibold">Grace Hopper</p>
                  <p className="text-[var(--rd-color-text-muted)]">Compilers team</p>
                </div>
              </div>
              <p className="text-[var(--rd-color-text-muted)]">Tab moves into the card; Esc closes it and keeps your place.</p>
              <div className="flex gap-4">
                <a href="#grace-profile" className="font-medium underline underline-offset-4 outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)]">
                  View profile
                </a>
                <a href="#grace-open-work" className="font-medium underline underline-offset-4 outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)]">
                  Open work
                </a>
              </div>
            </div>
          </HoverCardContent>
        </HoverCard>
        .
      </p>
    </div>
  );
}

Placement

import { HoverCard, HoverCardContent, HoverCardTrigger } from "@rdloom/react";

export default function HoverCardPlacementExample() {
  return (
    <div className="flex flex-wrap justify-center gap-8 p-10 text-sm">
      {(["top", "bottom", "start", "end"] as const).map((placement) => (
        <HoverCard key={placement} placement={placement}>
          <HoverCardTrigger href={`#${placement}`}>Opens {placement}</HoverCardTrigger>
          <HoverCardContent label={`Preview placed ${placement}`}>
            <p>This card prefers the {placement} side and flips when there is no room.</p>
          </HoverCardContent>
        </HoverCard>
      ))}
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
childrenrequired

A HoverCardTrigger (the link) followed by a HoverCardContent (the card).

nodenone
openDelay

Milliseconds the pointer or keyboard focus waits on the trigger before the card opens.

number400
closeDelay

Milliseconds before the card closes after the pointer or focus leaves the trigger and the card. Moving onto the card keeps it open.

number150
placement

Preferred side of the trigger. Flips when there isn't room.

"top" | "bottom" | "start" | "end""bottom"
isOpen

Controlled open state.

booleannone
defaultOpen

Initial open state when uncontrolled.

booleannone
onOpenChange

Called when the card opens or closes.

(isOpen: boolean) => voidnone

Accessibility

Role group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab onto the trigger opens the card after the open delay, without moving focus
  • Tab again moves into the card when it has links or buttons; Tab past its last control continues to the next element and closes the card; Shift+Tab before its first control returns to the trigger
  • Esc closes the card and keeps focus on the trigger
  • The trigger keeps its own action: Enter follows the link

Screen readers announce

  • The trigger is read as a normal link; once the card is open its text is read as the link's description
  • A labelled group is announced when focus moves into the card

What your code must do

  • The card never holds the only copy of anything: the same information must be reachable from the link's destination or the page
  • Touch does nothing special: a tap follows the link, and no long press is needed
  • The card stays open while the pointer is over it, so its links can be reached
  • While open, the card is the description of the trigger (aria-describedby)
  • Opening never moves focus; the card is not modal and the page behind stays available

Guidelines

Use it when

  • A preview of something a link points to: a user, a repository, a product
  • Extra context that is also on the linked page

Avoid it when

  • The information is needed to complete a task: put it on the page
  • Plain hints: use Tooltip
  • Content with forms or long reading: use Popover or a page

Don't

  • Hiding the only way to reach an action inside the card
  • Relying on hover alone: the trigger must also work with focus and touch (the link itself)
  • Putting a card on a control that is not a link

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-raised
  • --rd-color-border-default
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-action-primary
  • --rd-color-focus-ring
  • --rd-radius-overlay