DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

HoverCard

A floating card that appears when a trigger is hovered or focused. Use for lightweight previews — user profiles, link previews, citations, or footnote definitions. Built on Base UI's preview-card primitive.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import {
  HoverCard,
  HoverCardTrigger,
  HoverCardContent,
  HoverCardHeader,
  HoverCardTitle,
  HoverCardDescription,
  HoverCardBody,
  HoverCardFooter,
  HoverCardArrow,
} from "@tessinaui/ui";
<HoverCard>
  <HoverCardTrigger render={<button>@blaz</button>} />
  <HoverCardContent side="bottom" align="start">
    <HoverCardHeader leading={<Avatar initials="BZ" />}>
      <HoverCardTitle>Blaz Zorcic</HoverCardTitle>
      <HoverCardDescription>Designer · Building TessinaUI.</HoverCardDescription>
    </HoverCardHeader>
    <HoverCardFooter>
      <Button variant="outline" size="xs">Message</Button>
      <Button size="xs">Follow</Button>
    </HoverCardFooter>
    <HoverCardArrow />
  </HoverCardContent>
</HoverCard>

Examples

Default

A user profile preview with an avatar, description, metadata, and footer actions.

Sizes

Four widths — sm, md, lg, and xl — each pairing a fixed width with a text scale.

Rounded

Corner radius from none to full, matched to the trigger for visual cohesion.

Intent border

Give a status preview a coloured border with intent. The border follows the intent on its own — showIntentBorder is only there to override it.

Positioning

Place the card on any side of the trigger — top, right, bottom, or left.

Loading

HoverCardSkeleton mirrors the card's avatar, text, and optional stats while content loads.

Member card

The compact form — who they are and the two things you want to do with them.

Metadata rows

The most common card in production software: a title, a short stack of icon/label rows, and the actions. A metadata row is two spans and an icon, so it stays your composition rather than another exported part.

Media

HoverCardMedia bleeds to the frame and is clipped by the card's own corner arc. Two shapes ride the same slot: a banner behind an avatar, and a thumbnail above a link preview.

Stats

A figure row under the description, for company and entity cards.

Scrollable

A card taller than the space beneath its trigger caps at the measured height and scrolls its body. The header and footer hold their height while the body moves.

Touch

Hovering does not exist on a phone. touch="press" (the default) opens the card on a tap; touch="off" keeps the card pointer-only and leaves the trigger's own tap action alone.

When to use HoverCard vs. Tooltip vs. Popover

Use HoverCard when…Use Tooltip when…Use Popover when…
The preview is rich (avatar, description, actions)You only need short informational textThe user needs interactive controls (forms, lists)
Trigger is hover/focus (mouse + keyboard)Trigger is hover/focus and content is non-interactiveTrigger is click
A tap opens the preview on touchTouch users gracefully don't see the tipWorks equally on touch + mouse
Examples: user profile, link preview, citationExamples: button label, icon meaningExamples: filter dropdown, rename input

The card is supplementary. Everything inside it must be reachable some other way. A portalled popup cannot sit in the tab order after its trigger, so a keyboard user tabbing forward will never land inside the card — interactive content there is a shortcut, never the only route. When the content is the point rather than a preview, reach for Popover.

Touch

Base UI's hover interaction is mouse-only: a touch never opens a preview card. Left alone that makes the component dead on every phone, so the trigger adds a press gesture for finger and pen input.

touchBehaviour
"press" (default)A tap opens the card. Tap again, tap outside, or press Escape to dismiss. On a link trigger the first tap opens the card instead of navigating — the card carries the real link.
"off"Pointer-only. The trigger keeps its own tap action; pair it with a Popover when phones need this content too.

The gesture is keyed off the live event's pointerType, not a (pointer: coarse) media query: a touchscreen laptop reports coarse: false while its screen is being touched, and an iPad with a trackpad has both pointers at once. A query evaluated at mount answers for the device, not for the gesture actually in hand.

Anatomy

Only HoverCard, HoverCardTrigger, and HoverCardContent are required. Everything else composes optionally inside HoverCardContent.

<HoverCard>
  <HoverCardTrigger>…</HoverCardTrigger>
  <HoverCardContent>
    <HoverCardMedia />
    <HoverCardHeader>
      <HoverCardTitle />
      <HoverCardDescription />
    </HoverCardHeader>
    <HoverCardBody />
    <HoverCardSeparator />
    <HoverCardFooter />
    <HoverCardArrow />
  </HoverCardContent>
</HoverCard>

Size

Widths are fluid ranges, not fixed: a compact card shrinks to its min, a rich one grows to its max, and both clamp to 100dvw - 2rem so the card never touches the bezel on a phone.

SizeWidthText
"sm"208–256pxxs
"md" (default)240–288pxsm
"lg"272–320pxsm
"xl"320–384pxbase

The card is also capped at the height the positioner measured (--available-height). Past that, HoverCardBody scrolls while the header and footer stay put — the popup itself is the clipping frame and never scrolls, because an overlay scrollbar would paint straight through its corner arc.

Intent

intent drives the border colour and the arrow's stroke, and it applies itself: any value but "none" paints the border. The popup is borderless at intent="none" — it takes its definition from shadow-lg and the surface contrast, which is what lets the arrow merge seamlessly with the card edge. Pass showIntentBorder={false} to keep the semantic intent without the stroke.

ValueBorder
"none" (default)none — there is no neutral border state
"error"border-error
"warning"border-warning
"success"border-success
"info"border-info

Rounded

ValueCSS
"none"rounded-none
"sm"rounded-md
"md" (default)rounded-lg
"lg"rounded-xl
"full"rounded-3xl

Positioning

HoverCardContent accepts side (top | right | bottom | left), align (start | center | end), sideOffset (px gap from trigger, default 18, raised to a floor of 21 whenever the arrow is shown), and alignOffset (px shift along the side, default 0).

collisionPadding (default 8) is the minimum gap kept from the viewport edge. It is grown by the device's safe-area insets, so the card clears the home indicator and the notch rather than stopping underneath them.

<HoverCardContent side="right" align="start" sideOffset={12}>
  …
</HoverCardContent>

Delays

Pass delay (open) and closeDelay (close) on the root, in milliseconds. Defaults match Base UI: delay = 600, closeDelay = 300.

<HoverCard delay={300} closeDelay={150}>
  …
</HoverCard>

The values propagate to HoverCardTrigger via context — override per-trigger if you have multiple triggers in one card group.

Header slots

HoverCardHeader takes optional leading (avatar / icon) and trailing (badge / status) slots in addition to its title + description children.

<HoverCardHeader
  leading={<Avatar initials="BZ" />}
  trailing={<Badge variant="dot" color="success" />}
>
  <HoverCardTitle>Blaz Zorcic</HoverCardTitle>
  <HoverCardDescription>Online now</HoverCardDescription>
</HoverCardHeader>

Matching button corners to the card

HoverCardFooter is a generic slot — buttons inside don't auto-inherit the card's rounded. Pass rounded explicitly on each button to keep its corners flowing into the card edge:

<HoverCard rounded="none">
  <HoverCardTrigger render={<button>@blaz</button>} />
  <HoverCardContent>
    …
    <HoverCardFooter>
      {/* match the HoverCard's rounded prop */}
      <Button variant="outline" size="xs" rounded="none">Message</Button>
      <Button variant="primary" size="xs" rounded="none">Follow</Button>
    </HoverCardFooter>
  </HoverCardContent>
</HoverCard>

Controlled

HoverCard accepts open, defaultOpen, and onOpenChange.

const [open, setOpen] = React.useState(false);

<HoverCard open={open} onOpenChange={setOpen}>
  …
</HoverCard>

Accessibility

  • The trigger renders a real <button type="button">, so it is focusable and announced. (Base UI's own default host is an <a>, which without an href is neither — paired with a mouse-only hover interaction that left a default trigger with no keyboard path and no touch path at all.)
  • The card opens on hover, keyboard focus, or a tap, and closes on Escape or an outside press. The pointer can travel from the trigger into the card without it closing, and nothing auto-hides while the trigger is still hovered — the three requirements of WCAG 1.4.13.
  • The popup is a named group, labelled by HoverCardTitle — never a dialog. Nothing is modal, focus is never trapped, and a portalled popup cannot follow its trigger in the tab order.
  • HoverCardTitle renders a <div>, not a heading. The popup is portalled to document.body, so an <h3> there spliced a phantom level into the page's heading outline every time a card opened.
  • The trigger is described by the card only when a press opened it. A preview announced on every passing focus is the disruption Base UI hides these cards from assistive tech to avoid; a deliberate press is a request for the preview, so answering it then is not an interruption.
  • Give inline triggers a real 44px tap target — the examples do it with an invisible before: pseudo-element so the line height is untouched (WCAG 2.5.5).
  • Use intent="error" or "warning" to give status previews a visual signal that complements text content — the border comes with it.

Assistive tech matrix

Screen readerStatusWhat was heard
VoiceOver (macOS / Safari)Verified"@blaz button" — announced as a button with its name. After the card opened on focus, three successive captures were still the button: the reader is not walked through the preview.
Plain keyboard, no screen reader (WebKit)VerifiedTab off the trigger closes the card and moves past it — the portalled content never enters the tab order. This is the contract: the card is supplementary, so nothing inside it may be the only route to an action.
VoiceOver, stepping past the triggerVerified — differs from plain keyboardWith VO running the card stays open and VO does reach its Message / Follow buttons. Reachable is better than stranded, but do not design for it: the plain-keyboard row above is the guarantee.
VoiceOver — the press pathStructural onlyA tap wires aria-describedby to the card (verified on iPhone 13 and Pixel 5 profiles), but VoiceOver on the desktop has no touch pointer to press with, so this was not confirmed audibly.
NVDAUntestedNo Windows machine in this project's loop.
JAWSUntestedNo Windows machine in this project's loop.

RTL

Pass dir="rtl" to the root. Header alignment, the leading/trailing slots, and footer button order all flip to follow the reading direction.

<HoverCard dir="rtl">
  <HoverCardTrigger render={<button>@blaz</button>} />
  <HoverCardContent>
    <HoverCardHeader>
      <HoverCardTitle>بلاز زورتشيتش</HoverCardTitle>
      <HoverCardDescription>مصمم · يبني TessinaUI.</HoverCardDescription>
    </HoverCardHeader>
    <HoverCardArrow />
  </HoverCardContent>
</HoverCard>

API Reference

HoverCard (root)

PropTypeDefaultDescription
size"sm" | "md" | "lg" | "xl""md"Width + text size of the popup
rounded"none" | "sm" | "md" | "lg" | "full""md"Corner radius
intent"none" | "error" | "warning" | "success" | "info""none"Used by the optional intent border
showIntentBorderbooleanintent !== "none"Overrides whether the border picks up the intent colour; unset, it follows intent
dir"ltr" | "rtl""ltr"Reading direction
delaynumber600 (Base UI)Open delay in ms (forwarded to Trigger)
closeDelaynumber300 (Base UI)Close delay in ms (forwarded to Trigger)
touch"press" | "off""press"What a finger or pen tap does — see Touch
openboolean—Controlled open state
defaultOpenbooleanfalseUncontrolled initial state
onOpenChange(open, event) => void—Fired when open state changes

HoverCardContent

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"Which side of the trigger to position on
align"start" | "center" | "end""center"Alignment along the side axis
arrowbooleanfalseRender the built-in arrow tail. An explicit <HoverCardArrow /> child does the same and wins when both are present
sideOffsetnumber18Distance in px from the trigger (floored at 21 when the arrow is shown)
alignOffsetnumber0Shift along the side axis
collisionPaddingnumber8Minimum gap from the viewport edge, grown by the device's safe-area insets
portalbooleantrueRender inside a portal
containerHTMLElement | nulldocument.bodyPortal target
size, rounded, intent, showIntentBorder—inheritedOverride the root's variant

HoverCardHeader

PropTypeDefaultDescription
leadingReactNode—Avatar or icon rendered beside the title
trailingReactNode—Slot for badge, status dot, or follow button

HoverCardTrigger

Renders a <button>. Use render={<a href="…" />} to swap in a link, or render={<MyComponent />} for any element. Accepts delay / closeDelay to override the values inherited from the root.

HoverCardMedia

A band that bleeds to the card's frame — a banner behind an avatar, or a thumbnail above a link preview. The card clips it, so the band needs no radius of its own.

PropTypeDefaultDescription
heightnumber | string—Block size for the band; a number is read as px. Leave unset to let the content size it

HoverCardSkeleton

Paints the card's surface with placeholder bars while content loads.

PropTypeDefaultDescription
size"sm" | "md" | "lg" | "xl""md"Width + text size of the placeholder surface
rounded"none" | "sm" | "md" | "lg" | "full""md"Corner radius
intent"none" | "error" | "warning" | "success" | "info""none"Passed through to the surface variants
showAvatarbooleantrueShow the round avatar placeholder
linesnumber2Number of body text bars
showStatsbooleanfalseShow the three short stat bars
labelstring"Loading"What assistive tech announces. Rendered as sr-only text, never aria-label — a role="status" region announces its content, so a label-only region is silent on insertion

Sub-components

HoverCardTitle, HoverCardDescription, HoverCardBody, HoverCardSeparator, HoverCardFooter, HoverCardArrow, HoverCardBackdrop — all accept className and any HTML props.

Every part publishes a stable data-slot (hover-card-trigger, -content, -media, -header, -title, -description, -body, -separator, -footer, -arrow, -backdrop, -skeleton) for styling and test targeting.

Notes

  • Built on @base-ui/react/preview-card — Base UI's name for the HoverCard primitive.
  • Enter/exit animations use Base UI's data-[starting-style] / data-[ending-style] plus position-aware data-[side=*] attributes, so the popup "lifts off" from whichever side it opens on.
  • The arrow is an SVG bubble contour on a 32×32 square canvas. Its fill closes over the popup's edge so the two surfaces merge with no seam, and whenever the card carries an intent border the contour is stroked in the same colour, so the outline carries around the tip.
  • HoverCardContent publishes --hover-card-inner-radius, so your own content inside the frame can write rounded-[var(--hover-card-inner-radius)] and square off exactly when the card does.
GridIconButton

On this page

PlaygroundInstallationUsageExamplesDefaultSizesRoundedIntent borderPositioningLoadingMember cardMetadata rowsMediaStatsScrollableTouchWhen to use HoverCard vs. Tooltip vs. PopoverTouchAnatomySizeIntentRoundedPositioningDelaysHeader slotsMatching button corners to the cardControlledAccessibilityAssistive tech matrixRTLAPI ReferenceHoverCard (root)HoverCardContentHoverCardHeaderHoverCardTriggerHoverCardMediaHoverCardSkeletonSub-componentsNotes