|Kit

HoverCard

A rich preview panel on Ark UI's HoverCard — delayed, portaled and animated. Hover a trigger, see a card; add HoverCard.Group so a row of triggers shares one card that slides between them.

HoverCard sits between Popover and Tooltip: it's a rich panel and it opens on a delay — the "hover a username, see a profile card" pattern. It's a styled layer over Ark UI's HoverCard: Ark owns the open/close state, the open/close delays, positioning, and dismiss-on-outside-interaction; the kit adds the portaled, animated panel chrome (reusing Popover's surface).

Unlike Tooltip, there's no single-call convenience form — a hover card's content is arbitrary rich markup, so it's compound-only, like Popover.

import { HoverCard } from '@42-central/ui-react/hover-card';

<HoverCard.Root>
  <HoverCard.Trigger asChild>
    <a href="/norm">@the-norm</a>
  </HoverCard.Trigger>
  <HoverCard.Content>
    Rich <strong>preview</strong> content.
  </HoverCard.Content>
</HoverCard.Root>

Playground

HoverCard

Loading preview

Props

Code

<HoverCard
  position="bottom"
  openDelay={600}
  closeDelay={300}
  width="auto"
/>

Basic usage

Code

<HoverCard.Root>
  <HoverCard.Trigger asChild>
    <a href="/norm">@the-norm</a>
  </HoverCard.Trigger>
  <HoverCard.Content className="flex w-64 flex-col gap-2 p-3">
    <div className="flex items-center gap-3">
      <Avatar name="The Norm" color="name" />
      <div className="flex flex-col">
        <Text size="sm" span className="font-medium">The Norm</Text>
        <Text size="xs" c="muted" span>@the-norm</Text>
      </div>
    </div>
    <Text size="sm">
      The reference every project is graded against.
    </Text>
  </HoverCard.Content>
</HoverCard.Root>

Delays

Ark's machine defaults to a 600ms openDelay and a 300ms closeDelay — long enough that an incidental pass of the cursor doesn't pop a card open, but short enough to feel responsive on purpose. Both pass straight through to Ark; the kit adds no timer logic of its own.

Code

<HoverCard.Root openDelay={0} closeDelay={0}>…</HoverCard.Root>

Positioning

position sets the placement; the panel flips and shifts to stay in view (Ark / Floating UI). Prefer the logical, RTL-aware sides start / end — they resolve to left / right based on the active direction, so the card mirrors automatically under dir="rtl". (Physical left / right and Ark's aligned variants like top-start still work.) Unlike Popover, width defaults to "auto" (size to content) rather than "target" — a hover card's content is usually much larger than its trigger.

Code

<HoverCard.Root position="start">…</HoverCard.Root>
<HoverCard.Root position="top">…</HoverCard.Root>
<HoverCard.Root position="bottom">…</HoverCard.Root>
<HoverCard.Root position="end">…</HoverCard.Root>

Portal

withinPortal (default true) renders the card in a Portal so it escapes clipping ancestors; pass container to portal somewhere other than document.body. Set withinPortal={false} to keep the card inline instead — it's then clipped by an overflow: hidden ancestor like any other child, as long as that ancestor (or one between it and the panel) is positioned (e.g. position: relative) — standard CSS: overflow: hidden alone doesn't clip a position: absolute descendant unless something establishes a containing block for it.

A clipping ancestor (overflow-hidden)

Code

<HoverCard.Root>…</HoverCard.Root> // portaled by default
<HoverCard.Root withinPortal={false}>…</HoverCard.Root> // clips with its container

Disabled

disabled prevents the card from opening at all — hover and focus are both no-ops.

Code

<HoverCard.Root disabled>…</HoverCard.Root>

Grouping — HoverCard.Group

HoverCard.Group puts several trigger targets (a row of avatars, say) on one shared card instead of a separate card per trigger — the same HoverCard.Trigger / HoverCard.Content parts HoverCard.Root uses, just several of each, matched by a shared value (like Tabs.Trigger / Tabs.Content). Ark's HoverCard machine natively supports several named triggers under a single Root/Content — hovering from one target to the next while the card is already open just repositions the same panel, no close/reopen and no delay replay — and a CSS transition on that reposition makes the panel visibly slide from one target to the next.

Trigger/Content must be Group's own direct children — Group renders no DOM node of its own (like Root), so for row-level layout (overlapping avatars, say), wrap Group itself instead of wrapping them inside it.

Code

<HoverCard.Group>
  <HoverCard.Trigger value="ada">
    <Avatar name="Ada Lovelace" />
  </HoverCard.Trigger>
  <HoverCard.Trigger value="grace">
    <Avatar name="Grace Hopper" />
  </HoverCard.Trigger>

  <HoverCard.Content value="ada">Ada's card content</HoverCard.Content>
  <HoverCard.Content value="grace">Grace's card content</HoverCard.Content>
</HoverCard.Group>

Accessibility

A hover card is a preview of where a link goes, for sighted mouse users. That is Ark's design, and the kit keeps it:

  • Touch never opens it. Ark ignores touch pointers, because a tap belongs to the link it previews.
  • Its content can't be focused. Focusing the trigger opens the card, but Tab moves past it, so nothing inside is keyboard-reachable.
  • Screen readers aren't told it opened. The trigger carries no aria-expanded or aria-controls.

So keep its content a preview — everything in it should also be reachable by following the trigger's link. For a card people act on (a copy button, a "View profile" link, a follow button), use Popover's mode="hover" instead: it keeps the delayed hover preview, and a click, tap or Enter pins it as a regular, focusable, dismissable popover.

Anatomy

  • HoverCard.Root — wraps Ark's HoverCard.Root; owns open state, delays, and positioning.
  • HoverCard.Trigger — the trigger; use asChild to wrap your own element. Inside a Group, give it a value to identify its target.
  • HoverCard.Content — the portaled, animated panel. Inside a Group, give it a value matching its Trigger.
  • HoverCard.Group — one shared Root/Content across several value-matched Trigger/Content pairs, so the panel slides between them instead of closing and reopening.

testId

HoverCard.Root and HoverCard.Group are headless providers with no DOM node of their own, so neither takes a testId. HoverCard.Content (the panel — one testId per value, inside a Group) takes its own testId. HoverCard.Trigger is a raw Ark re-export and was left alone: Ark already forwards an arbitrary data-testid straight through natively. See Testing.

API

HoverCard.Root props (beyond Ark's HoverCard.Root):

Prop

Type

HoverCard.Group props (the same panel-level shape as HoverCard.Root, since there's one shared panel):

Prop

Type

HoverCard.Content's one prop beyond Ark's own HoverCard.Content, meaningful only inside a Group:

Prop

Type

Changelog

v0.9.0
  • Opened inside a Modal or Drawer, an overlay portals into it, so it always paints above its host

    #135
v0.8.0
  • New HoverCard component, where cards in a HoverCard.Group share one sliding card

    #116

On this page