Hover Card
A shadcn-style hover card component built with Ark UI primitives.
Liked by @sarah_chen and 3 others.
Sarah Chen
@sarah_chen
Design Engineer at Acme Inc. Building beautiful interfaces and design systems.
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardPopup,
HoverCardTrigger,
} from "@/components/ui/hover-card";
const HOVER_PROFILE = {
avatar: "https://i.pravatar.cc/300?u=sarah",
bio: "Design Engineer at Acme Inc. Building beautiful interfaces and design systems.",
name: "Sarah Chen",
username: "@sarah_chen",
} as const;
const HoverCardProfilePreview = () => (
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<img
alt=""
className="size-14 shrink-0 rounded-full object-cover"
src={HOVER_PROFILE.avatar}
/>
<Button size="sm" type="button" variant="secondary">
Follow
</Button>
</div>
<div>
<p className="font-semibold text-foreground text-sm leading-tight">
{HOVER_PROFILE.name}
</p>
<p className="text-muted-foreground text-sm">{HOVER_PROFILE.username}</p>
</div>
<p className="text-foreground text-sm leading-snug">{HOVER_PROFILE.bio}</p>
<div className="flex gap-4 text-muted-foreground text-xs">
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
2,456
</span>
<span>Following</span>
</div>
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
14.5K
</span>
<span>Followers</span>
</div>
</div>
</div>
);
const HoverCardBasicDemo = () => (
<HoverCard>
<p className="max-w-md text-muted-foreground text-sm leading-relaxed">
Liked by{" "}
<HoverCardTrigger asChild>
<a
className="font-medium text-primary underline underline-offset-2"
href="#hover-basic"
>
{HOVER_PROFILE.username}
</a>
</HoverCardTrigger>{" "}
and 3 others.
</p>
<HoverCardPopup>
<HoverCardProfilePreview />
</HoverCardPopup>
</HoverCard>
);
export default HoverCardBasicDemo;
Installation
npx shadcn@latest add @ark-cn/hover-cardInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
"use client";
import {
HoverCard as HoverCardPrimitive,
useHoverCard,
useHoverCardContext,
} from "@ark-ui/react/hover-card";
import { Portal } from "@ark-ui/react/portal";
import { cn } from "@/lib/utils";
export type HoverCardProps = HoverCardPrimitive.RootProps;
export const HoverCard = (props: HoverCardProps) => (
<HoverCardPrimitive.Root data-slot="hover-card" {...props} />
);
export type HoverCardPopupProps = HoverCardPrimitive.ContentProps & {
arrowClassName?: string;
arrowTipClassName?: string;
disablePortal?: boolean;
positionerClassName?: string;
showArrow?: boolean;
};
export const HoverCardPopup = ({
arrowClassName,
arrowTipClassName,
children,
className,
disablePortal,
positionerClassName,
showArrow = true,
...contentProps
}: HoverCardPopupProps) => {
const inner = (
<HoverCardPrimitive.Positioner
className={cn(!disablePortal && "z-50", positionerClassName)}
data-slot="hover-card-positioner"
>
<HoverCardPrimitive.Content
className={cn(
"relative z-[calc(50+var(--layer-index,0))] w-[min(20rem,var(--available-width,20rem))] max-w-[min(20rem,var(--available-width,20rem))] rounded-xl border border-border/80 bg-popover p-4 text-popover-foreground shadow-md outline-none ring-1 ring-border/20",
"origin-(--transform-origin) transition-[opacity,transform] duration-150 data-[state=closed]:scale-[0.98] data-[state=closed]:opacity-0 data-[state=open]:scale-100 data-[state=open]:opacity-100 data-has-nested:data-[state=open]:scale-[calc(1-var(--nested-layer-count,0)*0.05)]",
className,
)}
data-slot="hover-card-content"
{...contentProps}
>
{showArrow ? (
<HoverCardPrimitive.Arrow
className={cn(
"[--arrow-background:var(--popover)] [--arrow-size:10px] [--arrow-shadow-color:var(--border)]",
arrowClassName,
)}
data-slot="hover-card-arrow"
>
<HoverCardPrimitive.ArrowTip
className={cn(
"border-border border-t border-l",
arrowTipClassName,
)}
data-slot="hover-card-arrow-tip"
/>
</HoverCardPrimitive.Arrow>
) : null}
{children}
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Positioner>
);
return disablePortal ? inner : <Portal>{inner}</Portal>;
};
export const HoverCardTrigger = ({
className,
...props
}: HoverCardPrimitive.TriggerProps) => (
<HoverCardPrimitive.Trigger
className={cn(className)}
data-slot="hover-card-trigger"
{...props}
/>
);
export const HoverCardArrow = ({
className,
...props
}: HoverCardPrimitive.ArrowProps) => (
<HoverCardPrimitive.Arrow
className={cn(className)}
data-slot="hover-card-arrow-raw"
{...props}
/>
);
export const HoverCardArrowTip = ({
className,
...props
}: HoverCardPrimitive.ArrowTipProps) => (
<HoverCardPrimitive.ArrowTip
className={cn(className)}
data-slot="hover-card-arrow-tip-raw"
{...props}
/>
);
export const HoverCardPositioner = ({
className,
...props
}: HoverCardPrimitive.PositionerProps) => (
<HoverCardPrimitive.Positioner
className={cn(className)}
data-slot="hover-card-positioner-raw"
{...props}
/>
);
export const HoverCardContent = ({
className,
...props
}: HoverCardPrimitive.ContentProps) => (
<HoverCardPrimitive.Content
className={cn(className)}
data-slot="hover-card-content-raw"
{...props}
/>
);
export const HoverCardContext = HoverCardPrimitive.Context;
export const HoverCardRootProvider = (
props: HoverCardPrimitive.RootProviderProps,
) => <HoverCardPrimitive.RootProvider {...props} />;
export { useHoverCard, useHoverCardContext };
Update import aliases to match your project setup.
Usage
import * as HoverCard from "@/components/ui/hover-card"Read exported parts in src/components/ui/hover-card.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
Liked by @sarah_chen and 3 others.
Sarah Chen
@sarah_chen
Design Engineer at Acme Inc. Building beautiful interfaces and design systems.
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardPopup,
HoverCardTrigger,
} from "@/components/ui/hover-card";
const HOVER_PROFILE = {
avatar: "https://i.pravatar.cc/300?u=sarah",
bio: "Design Engineer at Acme Inc. Building beautiful interfaces and design systems.",
name: "Sarah Chen",
username: "@sarah_chen",
} as const;
const HoverCardProfilePreview = () => (
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<img
alt=""
className="size-14 shrink-0 rounded-full object-cover"
src={HOVER_PROFILE.avatar}
/>
<Button size="sm" type="button" variant="secondary">
Follow
</Button>
</div>
<div>
<p className="font-semibold text-foreground text-sm leading-tight">
{HOVER_PROFILE.name}
</p>
<p className="text-muted-foreground text-sm">{HOVER_PROFILE.username}</p>
</div>
<p className="text-foreground text-sm leading-snug">{HOVER_PROFILE.bio}</p>
<div className="flex gap-4 text-muted-foreground text-xs">
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
2,456
</span>
<span>Following</span>
</div>
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
14.5K
</span>
<span>Followers</span>
</div>
</div>
</div>
);
const HoverCardBasicDemo = () => (
<HoverCard>
<p className="max-w-md text-muted-foreground text-sm leading-relaxed">
Liked by{" "}
<HoverCardTrigger asChild>
<a
className="font-medium text-primary underline underline-offset-2"
href="#hover-basic"
>
{HOVER_PROFILE.username}
</a>
</HoverCardTrigger>{" "}
and 3 others.
</p>
<HoverCardPopup>
<HoverCardProfilePreview />
</HoverCardPopup>
</HoverCard>
);
export default HoverCardBasicDemo;
Context Hook
Panel: closed
@sarah_chenSarah Chen
@sarah_chen
Design Engineer at Acme Inc. Building beautiful interfaces and design systems.
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardPopup,
HoverCardTrigger,
useHoverCardContext,
} from "@/components/ui/hover-card";
const HOVER_PROFILE = {
avatar: "https://i.pravatar.cc/300?u=sarah",
bio: "Design Engineer at Acme Inc. Building beautiful interfaces and design systems.",
name: "Sarah Chen",
username: "@sarah_chen",
} as const;
const HoverCardProfilePreview = () => (
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<img
alt=""
className="size-14 shrink-0 rounded-full object-cover"
src={HOVER_PROFILE.avatar}
/>
<Button size="sm" type="button" variant="secondary">
Follow
</Button>
</div>
<div>
<p className="font-semibold text-foreground text-sm leading-tight">
{HOVER_PROFILE.name}
</p>
<p className="text-muted-foreground text-sm">{HOVER_PROFILE.username}</p>
</div>
<p className="text-foreground text-sm leading-snug">{HOVER_PROFILE.bio}</p>
<div className="flex gap-4 text-muted-foreground text-xs">
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
2,456
</span>
<span>Following</span>
</div>
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
14.5K
</span>
<span>Followers</span>
</div>
</div>
</div>
);
const HoverCardContextHookDemo = () => {
const Status = () => {
const { open } = useHoverCardContext();
return (
<p className="text-muted-foreground text-xs">
Panel: {open ? "open" : "closed"}
</p>
);
};
return (
<HoverCard>
<div className="flex flex-col gap-1">
<Status />
<HoverCardTrigger asChild>
<a
className="font-medium text-primary underline underline-offset-2"
href="#hover-hook"
>
{HOVER_PROFILE.username}
</a>
</HoverCardTrigger>
</div>
<HoverCardPopup>
<HoverCardProfilePreview />
</HoverCardPopup>
</HoverCard>
);
};
export default HoverCardContextHookDemo;
Context
Liked by @sarah_chen
Sarah Chen
@sarah_chen
Design Engineer at Acme Inc. Building beautiful interfaces and design systems.
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContext,
HoverCardPopup,
HoverCardTrigger,
} from "@/components/ui/hover-card";
const HOVER_PROFILE = {
avatar: "https://i.pravatar.cc/300?u=sarah",
bio: "Design Engineer at Acme Inc. Building beautiful interfaces and design systems.",
name: "Sarah Chen",
username: "@sarah_chen",
} as const;
const HoverCardProfilePreview = () => (
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<img
alt=""
className="size-14 shrink-0 rounded-full object-cover"
src={HOVER_PROFILE.avatar}
/>
<Button size="sm" type="button" variant="secondary">
Follow
</Button>
</div>
<div>
<p className="font-semibold text-foreground text-sm leading-tight">
{HOVER_PROFILE.name}
</p>
<p className="text-muted-foreground text-sm">{HOVER_PROFILE.username}</p>
</div>
<p className="text-foreground text-sm leading-snug">{HOVER_PROFILE.bio}</p>
<div className="flex gap-4 text-muted-foreground text-xs">
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
2,456
</span>
<span>Following</span>
</div>
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
14.5K
</span>
<span>Followers</span>
</div>
</div>
</div>
);
const HoverCardContextDemo = () => (
<HoverCard>
<HoverCardContext>
{(ctx) => (
<p className="max-w-md text-muted-foreground text-sm leading-relaxed">
Liked by{" "}
<HoverCardTrigger asChild>
<a
className="inline-flex items-center gap-1 font-medium text-primary underline underline-offset-2"
href="#hover-context"
>
{HOVER_PROFILE.username}
{ctx.open ? (
<ChevronUpIcon className="size-3.5" />
) : (
<ChevronDownIcon className="size-3.5" />
)}
</a>
</HoverCardTrigger>
</p>
)}
</HoverCardContext>
<HoverCardPopup>
<HoverCardProfilePreview />
</HoverCardPopup>
</HoverCard>
);
export default HoverCardContextDemo;
Controlled
Liked by @sarah_chen and 3 others.
Sarah Chen
@sarah_chen
Design Engineer at Acme Inc. Building beautiful interfaces and design systems.
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardPopup,
HoverCardTrigger,
} from "@/components/ui/hover-card";
const HOVER_PROFILE = {
avatar: "https://i.pravatar.cc/300?u=sarah",
bio: "Design Engineer at Acme Inc. Building beautiful interfaces and design systems.",
name: "Sarah Chen",
username: "@sarah_chen",
} as const;
const HoverCardControlledDemo = () => {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col gap-3">
<Button
size="sm"
type="button"
variant="outline"
onClick={() => setOpen((o) => !o)}
>
Toggle
</Button>
<HoverCard onOpenChange={(e) => setOpen(e.open)} open={open}>
<p className="max-w-md text-muted-foreground text-sm leading-relaxed">
Liked by{" "}
<HoverCardTrigger asChild>
<a
className="font-medium text-primary underline underline-offset-2"
href="#hover-controlled"
>
{HOVER_PROFILE.username}
</a>
</HoverCardTrigger>{" "}
and 3 others.
</p>
<HoverCardPopup>
<div className="flex flex-col gap-3">
<img
alt=""
className="size-16 rounded-full object-cover"
src={HOVER_PROFILE.avatar}
/>
<div>
<p className="font-semibold text-foreground text-sm">
{HOVER_PROFILE.name}
</p>
<p className="text-muted-foreground text-sm">
{HOVER_PROFILE.username}
</p>
</div>
<p className="text-foreground text-sm">{HOVER_PROFILE.bio}</p>
</div>
</HoverCardPopup>
</HoverCard>
</div>
);
};
export default HoverCardControlledDemo;
Liked by @sarah_chen and 3 others.
Sarah Chen
@sarah_chen
Design Engineer at Acme Inc. Building beautiful interfaces and design systems.
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardPopup,
HoverCardTrigger,
} from "@/components/ui/hover-card";
const HOVER_PROFILE = {
avatar: "https://i.pravatar.cc/300?u=sarah",
bio: "Design Engineer at Acme Inc. Building beautiful interfaces and design systems.",
name: "Sarah Chen",
username: "@sarah_chen",
} as const;
const HoverCardProfilePreview = () => (
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<img
alt=""
className="size-14 shrink-0 rounded-full object-cover"
src={HOVER_PROFILE.avatar}
/>
<Button size="sm" type="button" variant="secondary">
Follow
</Button>
</div>
<div>
<p className="font-semibold text-foreground text-sm leading-tight">
{HOVER_PROFILE.name}
</p>
<p className="text-muted-foreground text-sm">{HOVER_PROFILE.username}</p>
</div>
<p className="text-foreground text-sm leading-snug">{HOVER_PROFILE.bio}</p>
<div className="flex gap-4 text-muted-foreground text-xs">
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
2,456
</span>
<span>Following</span>
</div>
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
14.5K
</span>
<span>Followers</span>
</div>
</div>
</div>
);
const HoverCardBasicDemo = () => (
<HoverCard>
<p className="max-w-md text-muted-foreground text-sm leading-relaxed">
Liked by{" "}
<HoverCardTrigger asChild>
<a
className="font-medium text-primary underline underline-offset-2"
href="#hover-basic"
>
{HOVER_PROFILE.username}
</a>
</HoverCardTrigger>{" "}
and 3 others.
</p>
<HoverCardPopup>
<HoverCardProfilePreview />
</HoverCardPopup>
</HoverCard>
);
export default HoverCardBasicDemo;
Root Provider
Liked by @sarah_chen
Sarah Chen
@sarah_chen
Design Engineer at Acme Inc. Building beautiful interfaces and design systems.
import { Button } from "@/components/ui/button";
import {
HoverCardPopup,
HoverCardRootProvider,
HoverCardTrigger,
useHoverCard,
} from "@/components/ui/hover-card";
const HOVER_PROFILE = {
avatar: "https://i.pravatar.cc/300?u=sarah",
bio: "Design Engineer at Acme Inc. Building beautiful interfaces and design systems.",
name: "Sarah Chen",
username: "@sarah_chen",
} as const;
const HoverCardProfilePreview = () => (
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<img
alt=""
className="size-14 shrink-0 rounded-full object-cover"
src={HOVER_PROFILE.avatar}
/>
<Button size="sm" type="button" variant="secondary">
Follow
</Button>
</div>
<div>
<p className="font-semibold text-foreground text-sm leading-tight">
{HOVER_PROFILE.name}
</p>
<p className="text-muted-foreground text-sm">{HOVER_PROFILE.username}</p>
</div>
<p className="text-foreground text-sm leading-snug">{HOVER_PROFILE.bio}</p>
<div className="flex gap-4 text-muted-foreground text-xs">
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
2,456
</span>
<span>Following</span>
</div>
<div className="flex gap-1">
<span aria-hidden className="font-medium text-foreground">
14.5K
</span>
<span>Followers</span>
</div>
</div>
</div>
);
const HoverCardRootProviderDemo = () => {
const hoverCard = useHoverCard();
return (
<div className="flex flex-col gap-2">
<output className="text-muted-foreground text-xs">
Open: {String(hoverCard.open)}
</output>
<HoverCardRootProvider value={hoverCard}>
<p className="max-w-md text-muted-foreground text-sm leading-relaxed">
Liked by{" "}
<HoverCardTrigger asChild>
<a
className="font-medium text-primary underline underline-offset-2"
href="#hover-provider"
>
{HOVER_PROFILE.username}
</a>
</HoverCardTrigger>
</p>
<HoverCardPopup>
<HoverCardProfilePreview />
</HoverCardPopup>
</HoverCardRootProvider>
</div>
);
};
export default HoverCardRootProviderDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
HoverCardPopup
| Prop | Type | Description |
|---|---|---|
| arrowClassName? | string | Class on the arrow element. |
| arrowTipClassName? | string | Class on the arrow tip. |
| disablePortal? | boolean | Renders content in-place instead of portaled. |
| positionerClassName? | string | Extra class on the positioner wrapper. |
| showArrow? | boolean | Toggles the arrow (default true). |
See the ARK UI documentation for the full API.