Tooltip
StableA few words on hover (after a short wait) or keyboard focus, in the top layer. Esc hides it. Best for naming icon buttons.
Wrap one focusable element. The tip shows 450 ms after the pointer arrives, or at once when the element takes focus. The pointer can move onto the tip and rest there; it hides 100 ms after the pointer has left both, when focus moves on, or on Esc. Only one tip shows at a time: opening one closes any other. It sits above the element (or below with side="bottom") and flips when there is no room.
Naming icon buttons
import { useState } from 'react'import { IconButton, Tooltip } from '@rchursin/product/ui'
/** * Hover and wait a moment, or Tab to a button: the tip shows at once on focus. Esc hides it. * Each IconButton passes noTooltip, so the browser's own title tooltip doesn't show as well. */export default function TooltipBasic() { const [zoom, setZoom] = useState(100) const step = (by: number) => setZoom((z) => Math.min(200, Math.max(50, z + by)))
return ( <div className="flex items-center gap-1 rounded-xl p-1 ring-1 ring-line ring-inset"> <Tooltip content="Zoom out"> <IconButton icon="zoom-out" label="Zoom out" noTooltip onClick={() => step(-25)} /> </Tooltip> <output aria-live="polite" className="w-12 text-center font-mono text-xs text-fg-2 tabular-nums"> {zoom}% </output> <Tooltip content="Zoom in"> <IconButton icon="zoom-in" label="Zoom in" noTooltip onClick={() => step(25)} /> </Tooltip> <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" /> <Tooltip content="Reset to 100%"> <IconButton icon="rotate-ccw" label="Reset to 100%" noTooltip onClick={() => setZoom(100)} /> </Tooltip> </div> )}Usage
import { Tooltip } from '@rchursin/product/ui'<Tooltip content="Zoom in"> <IconButton icon="zoom-in" label="Zoom in" noTooltip onClick={zoomIn} /></Tooltip>Examples
Sides
Below for a toolbar along the top edge, above (the default) for one along the bottom.
import { IconButton, Tooltip } from '@rchursin/product/ui'
/** * side="bottom" for a toolbar along the top edge, the default "top" for one along the bottom. * Either way it flips when there is no room. The tip names the action, like the button does. */export default function TooltipSides() { return ( <div className="flex w-full max-w-sm flex-col overflow-hidden rounded-xl ring-1 ring-line ring-inset"> <div className="flex items-center justify-between gap-2 border-b border-line px-2 py-1.5"> <span className="px-1.5 text-xs font-medium">Sample note</span> <div className="flex items-center"> <Tooltip content="Pin note" side="bottom"> <IconButton icon="pin" label="Pin note" size="sm" noTooltip /> </Tooltip> <Tooltip content="Share note" side="bottom"> <IconButton icon="share" label="Share note" size="sm" noTooltip /> </Tooltip> </div> </div> <p className="px-3.5 py-6 text-[13px]/6 text-fg-2">Split the cost report by model before Friday, and flag every run over a dollar.</p> <div className="flex items-center gap-0.5 border-t border-line px-2 py-1.5"> <Tooltip content="Bold"> <IconButton icon="bold" label="Bold" size="sm" noTooltip /> </Tooltip> <Tooltip content="Italic"> <IconButton icon="italic" label="Italic" size="sm" noTooltip /> </Tooltip> <Tooltip content="Add a link"> <IconButton icon="link" label="Add a link" size="sm" noTooltip /> </Tooltip> </div> </div> )}API
Tooltip props
| Prop | Type | Default | Description |
|---|---|---|---|
contentrequired | ReactNode | – | What the tip says. A few words; the pointer can rest on it and select the text. |
childrenrequired | ReactElement | – | One focusable element that passes aria-describedby on to its DOM node: a native element, Button or IconButton. |
side | 'top' | 'bottom' | 'top' | The side to try first. It flips when there is no room. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Focusing the element shows the tip straight away; it stays while it has keyboard focus, even if the pointer leaves, and hides when focus moves on. |
| Esc | Hides the tip. |
- The tip is role="tooltip" in a manual popover, so it shows in the top layer and is never clipped.
- The element itself carries aria-describedby pointing at the tip (added to any description it already has), so screen readers read the tip as its description when it takes focus, whether or not the tip is showing yet.
- Hoverable, dismissible and persistent (WCAG 1.4.13): the pointer can cross the 6 pixel gap onto the tip without it closing, Esc hides it without moving focus, and it stays until the pointer and keyboard focus have both left.
- Touch screens have no hover, so a tooltip can’t be the only place something is said.
Guidelines
Do
- Use it to name icon buttons, with the same words as their label.
- Pass noTooltip to IconButton so the browser’s title tooltip doesn’t show as well.
Don’t
- Don’t put links, buttons or anything to copy inside it.
- Don’t say anything in it that isn’t also on screen or in the element’s name.
- Don’t wrap something that can’t take focus; keyboard users would never see the tip.
In use
Related
- Icon buttonA square button with just an icon. The label is required: it is the accessible name and the tooltip.
- PopoverA panel beside its trigger, on the native popover API: never clipped, closed by Esc or a click outside, and it hands focus back.
- Key capsKeyboard keys cut from the same metal as the buttons, and shortcuts written as key caps.
Source
The component’s own files, exactly as they run on this site.
/** * Small pieces: avatars, chips, tooltip, empty state. */import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement, type ReactNode,} from "react"import { cx } from "../../components/cx"import { Icon, type IconName } from "../icons/Icon"import { computePosition } from "./popover"
/* ---------- Avatar: initials on a grey square (the brand square, softened) ---------- */
const tones = [