Skip to main content

Hover Card

For sighted users to preview content available behind a link.

When to use

A HoverCard shows a preview of richer content (a user card, link preview, etc.) when a sighted user hovers a link or trigger.

  • Prefer Tooltip for short, non-interactive labels on controls.
  • Prefer Popover when the content must be reachable by all users and may contain interactive elements.

warning

Do not rely on a HoverCard as the only accessible path to important content. Because it opens on pointer hover, it is not a reliable path for keyboard or screen reader users. Any information inside a HoverCard must also be available through another fully accessible path (usually the underlying link).
import { Button } from "@ngrok/mantle/button";import { HoverCard } from "@ngrok/mantle/hover-card";import { Icon } from "@ngrok/mantle/icon";import { CalendarIcon } from "@phosphor-icons/react/Calendar";import { ShrimpIcon } from "@phosphor-icons/react/Shrimp"; <HoverCard.Root>	<HoverCard.Trigger asChild>		<Button type="button" appearance="link" intent="accent">			Open Hover Card		</Button>	</HoverCard.Trigger>	<HoverCard.Content className="w-80">		<div className="flex justify-between space-x-4">			<div className="flex size-16 shrink-0 items-center justify-center rounded-full bg-pink-300">				<Icon svg={<ShrimpIcon />} className="size-12" />			</div>			<div className="space-y-1">				<h4 className="text-sm font-medium">@ngrok/mantle</h4>				<p className="text-sm">The Design System – created and maintained by @ngrok.</p>				<div className="flex items-center pt-2">					<Icon svg={<CalendarIcon />} className="mr-2 h-4 w-4 opacity-70" />{" "}					<span className="text-muted-foreground text-xs">Joined November 2023</span>				</div>			</div>		</div>	</HoverCard.Content></HoverCard.Root>;

Arrow

Render HoverCard.Arrow as a child of HoverCard.Content when the card previews one specific link and needs a tip pointing at it. Radix positions the arrow with floating-ui's arrow middleware, so it stays centered on the trigger when collision detection shifts or flips the content. The arrow's fill and border repaint HoverCard.Content's own edge across its base, so the two edges read as one line. The tip also casts its own shadow, because shadow-md on HoverCard.Content stops at that content's border and an unshadowed tip reads as pasted on. That shadow is clipped at the base, so it never darkens the content's interior.

HoverCard.Arrow takes no asChild. The shape is two layers (a filled polygon, plus a polyline that strokes the slanted edges only), and a swapped element loses the border. Restyle it with className instead: fill-* for the surface and a stroke-* on the arrow's polyline child for the edge, since Radix renders the arrow as an svg where bg-* paints nothing.

import { Button } from "@ngrok/mantle/button";import { HoverCard } from "@ngrok/mantle/hover-card"; <HoverCard.Root>	<HoverCard.Trigger asChild>		<Button type="button" appearance="link" intent="accent">			@ngrok/mantle		</Button>	</HoverCard.Trigger>	<HoverCard.Content className="w-64">		<HoverCard.Arrow />		<p className="text-sm">The Design System – created and maintained by @ngrok.</p>	</HoverCard.Content></HoverCard.Root>;

Layering and z-index

HoverCard.Content renders at Tailwind z-50, Mantle's float tier. When composed inside an open Dialog, AlertDialog, or Sheet, it portals into that overlay's positioner and paints above the overlay that owns it. Outside every overlay, it portals to document.body, below the overlay tier (z-60). When sibling floats share a container, the most recently mounted float paints on top.

HoverCard does not render a backdrop. Modal layers such as Dialog, Sheet, and AlertDialog do render backdrops, so stacking those layers compounds the dim and blur effect on the layers underneath.

Composition

Compose the parts of a HoverCard together to build your own:

HoverCard.Root├── HoverCard.Trigger└── HoverCard.Content    └── HoverCard.Arrow

API Reference

The HoverCard component is built on top of Radix UI Hover Card.

HoverCard.Root

The root stateful component that manages the open/closed state of the hover card.

HoverCard.Content renders at Tailwind z-50, Mantle's float tier. When composed inside an open Dialog, AlertDialog, or Sheet, it portals into that overlay's positioner and paints above the overlay that owns it. Outside every overlay, it portals to document.body, below the overlay tier (z-60). When sibling floats share a container, the most recently mounted float paints on top.

PropTypeDefaultDescription
openbooleanThe controlled open state of the hover card. Must be used in conjunction with onOpenChange.
onOpenChange(open: boolean) => voidEvent handler called when the open state of the hover card changes.
defaultOpenbooleanfalseThe open state of the hover card when it is initially rendered. Use when you do not need to control its open state.
openDelaynumber100The duration in milliseconds from when the mouse enters the trigger until the hover card opens.
closeDelaynumber300The duration in milliseconds from when the mouse leaves the trigger or content until the hover card closes.

HoverCard.Trigger

The trigger element that opens the hover card when hovered.

PropTypeDefaultDescription
asChildbooleanfalseUse the asChild prop to compose the trigger functionality onto your own component.

HoverCard.Content

The content to render inside the hover card. Appears in a portal with rich styling and animations.

HoverCard.Content renders at Tailwind z-50, Mantle's float tier. When composed inside an open Dialog, AlertDialog, or Sheet, it portals into that overlay's positioner and paints above the overlay that owns it. Outside every overlay, it portals to document.body, below the overlay tier (z-60). When sibling floats share a container, the most recently mounted float paints on top.

HoverCard.Content sets position: relative, so HoverCard.Arrow's absolutely-positioned wrapper keeps one containing block through the open animation. An absolutely-positioned child of the content therefore anchors to the content.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled.
align"start" | "center" | "end""center"The preferred alignment against the trigger. May change when collisions occur.
sideOffsetnumber4The distance in pixels from the trigger.
alignOffsetnumber0An offset in pixels from the start or end alignment options.
avoidCollisionsbooleantrueWhen true, overrides the side and align preferences to prevent collisions with boundary edges.
collisionBoundaryElement | null | Array<Element | null>[]The element used as the collision boundary. By default this is the viewport.
collisionPaddingnumber | Partial<Record<Side, number>>0The distance in pixels from the boundary edges where collision detection should occur.
sticky"partial" | "always""partial"The sticky behavior on the align axis.
hideWhenDetachedbooleanfalseWhether to hide the content when the trigger becomes fully occluded.

HoverCard.Arrow

An optional tip that points from HoverCard.Content at its trigger. Render it as a child of HoverCard.Content. Radix renders it as an svg, so fill-* paints the surface and bg-* paints nothing.

It takes no asChild — the shape is a filled polygon plus a polyline that strokes the slanted edges, and a swapped element loses the border that continues HoverCard.Content's edge.

PropTypeDefaultDescription
widthnumber14The width of the arrow in pixels.
heightnumber7The height of the arrow in pixels. Radix adds this to sideOffset, so the tip lands sideOffset pixels from the trigger.
classNamestringClasses for the svg. Reach for fill-* for the surface, and target the polyline child for the edge: [&>polyline]:…. The part already sets filter and clip-path for its shadow.

HoverCard.Portal

The portal container that renders hover card content outside the normal DOM tree. HoverCard.Content already renders inside this portal internally, so you rarely need this part directly — reach for it only to customize portal placement with container, or to wrap multiple HoverCard.Content instances in one shared portal.

PropTypeDefaultDescription
containerElement | DocumentFragment | nullnearest layer containerThe container element to portal the content into. When omitted, the content mounts into an enclosing overlay's positioner when one is open, else document.body.
forceMounttrueForce mounting when more control is needed, such as animating the content with a React animation library.