Tooltip set
Three tooltip patterns: icon label with shortcut, disabled reason and a two-line hint.
npx shadcn@latest add https://ui.goodtabs.co/r/tooltip-set.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/tooltip-set
Tip names an icon-only button and shows its shortcut as keycaps.
Disabled reason
Add a payment method to publish.
DisabledTip makes the wrapper focusable so the reason is reachable by keyboard.
Two-line hint
Every route is rendered with so first paint stays fast.
Hint attaches a title and a sentence to a term in running text.
Usage
import { Tip, DisabledTip, Hint } from "@/components/ui/tooltip-set"
<Tip label="Copy link" shortcut={["⌘", "C"]}>
<Button size="icon" aria-label="Copy link">...</Button>
</Tip>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | Tip: the label, a few words. | |
| shortcut | string[] | Tip: keycaps shown after the label. | |
| side | "top" | "right" | "bottom" | "left" | "top" | Preferred side on all three. |
| delay | number | 300 | Tip: milliseconds before hover opens it. |
| reason | string | DisabledTip: why the control is disabled. | |
| title | string | Hint: the bold first line. | |
| body | string | Hint: one or two sentences. |
Source
tooltip-set/tooltip-set.tsx
"use client";
import * as React from "react";
import { Kbd } from "@/components/ui/kbd";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
type Side = "top" | "right" | "bottom" | "left";
type TipProps = {
/** The control the tip describes. Rendered with asChild, so it must forward a ref. */
children: React.ReactElement;
/** The label. Keep it to a few words. */
label: string;
/** Shortcut keys shown as keycaps after the label, for example ["⌘", "K"]. */
shortcut?: string[];
side?: Side;
/** Milliseconds before it opens on hover. Keyboard focus opens it straight away. */
delay?: number;
};
const KEY = "border-background/25 bg-background/10 text-background";
/**
* A label tooltip for an icon-only control, with an optional shortcut. The tip repeats the
* control's accessible name, so give the control its own aria-label as well.
*/
export function Tip({ children, label, shortcut, side = "top", delay = 300 }: TipProps) {
return (
<TooltipProvider delayDuration={delay}>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent side={side} sideOffset={6}>
{label}
{shortcut && (
<span className="flex items-center gap-1" aria-hidden="true">
{shortcut.map((k) => (
<Kbd key={k} className={KEY}>
{k}
</Kbd>
))}
</span>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
type DisabledTipProps = {
/** The disabled control. */
children: React.ReactNode;
/** Why it is disabled, and what to do about it. */
reason: string;
side?: Side;
};
/**
* Explains a disabled control. Disabled buttons cannot take focus or fire pointer events,
* so the tip hangs on a focusable wrapper and the reason is wired up with aria-describedby.
*/
export function DisabledTip({ children, reason, side = "top" }: DisabledTipProps) {
const id = React.useId();
return (
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
<span
tabIndex={0}
aria-describedby={id}
className="inline-flex rounded-lg outline-none focus-visible:ring-3 focus-visible:ring-ring/50 [&>*]:pointer-events-none"
>
{children}
</span>
</TooltipTrigger>
<TooltipContent side={side} sideOffset={6}>
{reason}
</TooltipContent>
</Tooltip>
<span id={id} className="sr-only">
{reason}
</span>
</TooltipProvider>
);
}
type HintProps = {
/** The term or icon that needs a longer explanation. */
children: React.ReactElement;
title: string;
body: string;
side?: Side;
};
/**
* A two-line tooltip: a title and a sentence. For a term in running text; the trigger should be a
* real button or link so keyboard users can reach it.
*/
export function Hint({ children, title, body, side = "top" }: HintProps) {
return (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent side={side} sideOffset={6} className="block max-w-56 py-2 leading-snug">
<span className="block font-medium">{title}</span>
<span className="block text-background/75">{body}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
For AI agents
Hand your agent this page, the markdown at /components/tooltip-set/prompt.md, or the Copy AI prompt button above.
When to use
- Naming an icon-only control
- Explaining why a button is disabled
- A short definition for a term in text
When not to use
- Anything interactive or longer than two lines, use a hover card or popover
- Information that touch users need, tooltips do not open on tap
- Replacing a visible label
Agent notes
- Client component; each export wraps its own TooltipProvider so it works without app-level setup
- The tip never replaces an accessible name: give icon buttons an aria-label too
- Keycaps are aria-hidden; put the shortcut on the control with aria-keyshortcuts if it matters
- DisabledTip makes its wrapper tabIndex 0 and sets pointer-events none on the child so hover reaches the wrapper
- Children of Tip and Hint must forward a ref and spread props, as Button and native elements do
- Tooltip content is inverted (bg-foreground), which is the shipped primitive's style