Tooltip
A short label revealed on hover or focus, for controls whose purpose is not obvious.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/tooltip.jsonUsage
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'<Tooltip>
<TooltipTrigger render={<Button size="icon-md" aria-label="Export"><DownloadIcon /></Button>} />
<TooltipContent>Export study as DICOM</TooltipContent>
</Tooltip>Tooltip provides its own context, so a single tooltip does not need a TooltipProvider around
it. The trigger is your element, by way of render — one node, one set of handlers, no wrapper
that swallows the layout.
A tooltip is not a label
It appears on hover and focus, which means it is unavailable on touch and gone the moment attention moves. Anything a person needs in order to decide belongs on the page.
{/* Wrong: the only name is in the tooltip */}
<TooltipTrigger render={<Button size="icon-md"><DownloadIcon /></Button>} />
{/* Right: named for assistive technology, tooltip adds detail */}
<TooltipTrigger render={<Button size="icon-md" aria-label="Export"><DownloadIcon /></Button>} />Do not put a tooltip on plain text that is not focusable, do not put interactive content inside one — a link in a tooltip cannot be reached by keyboard before the tooltip closes — and do not use one to hold an error message. Errors belong in a Field, where they stay put and are announced.
Placement
side and sideOffset are forwarded to the positioner. top is the default and the right
choice for a toolbar along the bottom of a viewport; right suits a vertical rail, where a
tooltip above the button covers the button above it. The positioner flips the side when there is
no room, so side is a preference rather than a promise.
On a disabled control
A disabled <button> fires no pointer events, so a tooltip whose trigger is that button never
opens — the left-hand example above is the bug, live. This surprises people regularly, and it is
browser behaviour rather than anything Base UI could fix.
The fix is to make the trigger a focusable wrapper around the disabled button, with
pointer-events-none on the button so the hover lands on the wrapper instead:
<Tooltip>
<TooltipTrigger render={<span tabIndex={0} className="inline-flex rounded-md" />}>
<Button disabled className="pointer-events-none">Amend report</Button>
</TooltipTrigger>
<TooltipContent>Signed reports can only be amended by the reporting radiologist.</TooltipContent>
</Tooltip>The tabIndex={0} is the part worth keeping. A span is not focusable, and without it the
explanation for a disabled control is reachable by pointer only — which is exactly the group of
readers least likely to be able to guess why the button is dead. If the control is disabled for
a reason the reader could act on, consider saying it on the page instead.
In a toolbar
Icon buttons are where tooltips earn their place, and a toolbar is where the delay is felt. Without grouping, each tooltip waits its own 200ms and moving along the row is a series of small pauses. Wrap the row in a provider and the first hint costs the delay while its neighbours open instantly:
<TooltipProvider delay={200} closeDelay={80}>
<Toolbar>{/* …a Tooltip per button… */}</Toolbar>
</TooltipProvider>Tooltip only supplies a provider of its own when there is not one already, so an outer provider
wins and a lone tooltip still works with nothing around it. Wrapping unconditionally is the
obvious version and it is wrong — the inner provider shadows the outer one, and grouping silently
never happens.
Revealing truncated text
| Accession | Study description |
|---|---|
| ACC-4471902 | |
| ACC-4471915 |
A study description clipped to a column width is the one case where a tooltip may carry the whole
content rather than a hint, because nothing is being hidden — the text is on the page, just cut
off. Make the truncated cell a button so it is focusable, otherwise the full text exists for
pointer users only.
Styling
The popup is --background-neutral-default in both themes. A tooltip sits above the interface,
and matching the page makes it read as part of it. It is capped at 260px wide, which is a hint's
worth of text: if yours does not fit, it is not a tooltip.
Keyboard and touch
Focusing the trigger opens the tooltip with no delay, and Escape closes it while focus stays where it is. There is no touch equivalent of hover, so a tooltip is simply absent on a phone — plan the interface so that nothing is lost when it is.
Accessibility
The popup carries role="tooltip" and an id, and the trigger points at it with
aria-describedby, so the hint is announced after the control's name.
Base UI 1.7 wires none of that. Its popup has no role and no id, and its trigger no
aria-describedby — the tooltip is drawn on screen and is absent from the accessibility tree
entirely. Our Tooltip adds the two attributes. If you build a tooltip out of the primitive
directly, add them yourself, and check by reading the DOM rather than by trusting the component.
aria-describedby is present whether or not the tooltip is open. While it is closed the popup is
unmounted and the reference resolves to nothing, which assistive technology ignores.
API Reference
Everything Base UI's Tooltip accepts.
TooltipContent takes the popup's props plus two of the positioner's, which it forwards:
Prop
Type
On TooltipProvider:
Prop
Type
On Tooltip:
Prop
Type