Tooltip
A short label for a control, shown when the pointer rests on it and when it takes keyboard focus.
Release notes
- Rest the pointer on a button: its name shows after 300 ms and goes when the pointer leaves.
- Tab to a button: the name shows at once, and Escape hides it.
- A click hides it too. A tap shows no tooltip, so every button also has an aria-label.
Field marks
- Import
import { Tooltip } from "cennge-ui";- Size
- 0.9 KB gzip for this import, its dependencies aside
- Pattern
- WAI-ARIA tooltip:
role="tooltip", the trigger'saria-describedbywhile shown - Keyboard
- Esc
- State
- Controlled with
isOpenandonOpenChange, or uncontrolled - SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Delay
- 300 ms on hover, none on keyboard focus
- Touch
- Not shown: touch pointers, and the focus a tap gives, do not open it
- Tests
- Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: overlays
- Source
- src/components/tooltip
When to use#
Use a Tooltip to name an icon button, to show the shortcut of a command, or to say where a link goes. It holds a few words, perhaps a shortcut: there is nothing inside to click, and it goes when the pointer leaves.
A tap never shows it, so put nothing there that the reader needs to finish a task. A field's rules belong in its description, an error in its error message.
Similar#
- Popover opens on a click and can hold buttons, fields and links.
- HoverCard previews what a link points to, a card rather than a label.
Name an icon button#
The tooltip describes its trigger, it does not name it: give an icon-only button an aria-label as well. The trigger is your own element, a Button or a link through asChild, and must take a ref.
import { Button, Tooltip, toast } from "cennge-ui";
const install = "pnpm add cennge-ui";
function StarIcon() {
return (
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" aria-hidden="true">
<path d="m12 3.5 2.6 5.3 5.9.9-4.3 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8-4.3-4.1 5.9-.9z" />
</svg>
);
}
function TerminalIcon() {
return (
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m5 7 5 5-5 5m8 0h6" />
</svg>
);
}
function ExternalIcon() {
return (
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M14 5h5v5m0-5-8 8M17 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h4" />
</svg>
);
}
export default function RepositoryActions() {
return (
<div className="flex gap-1">
<Tooltip content="Star cennge-ui">
<Button isIconOnly variant="flat" aria-label="Star" onClick={() => toast("Starred cennge-ui")}>
<StarIcon />
</Button>
</Tooltip>
<Tooltip content={`Copy “${install}”`}>
<Button
isIconOnly
variant="flat"
aria-label="Copy the install command"
onClick={async () => {
await navigator.clipboard.writeText(install);
toast("Copied", { description: install });
}}
>
<TerminalIcon />
</Button>
</Tooltip>
<Tooltip content="github.com/Cennge/cennge-ui">
<Button asChild isIconOnly variant="flat">
<a href="https://github.com/Cennge/cennge-ui" aria-label="Open on GitHub">
<ExternalIcon />
</a>
</Button>
</Tooltip>
</div>
);
}Colour, side and arrow#
color takes the seven colours of the library, placement the side, align the edge it lines up with, and showArrow draws an arrow in the tooltip's colour. When its side has no room it flips to the opposite one, and along its side it slides to stay 8px inside the screen.
import { Button, Tooltip, toast } from "cennge-ui";
function TrashIcon() {
return (
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M5 7h14M10 7V5h4v2m-7 0 .8 11.2a1 1 0 0 0 1 .8h4.4a1 1 0 0 0 1-.8L17 7" />
</svg>
);
}
export default function TooltipColors() {
return (
<div className="flex flex-wrap items-center gap-3">
<Tooltip content="Publishes cennge-ui 0.3.2 to npm" color="primary" placement="bottom" showArrow>
<Button color="primary" onClick={() => toast.success("Published", { description: "cennge-ui@0.3.2" })}>
Publish
</Button>
</Tooltip>
<Tooltip content="Deletes fix/select-chevron for good" color="danger" placement="top" align="start" showArrow>
<Button isIconOnly color="danger" variant="flat" aria-label="Delete branch" onClick={() => toast("Branch deleted")}>
<TrashIcon />
</Button>
</Tooltip>
</div>
);
}One delay for the whole app#
On its own, every Tooltip waits 300 ms before it opens on hover, and delay changes that for one. Under CenngeProvider the tooltips share a provider: tooltipDelay sets the delay for all of them, and once one has opened, moving straight to the next opens it without the wait. Rest on one script below, then slide along the row.
import { Button, CenngeProvider, Tooltip, toast } from "cennge-ui";
const scripts = [
{ name: "Install", command: "pnpm install" },
{ name: "Typecheck", command: "pnpm typecheck" },
{ name: "Unit tests", command: "pnpm test" },
{ name: "End-to-end", command: "pnpm test:e2e" },
{ name: "Build", command: "pnpm build" },
{ name: "Size", command: "pnpm size" },
];
export default function SharedDelay() {
// Usually once, around the whole app.
return (
<CenngeProvider tooltipDelay={500}>
<div className="flex flex-wrap gap-1.5">
{scripts.map((script) => (
<Tooltip key={script.name} content={<code className="font-mono">{script.command}</code>}>
<Button
size="sm"
variant="flat"
onClick={async () => {
await navigator.clipboard.writeText(script.command);
toast("Copied", { description: script.command });
}}
>
{script.name}
</Button>
</Tooltip>
))}
</div>
</CenngeProvider>
);
}Accessibility#
- While it is shown, the tooltip has
role="tooltip"and the trigger'saria-describedbypoints at it, so screen readers read it after the trigger's name. - It opens on hover after the delay and on keyboard focus at once. Escape, a click on the trigger or the pointer leaving closes it; the pointer can move onto the tooltip without closing it. One tooltip shows at a time.
- A scroll of the page or of any box around the trigger closes it. That includes the scroll the browser makes when Tab reaches a trigger below the fold, so that trigger shows no tooltip until it is focused again.
isDisabledrenders the trigger alone, with no tooltip.- The end-to-end tests hover a button and leave it in Chromium, Firefox and WebKit on desktop, and on every profile, the phones included, Tab to it and press Escape. Hover is not tested on the phone profiles: touch screens have no hover.
Styling#
className goes to the tooltip, which is at most 20rem wide, in text-xs. The default colour is bg-surface-4 with a border; the other colours fill it with the colour and its foreground.