Tooltip
A popup that appears when an element is hovered or focused, showing a hint for sighted users. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipPopup,
TooltipTrigger,
} from "@/components/ui/tooltip";
export default function Particle() {
return (
<Tooltip>
<TooltipTrigger render={<Button variant="outline" />}>
Hover me
</TooltipTrigger>
<TooltipPopup>Helpful hint</TooltipPopup>
</Tooltip>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/tooltip
Usage
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"<Tooltip>
<TooltipTrigger render={<Button variant="outline" />}>
Hover me
</TooltipTrigger>
<TooltipContent>Helpful hint</TooltipContent>
</Tooltip>Grouped Tooltips
Wrap multiple tooltips in TooltipProvider so that once one becomes visible, adjacent tooltips appear instantly without the usual delay:
<TooltipProvider>
<Tooltip>
<TooltipTrigger>...</TooltipTrigger>
<TooltipContent>First</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>...</TooltipTrigger>
<TooltipContent>Second</TooltipContent>
</Tooltip>
</TooltipProvider>TooltipContent Props
side: Which side the tooltip opens from. Options:"top"(default),"bottom","left","right","inline-start","inline-end"align: Alignment relative to the trigger. Options:"center"(default),"start","end"sideOffset: Gap in pixels between the trigger and tooltip. Default:4
<TooltipContent side="bottom" align="start" sideOffset={8}>
Opens below, aligned to the start
</TooltipContent>Examples
Grouped Tooltips
Wrap multiple tooltips in TooltipProvider so they appear instantly after the first one opens. Once any tooltip in the group is visible, adjacent ones skip the delay and show immediately.
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
import {
Tooltip,
TooltipPopup,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export default function Particle() {
return (
<TooltipProvider>
<ToggleGroup defaultValue={["bold"]} multiple>
<Tooltip>
<TooltipTrigger
render={<ToggleGroupItem aria-label="Toggle bold" value="bold" />}
>
<BoldIcon />
</TooltipTrigger>
<TooltipPopup>Bold</TooltipPopup>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<ToggleGroupItem aria-label="Toggle italic" value="italic" />
}
>
<ItalicIcon />
</TooltipTrigger>
<TooltipPopup>Italic</TooltipPopup>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<ToggleGroupItem
aria-label="Toggle underline"
value="underline"
/>
}
>
<UnderlineIcon />
</TooltipTrigger>
<TooltipPopup>Underline</TooltipPopup>
</Tooltip>
</ToggleGroup>
</TooltipProvider>
);
}
Animated Tooltips
Create tooltips that smoothly transition between triggers using a shared handle. All triggers share a single popup — position, size, and content animate automatically as focus moves between them.
To create detached triggers:
- Create a handle using
TooltipCreateHandle - Attach the same handle to multiple
TooltipTriggercomponents - Each trigger provides a
payloadprop containing the content component - Use a single
Tooltipcomponent with the handle to render the popup
"use client";
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import type { ComponentType } from "react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
import {
Tooltip,
TooltipCreateHandle,
TooltipPopup,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const tooltipHandle = TooltipCreateHandle<ComponentType>();
const BoldContent = () => {
return <span>Make text bold</span>;
};
const ItalicContent = () => {
return <span>Apply italic formatting to text</span>;
};
const UnderlineContent = () => {
return <span>Underline text</span>;
};
export default function Particle() {
return (
<TooltipProvider>
<ToggleGroup defaultValue={["bold"]} multiple>
<TooltipTrigger
className="after:absolute after:left-full after:h-full after:w-1"
handle={tooltipHandle}
payload={BoldContent}
render={<ToggleGroupItem aria-label="Toggle bold" value="bold" />}
>
<BoldIcon aria-hidden="true" />
</TooltipTrigger>
<TooltipTrigger
className="after:absolute after:left-full after:h-full after:w-1"
handle={tooltipHandle}
payload={ItalicContent}
render={<ToggleGroupItem aria-label="Toggle italic" value="italic" />}
>
<ItalicIcon aria-hidden="true" />
</TooltipTrigger>
<TooltipTrigger
className="after:absolute after:left-full after:h-full after:w-1"
handle={tooltipHandle}
payload={UnderlineContent}
render={
<ToggleGroupItem aria-label="Toggle underline" value="underline" />
}
>
<UnderlineIcon aria-hidden="true" />
</TooltipTrigger>
</ToggleGroup>
<Tooltip handle={tooltipHandle}>
{({ payload: Payload }) => (
<TooltipPopup>{Payload !== undefined && <Payload />}</TooltipPopup>
)}
</Tooltip>
</TooltipProvider>
);
}
Placement
Use the side prop on TooltipContent to control which side of the trigger the tooltip appears on. Defaults to "top".
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
const sides = [
"inline-start",
"left",
"top",
"bottom",
"right",
"inline-end",
] as const;
export function Pattern() {
return (
<div className="grid max-w-xs grid-cols-3 gap-2">
{sides.map((side) => (
<Tooltip key={side}>
<TooltipTrigger
render={<Button className="w-full" variant="outline" />}
>
{side.replace("-", " ")[0]?.toUpperCase() +
side.replace("-", " ").slice(1)}
</TooltipTrigger>
<TooltipContent side={side}>
<p className="text-sm">Add to library</p>
</TooltipContent>
</Tooltip>
))}
</div>
);
}
Info Icon
A common pattern for inline contextual help — an icon-only ghost button that reveals explanatory text on hover without cluttering the layout.
import { InfoIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<Tooltip>
<TooltipTrigger
render={
<Button aria-label="More information" size="icon" variant="ghost" />
}
>
<InfoIcon />
</TooltipTrigger>
<TooltipContent>
<p className="text-center text-sm">
Additional information and help context.
</p>
</TooltipContent>
</Tooltip>
</div>
);
}
Rich Tooltip
Tooltips can contain structured content beyond a single string — badges, lists, and links are all valid. Keep rich tooltips concise so they supplement rather than replace a proper UI surface.
import { ArrowRightIcon, InfoIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<Tooltip>
<TooltipTrigger
render={
<Button aria-label="Notifications" size="icon" variant="outline" />
}
>
<div className="relative">
<InfoIcon />
<Badge
className="absolute -top-3.5 -right-3.5"
size="sm"
variant="destructive"
>
3
</Badge>
</div>
</TooltipTrigger>
<TooltipContent className="p-3">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<span className="font-medium">Notifications</span>
<Badge size="sm" variant="destructive">
3 new
</Badge>
</div>
<div className="flex flex-col gap-1 opacity-80">
<p>• Sarah commented on your PR</p>
<p>• Build #421 completed</p>
<p>• New team member joined</p>
</div>
<a
className="flex items-center gap-1 font-medium underline underline-offset-2"
href="#"
>
View all
<ArrowRightIcon className="size-3.5" />
</a>
</div>
</TooltipContent>
</Tooltip>
</div>
);
}
Keyboard Shortcut Toolbar
Each icon-only button in the toolbar shows its label and keyboard shortcut in a tooltip. Pairing TooltipProvider with Kbd components gives users a discoverable reference without permanent labels.
import {
AlignCenterIcon,
AlignJustifyIcon,
AlignLeftIcon,
AlignRightIcon,
BoldIcon,
ItalicIcon,
StrikethroughIcon,
UnderlineIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const tools = [
{ icon: BoldIcon, label: "Bold", shortcut: ["⌘", "B"] },
{ icon: ItalicIcon, label: "Italic", shortcut: ["⌘", "I"] },
{ icon: UnderlineIcon, label: "Underline", shortcut: ["⌘", "U"] },
{
icon: StrikethroughIcon,
label: "Strikethrough",
shortcut: ["⌘", "⇧", "X"],
},
] as const;
const aligns = [
{ icon: AlignLeftIcon, label: "Align left", shortcut: ["⌘", "⇧", "L"] },
{ icon: AlignCenterIcon, label: "Align center", shortcut: ["⌘", "⇧", "E"] },
{ icon: AlignRightIcon, label: "Align right", shortcut: ["⌘", "⇧", "R"] },
{ icon: AlignJustifyIcon, label: "Justify", shortcut: ["⌘", "⇧", "J"] },
] as const;
export function Pattern() {
return (
<div className="flex items-center justify-center">
<TooltipProvider>
<div className="flex items-center gap-1 rounded-lg border bg-background p-1 shadow-sm">
{tools.map(({ icon: Icon, label, shortcut }) => (
<Tooltip key={label}>
<TooltipTrigger render={<Button size="icon" variant="ghost" />}>
<Icon aria-hidden="true" className="size-4" />
<span className="sr-only">{label}</span>
</TooltipTrigger>
<TooltipContent className="px-2.5 py-2">
<div className="flex items-center gap-3">
<span>{label}</span>
<span className="flex items-center gap-0.5">
{shortcut.map((k) => (
<Kbd key={k}>{k}</Kbd>
))}
</span>
</div>
</TooltipContent>
</Tooltip>
))}
<div className="mx-1 h-5 w-px bg-border" />
{aligns.map(({ icon: Icon, label, shortcut }) => (
<Tooltip key={label}>
<TooltipTrigger render={<Button size="icon" variant="ghost" />}>
<Icon aria-hidden="true" className="size-4" />
<span className="sr-only">{label}</span>
</TooltipTrigger>
<TooltipContent className="px-2.5 py-2">
<div className="flex items-center gap-3">
<span>{label}</span>
<span className="flex items-center gap-0.5">
{shortcut.map((k) => (
<Kbd key={k}>{k}</Kbd>
))}
</span>
</div>
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
</div>
);
}
Avatar Stack
Stacked member avatars with per-avatar name and role tooltips. The overflow badge lists remaining members, and the dashed add button completes the assignment UI pattern.
import { PlusIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const members = [
{
initials: "SC",
name: "Sarah Chen",
role: "Lead Designer",
src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&h=64&dpr=2&q=80",
},
{
initials: "MK",
name: "Marcus Kim",
role: "Frontend Engineer",
src: "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?w=64&h=64&dpr=2&q=80",
},
{
initials: "AJ",
name: "Aisha Johnson",
role: "Product Manager",
src: "https://images.unsplash.com/photo-1517841905240-472988babdf9?w=64&h=64&dpr=2&q=80",
},
{
initials: "LR",
name: "Liam Rivera",
role: "Backend Engineer",
src: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=64&h=64&dpr=2&q=80",
},
{
initials: "PW",
name: "Priya Wang",
role: "QA Engineer",
src: "",
},
] as const;
const overflow = ["Nina Patel", "Omar Hassan", "Lea Müller"];
export function Pattern() {
return (
<div className="flex min-h-[100px] items-center justify-center">
<TooltipProvider>
<div className="flex items-center">
{members.map((member, i) => (
<Tooltip key={member.name}>
<TooltipTrigger
className="focus-visible:outline-none"
style={{ zIndex: members.length - i }}
>
<Avatar className="-ml-2.5 size-9 ring-2 ring-background transition-transform first:ml-0 hover:z-10 hover:-translate-y-1">
{member.src && (
<AvatarImage alt={member.name} src={member.src} />
)}
<AvatarFallback className="text-xs">
{member.initials}
</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent className="px-3 py-2">
<p className="font-medium">{member.name}</p>
<p className="text-muted-foreground">{member.role}</p>
</TooltipContent>
</Tooltip>
))}
<Tooltip>
<TooltipTrigger className="-ml-2.5 focus-visible:outline-none">
<div className="flex size-9 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground text-xs ring-2 ring-background transition-colors hover:bg-muted/80">
+{overflow.length}
</div>
</TooltipTrigger>
<TooltipContent className="px-3 py-2">
<p className="mb-1 font-medium">{overflow.length} more members</p>
<div className="space-y-0.5">
{overflow.map((name) => (
<p className="text-muted-foreground" key={name}>
{name}
</p>
))}
</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger className="-ml-2 focus-visible:outline-none">
<div className="flex size-9 items-center justify-center rounded-full border-2 border-border border-dashed bg-background text-muted-foreground transition-colors hover:bg-muted">
<PlusIcon className="size-4" />
</div>
</TooltipTrigger>
<TooltipContent>Add member</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
);
}
Trend Metrics
Compact metric badges that reveal a current-vs-previous breakdown on hover. The tooltip surfaces the exact numbers behind the percentage change without expanding the card layout.
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type Metric = {
label: string;
value: string;
change: number;
current: string;
previous: string;
period: string;
};
const metrics: Metric[] = [
{
change: 18.4,
current: "24,892",
label: "Page Views",
period: "vs last month",
previous: "21,025",
value: "+18.4%",
},
{
change: -3.2,
current: "3.8%",
label: "Bounce Rate",
period: "vs last month",
previous: "3.9%",
value: "-3.2%",
},
{
change: 7.1,
current: "1,204",
label: "Conversions",
period: "vs last month",
previous: "1,124",
value: "+7.1%",
},
];
export function Pattern() {
return (
<div className="flex min-h-[100px] items-center justify-center">
<TooltipProvider>
<div className="flex gap-4">
{metrics.map((metric) => {
const isUp = metric.change > 0;
const TrendIcon = isUp ? TrendingUpIcon : TrendingDownIcon;
return (
<Tooltip key={metric.label}>
<TooltipTrigger className="cursor-default rounded-lg border bg-background px-4 py-3 text-left shadow-xs transition-shadow hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<p className="text-muted-foreground text-xs">
{metric.label}
</p>
<div className="mt-1 flex items-center gap-1.5">
<TrendIcon
aria-hidden="true"
className={`size-3.5 ${isUp ? "text-emerald-500" : "text-rose-500"}`}
/>
<span
className={`font-semibold text-sm tabular-nums ${isUp ? "text-emerald-600 dark:text-emerald-400" : "text-rose-600 dark:text-rose-400"}`}
>
{metric.value}
</span>
</div>
</TooltipTrigger>
<TooltipContent className="px-3 py-2.5">
<p className="mb-1.5 font-medium">{metric.label}</p>
<div className="space-y-1 text-muted-foreground">
<div className="flex items-center justify-between gap-6">
<span>Current</span>
<span className="font-medium text-foreground tabular-nums">
{metric.current}
</span>
</div>
<div className="flex items-center justify-between gap-6">
<span>Previous</span>
<span className="tabular-nums">{metric.previous}</span>
</div>
<div className="mt-1 flex items-center justify-between gap-6 border-t pt-1">
<span>{metric.period}</span>
<span
className={`font-semibold tabular-nums ${isUp ? "text-emerald-500" : "text-rose-500"}`}
>
{metric.value}
</span>
</div>
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</TooltipProvider>
</div>
);
}
Color Palette Inspector
A brand palette swatch row where hovering each swatch reveals the color name and hex code. The mini swatch preview inside the tooltip reinforces the selection at a glance.
Brand palette — hover to inspect
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const palette = [
{ hex: "#0f172a", name: "Slate 950" },
{ hex: "#1e3a5f", name: "Navy" },
{ hex: "#2563eb", name: "Blue 600" },
{ hex: "#7c3aed", name: "Violet 600" },
{ hex: "#db2777", name: "Pink 600" },
{ hex: "#dc2626", name: "Red 600" },
{ hex: "#ea580c", name: "Orange 600" },
{ hex: "#ca8a04", name: "Yellow 600" },
{ hex: "#16a34a", name: "Green 600" },
{ hex: "#0891b2", name: "Cyan 600" },
{ hex: "#64748b", name: "Slate 500" },
{ hex: "#f8fafc", name: "Slate 50" },
] as const;
export function Pattern() {
return (
<div className="flex min-h-[100px] items-center justify-center">
<TooltipProvider>
<div className="space-y-2">
<p className="text-center text-muted-foreground text-xs">
Brand palette — hover to inspect
</p>
<div className="flex gap-1.5 rounded-xl border bg-muted/40 p-2.5">
{palette.map(({ hex, name }) => (
<Tooltip key={hex}>
<TooltipTrigger className="focus-visible:outline-none">
<div
className="size-8 rounded-md shadow-sm ring-1 ring-black/10 transition-transform hover:scale-110 hover:ring-2 hover:ring-black/20 dark:ring-white/10 dark:hover:ring-white/25"
style={{ backgroundColor: hex }}
/>
<span className="sr-only">{name}</span>
</TooltipTrigger>
<TooltipContent className="px-2.5 py-2">
<div className="flex items-center gap-2">
<div
className="size-3 rounded-sm ring-1 ring-black/10 dark:ring-white/10"
style={{ backgroundColor: hex }}
/>
<div>
<p className="font-medium leading-none">{name}</p>
<p className="mt-0.5 font-mono text-muted-foreground uppercase">
{hex}
</p>
</div>
</div>
</TooltipContent>
</Tooltip>
))}
</div>
</div>
</TooltipProvider>
</div>
);
}
Copy to Clipboard
A credentials panel where each field has a copy icon button. The tooltip content switches from "Copy to clipboard" to "Copied!" for 2 seconds after the click — giving precise feedback without a toast notification.
API Key
sk_live_9xKqP2mN...8rTv
Endpoint
https://api.cnippet.ui/v2
Webhook
https://hooks.cnippet.ui/events
"use client";
import {
CheckIcon,
CopyIcon,
KeyIcon,
LinkIcon,
WebhookIcon,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const fields = [
{ icon: KeyIcon, label: "API Key", value: "sk_live_9xKqP2mN...8rTv" },
{ icon: LinkIcon, label: "Endpoint", value: "https://api.cnippet.ui/v2" },
{
icon: WebhookIcon,
label: "Webhook",
value: "https://hooks.cnippet.ui/events",
},
] as const;
export function Pattern() {
const [copiedId, setCopiedId] = useState<string | null>(null);
const copy = (value: string, id: string) => {
navigator.clipboard.writeText(value).catch(() => null);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
};
return (
<div className="flex min-h-25 items-center justify-center">
<TooltipProvider>
<div className="w-72 space-y-1.5 rounded-xl border bg-background p-3 shadow-xs">
{fields.map(({ icon: Icon, label, value }) => (
<div
className="flex items-center gap-2.5 rounded-lg bg-muted/50 px-3 py-2"
key={label}
>
<Icon
aria-hidden="true"
className="size-3.5 shrink-0 text-muted-foreground"
/>
<div className="min-w-0 flex-1">
<p className="font-semibold text-[10px] text-muted-foreground uppercase tracking-wide">
{label}
</p>
<p className="truncate font-mono text-xs">{value}</p>
</div>
<Tooltip>
<TooltipTrigger
className="shrink-0"
onClick={() => copy(value, label)}
render={
<Button
aria-label={
copiedId === label ? "Copied" : `Copy ${label}`
}
className="size-6"
size="icon"
variant="ghost"
/>
}
>
{copiedId === label ? (
<CheckIcon
aria-hidden="true"
className="size-3.5 text-emerald-500"
/>
) : (
<CopyIcon aria-hidden="true" className="size-3.5" />
)}
</TooltipTrigger>
<TooltipContent>
{copiedId === label ? "Copied!" : "Copy to clipboard"}
</TooltipContent>
</Tooltip>
</div>
))}
</div>
</TooltipProvider>
</div>
);
}
Feature Gate
Pro-only toolbar buttons render at reduced opacity and show a rich upgrade tooltip with a feature description and a direct upgrade link. Free-tier buttons fall back to a plain label tooltip.
import {
ArrowRightIcon,
CrownIcon,
ImageIcon,
PaletteIcon,
PlugIcon,
SparklesIcon,
ZapIcon,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type Feature = {
icon: React.ElementType;
label: string;
description: string;
pro: boolean;
};
const features: Feature[] = [
{
description: "Automate repetitive tasks",
icon: ZapIcon,
label: "Automations",
pro: false,
},
{
description: "Browse and insert media",
icon: ImageIcon,
label: "Media Library",
pro: false,
},
{
description: "Build custom color themes",
icon: PaletteIcon,
label: "Custom Themes",
pro: true,
},
{
description: "Connect third-party tools",
icon: PlugIcon,
label: "Integrations",
pro: true,
},
{
description: "Generate content with AI",
icon: SparklesIcon,
label: "AI Assist",
pro: true,
},
];
export function Pattern() {
return (
<div className="flex min-h-25 items-center justify-center">
<TooltipProvider>
<div className="flex items-center gap-1 rounded-lg border bg-background p-1 shadow-xs">
{features.map(({ icon: Icon, label, description, pro }) => (
<Tooltip key={label}>
<TooltipTrigger
render={
<Button
aria-label={label}
className={pro ? "opacity-40" : ""}
size="icon"
variant="ghost"
/>
}
>
<Icon aria-hidden="true" className="size-4" />
</TooltipTrigger>
<TooltipContent className={pro ? "p-3" : undefined}>
{pro ? (
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<span className="font-medium">{label}</span>
<Badge className="gap-1" size="sm" variant="outline">
<CrownIcon aria-hidden="true" className="size-2.5" />
Pro
</Badge>
</div>
<p className="text-muted-foreground">{description}.</p>
<a
className="flex items-center gap-1 font-medium text-primary underline-offset-2 hover:underline"
href="#"
>
Upgrade to Pro
<ArrowRightIcon aria-hidden="true" className="size-3" />
</a>
</div>
) : (
label
)}
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
</div>
);
}
Form Validation Hints
Info icons next to field labels reveal format requirements on hover — username rules, password strength criteria, and email usage policy — without cluttering the form layout.
import { InfoIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type Field = {
id: string;
label: string;
placeholder: string;
hint: string;
};
const fields: Field[] = [
{
hint: "3–20 characters, letters, numbers, and underscores only.",
id: "username",
label: "Username",
placeholder: "john_doe",
},
{
hint: "Must be 8+ chars with at least one uppercase letter and one number.",
id: "password",
label: "Password",
placeholder: "••••••••",
},
{
hint: "We only use this for account recovery — no marketing emails.",
id: "email",
label: "Email",
placeholder: "you@example.com",
},
];
export function Pattern() {
return (
<div className="w-full max-w-sm space-y-4">
<TooltipProvider>
{fields.map(({ id, label, placeholder, hint }) => (
<div className="space-y-1.5" key={id}>
<div className="flex items-center gap-1.5">
<Label htmlFor={id}>{label}</Label>
<Tooltip>
<TooltipTrigger
aria-label={`${label} format requirements`}
className="inline-flex cursor-default text-muted-foreground hover:text-foreground"
>
<InfoIcon className="size-3.5" />
</TooltipTrigger>
<TooltipContent className="max-w-52">{hint}</TooltipContent>
</Tooltip>
</div>
<Input id={id} placeholder={placeholder} />
</div>
))}
</TooltipProvider>
</div>
);
}
System Status Indicators
A status bar where colored dots represent live service health. Hovering any dot reveals the service name, current status, uptime percentage, and average latency.
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type ServiceStatus = "operational" | "degraded" | "down";
type Service = {
name: string;
uptime: string;
latency: string;
status: ServiceStatus;
};
const services: Service[] = [
{
latency: "12 ms",
name: "API Gateway",
status: "operational",
uptime: "99.99%",
},
{
latency: "8 ms",
name: "Auth Service",
status: "operational",
uptime: "99.97%",
},
{
latency: "340 ms",
name: "Image CDN",
status: "degraded",
uptime: "98.12%",
},
{
latency: "22 ms",
name: "Database",
status: "operational",
uptime: "99.95%",
},
{ latency: "—", name: "Email Worker", status: "down", uptime: "94.60%" },
{
latency: "18 ms",
name: "Search Index",
status: "operational",
uptime: "99.90%",
},
];
const dotClass: Record<ServiceStatus, string> = {
degraded: "bg-amber-400",
down: "bg-red-500",
operational: "bg-emerald-500",
};
const statusLabel: Record<ServiceStatus, string> = {
degraded: "Degraded",
down: "Down",
operational: "Operational",
};
export function Pattern() {
return (
<div className="flex items-center gap-3 rounded-xl border bg-card p-4">
<span className="text-muted-foreground text-xs">Services</span>
<TooltipProvider>
<div className="flex items-center gap-2">
{services.map(({ name, uptime, latency, status }) => (
<Tooltip key={name}>
<TooltipTrigger className="cursor-default">
<span
aria-label={`${name}: ${statusLabel[status]}`}
className={`block size-2.5 rounded-full ${dotClass[status]}`}
/>
</TooltipTrigger>
<TooltipContent className="space-y-0.5 p-2.5">
<p className="font-medium">{name}</p>
<p className="text-muted-foreground">{statusLabel[status]}</p>
<p className="text-muted-foreground">Uptime {uptime}</p>
<p className="text-muted-foreground">Latency {latency}</p>
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
</div>
);
}
Sidebar Navigation
Icon-only nav buttons with tooltips anchored to the right side, showing the destination label and a one-line description. The active item is highlighted so the icon alone conveys context.
import {
BarChart2Icon,
FileTextIcon,
HomeIcon,
SettingsIcon,
UsersIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type NavItem = {
icon: React.ElementType;
label: string;
description: string;
active?: boolean;
};
const navItems: NavItem[] = [
{
active: true,
description: "Your workspace overview",
icon: HomeIcon,
label: "Home",
},
{
description: "Visitor and revenue analytics",
icon: BarChart2Icon,
label: "Analytics",
},
{
description: "Manage team members and roles",
icon: UsersIcon,
label: "Team",
},
{
description: "Create and edit documents",
icon: FileTextIcon,
label: "Docs",
},
{
description: "App preferences and integrations",
icon: SettingsIcon,
label: "Settings",
},
];
export function Pattern() {
return (
<div className="flex min-h-40 items-start">
<TooltipProvider>
<nav className="flex flex-col items-center gap-1 rounded-xl border bg-card p-1.5">
{navItems.map(({ icon: Icon, label, description, active }) => (
<Tooltip key={label}>
<TooltipTrigger
render={
<Button
aria-label={label}
className={active ? "bg-accent text-accent-foreground" : ""}
size="icon"
variant="ghost"
/>
}
>
<Icon className="size-4" />
</TooltipTrigger>
<TooltipContent className="p-2.5" side="right">
<p className="font-medium">{label}</p>
<p className="text-muted-foreground">{description}</p>
</TooltipContent>
</Tooltip>
))}
</nav>
</TooltipProvider>
</div>
);
}
Permissions Matrix
A role-by-resource access table where each cell icon (full / read / none) reveals a tooltip explaining what that access level means for that specific resource.
| Resource | admin | editor | viewer |
|---|---|---|---|
| Members | |||
| Documents | |||
| Integrations | |||
| Audit Logs |
import { CheckIcon, MinusIcon } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type Access = "full" | "read" | "none";
type Permission = {
resource: string;
admin: Access;
editor: Access;
viewer: Access;
detail: string;
};
const permissions: Permission[] = [
{
admin: "full",
detail: "Create, update, and delete workspace members and their roles.",
editor: "none",
resource: "Members",
viewer: "none",
},
{
admin: "full",
detail: "Publish, archive, and permanently remove documents.",
editor: "full",
resource: "Documents",
viewer: "read",
},
{
admin: "full",
detail: "Manage API keys, OAuth apps, and third-party connections.",
editor: "read",
resource: "Integrations",
viewer: "none",
},
{
admin: "full",
detail: "View and export activity logs for auditing.",
editor: "read",
resource: "Audit Logs",
viewer: "none",
},
];
const roles = ["admin", "editor", "viewer"] as const;
function AccessCell({ access, detail }: { access: Access; detail: string }) {
const icon =
access === "full" ? (
<CheckIcon className="size-3.5 text-emerald-500" />
) : access === "read" ? (
<CheckIcon className="size-3.5 text-amber-500" />
) : (
<MinusIcon className="size-3.5 text-muted-foreground" />
);
const label =
access === "full"
? "Full access"
: access === "read"
? "Read only"
: "No access";
return (
<Tooltip>
<TooltipTrigger className="flex cursor-default items-center justify-center">
{icon}
</TooltipTrigger>
<TooltipContent className="p-2.5">
<p className="font-medium">{label}</p>
<p className="max-w-44 text-muted-foreground">{detail}</p>
</TooltipContent>
</Tooltip>
);
}
export function Pattern() {
return (
<div className="w-full max-w-sm overflow-hidden rounded-xl border bg-card">
<TooltipProvider>
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/40">
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">
Resource
</th>
{roles.map((role) => (
<th
className="px-4 py-2.5 text-center font-medium text-muted-foreground capitalize"
key={role}
>
{role}
</th>
))}
</tr>
</thead>
<tbody>
{permissions.map(({ resource, admin, editor, viewer, detail }) => (
<tr className="border-b last:border-0" key={resource}>
<td className="px-4 py-2.5 font-medium">{resource}</td>
<td className="px-4 py-2.5 text-center">
<AccessCell access={admin} detail={detail} />
</td>
<td className="px-4 py-2.5 text-center">
<AccessCell access={editor} detail={detail} />
</td>
<td className="px-4 py-2.5 text-center">
<AccessCell access={viewer} detail={detail} />
</td>
</tr>
))}
</tbody>
</table>
</TooltipProvider>
</div>
);
}
Deployment Pipeline
A linear pipeline view with step icons (done, running, pending) connected by lines. Hovering each step reveals its name, status, duration, and a one-line detail message.
import {
CheckCircle2Icon,
CircleIcon,
LoaderIcon,
XCircleIcon,
} from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type StepStatus = "done" | "running" | "pending" | "failed";
type PipelineStep = {
name: string;
status: StepStatus;
duration: string;
detail: string;
};
const steps: PipelineStep[] = [
{
detail: "All 248 unit tests passed in 14 s.",
duration: "14 s",
name: "Test",
status: "done",
},
{
detail: "TypeScript compiled without errors; bundle size 142 kB.",
duration: "22 s",
name: "Build",
status: "done",
},
{
detail: "Docker image pushed to registry as v2.4.1.",
duration: "31 s",
name: "Publish",
status: "done",
},
{
detail: "Rolling update in progress — 3 of 5 pods updated.",
duration: "~45 s",
name: "Deploy",
status: "running",
},
{
detail: "Smoke tests will run once all pods are healthy.",
duration: "—",
name: "Verify",
status: "pending",
},
];
const statusIcon: Record<StepStatus, React.ReactElement> = {
done: <CheckCircle2Icon className="size-5 text-emerald-500" />,
failed: <XCircleIcon className="size-5 text-red-500" />,
pending: <CircleIcon className="size-5 text-muted-foreground" />,
running: <LoaderIcon className="size-5 animate-spin text-blue-500" />,
};
const statusLabel: Record<StepStatus, string> = {
done: "Completed",
failed: "Failed",
pending: "Waiting",
running: "In progress",
};
export function Pattern() {
return (
<div className="flex items-center gap-1 rounded-xl border bg-card px-5 py-4">
<TooltipProvider>
{steps.map(({ name, status, duration, detail }, index) => (
<div className="flex items-center gap-1" key={name}>
{index > 0 && (
<div
className={`h-px w-8 ${
status === "done" ? "bg-emerald-500" : "bg-border"
}`}
/>
)}
<Tooltip>
<TooltipTrigger className="flex cursor-default flex-col items-center gap-1">
{statusIcon[status]}
<span className="text-muted-foreground text-xs">{name}</span>
</TooltipTrigger>
<TooltipContent className="p-2.5">
<p className="font-medium">{name}</p>
<p className="text-muted-foreground">{statusLabel[status]}</p>
<p className="text-muted-foreground">Duration: {duration}</p>
<p className="mt-1 max-w-48 text-muted-foreground">{detail}</p>
</TooltipContent>
</Tooltip>
</div>
))}
</TooltipProvider>
</div>
);
}
On This Page

