Hover Card
A hover-triggered preview card on radix-ui, rendered in a portal with directional slide/zoom animations and three content sizes.
import { CalendarDays } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
export default function HoverCardDemo() {
return (
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="link" tone="brand">
@ai2
</Button>
</HoverCardTrigger>
<HoverCardContent>
<div className="flex gap-3">
<Avatar>
<AvatarImage src="https://i.pravatar.cc/80?img=13" alt="ai2" />
<AvatarFallback>AI</AvatarFallback>
</Avatar>
<div className="space-y-1">
<p className="text-sm font-semibold">ai2 design system</p>
<p className="text-sm text-muted-foreground">
Agent native components on the shadcn registry format.
</p>
<div className="flex items-center gap-1 pt-1 text-xs text-muted-foreground">
<CalendarDays className="size-3.5" />
Shipping since 2026
</div>
</div>
</div>
</HoverCardContent>
</HoverCard>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/hover-cardDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1 tw-animate-css@^1.4.0Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/hover-card.tsx"use client"
import type * as React from "react"
import { HoverCard as HoverCardPrimitive } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const hoverCardContentVariants = cva(
"z-50 max-w-[calc(100vw-2rem)] origin-(--radix-hover-card-content-transform-origin) rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 motion-reduce:animate-none",
{
variants: {
size: {
sm: "w-56",
md: "w-64",
lg: "w-80",
},
},
defaultVariants: { size: "md" },
}
)
function HoverCard(props: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger(
props: React.ComponentProps<typeof HoverCardPrimitive.Trigger>
) {
return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
}
function HoverCardContent({
className,
align = "center",
sideOffset = 6,
size,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content> &
VariantProps<typeof hoverCardContentVariants>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
data-size={size ?? "md"}
align={align}
sideOffset={sideOffset}
className={cn(hoverCardContentVariants({ size, className }))}
{...props}
/>
</HoverCardPrimitive.Portal>
)
}
export {
HoverCard,
HoverCardTrigger,
HoverCardContent,
hoverCardContentVariants,
}Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the popover colors will be missing.
Usage
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="link" tone="brand">
@ai2
</Button>
</HoverCardTrigger>
<HoverCardContent>
A preview shown when the trigger is hovered or focused.
</HoverCardContent>
</HoverCard>Wrap the trigger with HoverCardTrigger asChild to attach the card to any focusable element. The content renders in a portal and opens on hover or focus.
Examples
Profile preview
Sizes
Props
Every part also forwards its underlying radix props - e.g. open / onOpenChange on HoverCard and side on HoverCardContent.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Width preset on HoverCardContent: sm is w-56, md is w-64, lg is w-80. |
align | "start" | "center" | "end" | "center" | Alignment of the card against the trigger (on HoverCardContent). |
sideOffset | number | 6 | Gap in pixels between the trigger and the card (on HoverCardContent). |
openDelay | number | 700 | Delay in milliseconds before the card opens on hover (on HoverCard, radix default). |
closeDelay | number | 300 | Delay in milliseconds before the card closes after the pointer leaves (on HoverCard, radix default). |
ai2 Hover Card: rich hover previews for React, on radix-ui
The ai2 Hover Card is a shadcn-compatible hover card component for React, built on the radix-ui HoverCard primitive and styled with Tailwind CSS v4. It reveals a floating preview next to a trigger when a pointer user hovers or a keyboard user focuses it, rendered in a portal with an arrow origin and directional slide and zoom animations. A size prop offers three widths, so the same component fits a compact link preview or a roomier profile card.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Hover Card?
It is a three-part composition: HoverCard, HoverCardTrigger and HoverCardContent. The anatomy matches shadcn/ui exactly, so existing snippets, muscle memory and AI agents keep working without changes. The trigger forwards the radix asChild prop, so the card attaches to your own link, button or avatar without an extra wrapper element.
The content renders through a radix portal above everything else, so it is never clipped by an overflow container, and animates in from the side it is placed on using tw-animate-css utilities. Styling uses the popover token pair, so the card stays readable in both light and dark mode from day one, and the size prop switches its width between w-56, w-64 and w-80 without any other change.
Why use it
- Accessible by construction: The radix-ui HoverCard primitive opens on keyboard focus as well as hover and is intended for non-essential, supplementary content, so it never traps focus or blocks the page.
- Portal, never clipped: Content renders in a portal at z-50, so the card escapes overflow-hidden and transformed ancestors and always sits above the surrounding layout.
- Three content widths: A single size prop switches HoverCardContent between w-56, w-64 and w-80, so one component covers a terse link preview and a full profile card.
- Directional animations: data-side drives a fade, zoom and 1-notch slide from the correct direction, powered by tw-animate-css utilities rather than a JS animation library.
- Agent-readable metadata: The registry item describes its parts, size matrix and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its dependencies and the @ai2/tokens theme to your project.
- asChild trigger: HoverCardTrigger forwards the radix asChild prop, so the card attaches to your own link, button, avatar or badge without an extra wrapper element.
- Size prop: HoverCardContent accepts size="sm" | "md" | "lg" for w-56, w-64 and w-80, defaulting to md when the prop is omitted.
- Placement and alignment: Content forwards the radix side, align, sideOffset and collision props, with a center align and 6px sideOffset as defaults.
- Configurable timing: The root forwards openDelay and closeDelay, so you can make the card appear faster on quick previews or linger longer for reading.
- Data attributes for styling: Every part exposes data-slot, the content exposes data-state, data-side and data-size, so you can restyle placements or sizes from CSS without forking the component.
Production tips
- Reserve it for non-essential content: A hover card is progressive enhancement. Never put an action or information required to finish a task inside it, because it never opens on touch devices.
- Attach the trigger to a real control: Use asChild on a focusable element like a link or button so keyboard users can open the card. A hover card on a plain div is unreachable for them.
- Pick the size to fit the content: Use sm for a one-line preview, md for a short profile, and lg only when the card genuinely holds richer content; an oversized card looks empty.
- Do not nest interactive-heavy content: If users need to click around inside the floating layer, reach for a Popover instead; a hover card closes as the pointer moves away.
- Tune the delays for the context: Lower openDelay for a snappy link preview in dense text, or raise closeDelay so the card does not vanish while the pointer travels toward it.
Works with the rest of ai2
Hover cards enrich compact triggers across the registry. Attach one to a ai2 Button in the link variant or wrap an ai2 Avatar to reveal a profile preview on hover.
When the floating content needs to be interactive or stay open, reach for the ai2 Popover, and for a short label rather than a preview use the ai2 Tooltip. Everything shares one token source, so combinations stay visually consistent in both modes.