Skip to content

Tooltip

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/tooltip.json

The component lands in components/ui/tooltip.tsx and is yours to edit.

See it liveBoth widths and the four sides - in either theme, and in the accent of any product of the line.→

Not size. The line is whether anything inside can be reached.

A tooltip holds a phrase: no links, no buttons, no fields. There is no way to get into it — it is tied to a trigger that is hovered or focused, and it disappears the moment attention moves.

A Popover holds things you interact with, whatever its length. If the content has a button in it, it is a popover even if it is four words long.

And there is a second question, which matters more:

Base UI puts no role="tooltip" on the popup and no aria-describedby on the trigger, deliberately. A tooltip cannot be reached on a touch screen and is not reliably announced by a screen reader, so the library does not pretend otherwise.

So the trigger must carry its own aria-label, saying roughly what the tooltip says. The tooltip serves a sighted mouse or keyboard user; the aria-label is what everyone else gets.

<TooltipTrigger render={<Button variant="icon" aria-label="Archive" />}>
<ArchiveIcon />
</TooltipTrigger>

If the words are load-bearing — if not reading them means not understanding the control — this is the wrong component. Put them inline, or use a Popover with openOnHover on the trigger, which touch and screen reader users can reach.

<TooltipProvider delay={600}>
<Tooltip>
<TooltipTrigger render={<Button variant="icon" aria-label="Archive" />}>
<ArchiveIcon />
</TooltipTrigger>
<TooltipPopup>Move to the archive</TooltipPopup>
</Tooltip>
</TooltipProvider>

TooltipPopup renders its own portal, positioner and arrow.

TooltipProvider is optional and shared: once one tooltip in a group has opened, the next opens instantly instead of waiting out its delay again. A toolbar of icon buttons without it feels broken in a way nobody can name.

It opens on focus, not only on hover. This is the half that gets forgotten, and forgetting it is invisible: someone tabbing through a row of unlabelled icon buttons never hovers anything.

Escape closes it while the trigger still has focus — the way out for someone who can see it covering what they were reading.

delay is not a root prop. It belongs to the trigger, the same as on PreviewCard. Passing it to Tooltip is silently ignored at runtime.

Small text, on purpose. text-xs. It sits over the thing it explains and has to be readable without taking the page over.

--z-popup, above everything else. A tooltip labels whatever is on top of it, including the contents of a popover or a dialog, so it has to be able to sit above them.

No colour of its own. Every class is written in tokens, so the same list is correct in both themes and in every product’s accent — no dark: utilities anywhere in it.

Props

Read from the types in ui/tooltip.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

TooltipProvider

A shared delay for a group of tooltips. Wrap a toolbar in it and the second icon button explains itself instantly rather than making the reader wait again. Optional - a lone tooltip works without one.

Base UI's Tooltip.Provider, unchanged under dowel's name - its props are on its own page.

Tooltip

The root. Takes disabled, and the controlled open/onOpenChange.

Base UI's Tooltip.Root, unchanged under dowel's name - its props are on its own page.

TooltipTrigger

What it labels. Give it render to use your own button - and give that button an aria-label saying what the tooltip says, because the tooltip itself reaches nobody using a screen reader.

Base UI's Tooltip.Trigger, unchanged under dowel's name - its props are on its own page.

TooltipPopup

The label itself. Portalled and positioned, so it is not clipped by an ancestor with overflow: hidden.

PropTypeDefault
sizesm | wide'sm'
sidebottom | top | left | right | inline-end | inline-startPreferred side of the trigger. Base UI flips it when it does not fit, and defaults it to the top.
aligncenter | start | endAlignment along that side. Base UI centres it by default.
sideOffsetnumber | OffsetFunction6Distance from the trigger, in pixels.
arrowbooleantrueWhether to draw the arrow pointing back at the trigger.
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullWhere to portal to. Defaults to the raised host of the overlay this is opened inside (layer.tsx), and to the document body when there is none - either way not the element it was opened from, whose overflow would clip it. Pass an element to put it somewhere else, such as a container being screenshotted.

And every prop of Base UI's Tooltip.Popup, passed on.

TooltipArrow

The notch pointing back at the trigger. Base UI rotates it to whatever side the tooltip landed on, which is why the placement is keyed off data-side rather than off the side that was asked for.

Every prop of Base UI's Tooltip.Arrow, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 12 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/tooltip.tsx - the file a product copies - newest first.

  1. v0.40.0
    • fix hold the tooltip's width to its room 3f1d375
    • fix hold every surface over the page to its room on both axes d0436a7
  2. v0.33.0breaking
    • feat no-raw-button and no-arbitrary-scale ac7a11a
  3. v0.32.0breaking
    • feat a layer for popups inside overlays, a body that scrolls alone 5244cfd
  4. v0.28.0
    • feat measure in steps, and name the step that was missing 8dd8a28
  5. v0.26.1
    • fix shade a popup instead of staining the page behind it 9c7a96b
  6. v0.18.1
    • fix five defects a live run found, and none of the tests did 863226c
  7. v0.8.0
    • feat Dialog, ConfirmDialog, Drawer, Popover, PreviewCard, Tooltip 5dc3c06