Tooltip

.tooltip[popover="hint"][data-ui="tooltip"]

Tooltip — a short text hint shown on hover, focus or long-press.

How this differs from what it looks like

Not popover

A tooltip is a label or a short description of the thing it is attached to, it appears on hover and focus without being asked for, it holds no interactive content, and it is announced as part of its trigger through aria-describedby. A popover is a panel you opened on purpose: it can hold a form, links, a menu; it takes focus; it is dismissed with Escape or a click outside. The rule for choosing: if the content contains anything the user can operate, or if it would still be worth reading after the pointer moves away, it is a popover. If it is a sentence explaining the control under the cursor, it is a tooltip. Putting a button inside a tooltip makes it unreachable — the tooltip closes as soon as the pointer leaves the trigger, and no keyboard user can get into it at all.

Not hover-card

A hover card is the middle case, and it is a rich preview rather than a hint: a user avatar that reveals a profile card, a link that reveals the page it points at. It opens on hover like a tooltip, may contain links and images unlike a tooltip, and stays open while the pointer travels into it. Reach for it when the content is worth looking at; reach for tooltip when it is worth reading once.

Examples

A hint on an icon-only button

View “A hint on an icon-only button” as
Saves without leaving the page
Opens under its trigger
<button class="btn btn-outline btn-icon" id="demo-tip-save"
        aria-label="Save" interestfor="demo-tip-save-hint">
  <svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg>
</button>

<div class="tooltip" id="demo-tip-save-hint" popover="hint" data-ui="tooltip">
  Saves without leaving the page
</div>

<button class="btn btn-outline" id="demo-tip-below"
        interestfor="demo-tip-below-hint">Below instead</button>
<div class="tooltip tooltip-block-end" id="demo-tip-below-hint" popover="hint" data-ui="tooltip">
  Opens under its trigger
</div>

Hover it, then Tab to it, then press Escape. data-ui="tooltip" is not an enhancement on this component — it is what makes it work: measured 2026-08-08, interestfor is Chromium-only, so nothing opens the tooltip in Firefox or WebKit without it, and in WebKit popover="hint" degrades to popover="manual", which has neither Escape nor light dismiss.

The module fills exactly the gap each engine has and attaches nothing at all on Chromium. Note there is no aria-describedby in this markup: where interestfor is native the browser makes the association itself, and adding it by hand would double the announcement.

A term in running text

View “A term in running text” as

The build asserts the is present in every emitted file.

@layer ui.reset, ui.tokens, ui.base, ui.components, ui.blocks, ui.utilities;
<p>
  The build asserts the
  <button class="tooltip-term" type="button" interestfor="demo-tip-layer">layer statement</button>
  is present in every emitted file.
</p>

<div class="tooltip" id="demo-tip-layer" popover="hint" data-ui="tooltip">
  <code>@layer ui.reset, ui.tokens, ui.base, ui.components, ui.blocks, ui.utilities;</code>
</div>

The trigger is a <button>, not a <span>: interestfor is defined on <button> and <a> only, and a hint a keyboard user cannot reach is not a hint.

The dotted underline is the affordance — a tooltip nobody can see is a tooltip nobody reads. Retune the delays with --tooltip-delay-in and --tooltip-delay-out on any ancestor; they drive Chromium’s native interest-delay-* and the module’s timers from one declaration.

Tokens 13

Level 2, declared on .tooltip itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by tooltip
TokenDefault
--tooltip-durationvar(--duration-instant)
--tooltip-offsetvar(--space-2)
--tooltip-areablock-start span-all
--tooltip-tryflip-block, flip-inline, flip-block flip-inline
--tooltip-bgvar(--color-surface-inverse)
--tooltip-colorvar(--color-on-surface-inverse)
--tooltip-font-familyvar(--font-secondary)
--tooltip-font-sizevar(--font-size-xs)
--tooltip-max-inline-size18rem
--tooltip-padding-blockvar(--space-1)
--tooltip-padding-inlinevar(--space-2)
--tooltip-radiusvar(--radius-field)
--tooltip-shadowvar(--elevation-2)

Variants and states

Variants

  • .tooltip-block-end
  • .tooltip-inline-end
  • .tooltip-inline-start

Inside it

  • .tooltip-term

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :focus
  • :popover-open
  • data-legacy

Before you ship it

This one needs a script. Opt the markup in with data-ui="tooltip" and import import 'mostlycss/js/components/tooltip' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.

What you have to do 4 requirements

  1. Ship data-ui="tooltip". It is the component, not an enhancement: without it the tooltip never opens in Firefox or WebKit, and in WebKit it cannot be dismissed once it has, which fails WCAG 1.4.13 outright.
  2. Trigger it from a <button> or an <a>, and nothing else. A tooltip on a <span> is a tooltip a keyboard user cannot reach.
  3. Write neither aria-describedby nor role="tooltip". Both are supplied for you on whichever path runs, and writing them by hand doubles them.
  4. For a control that is unavailable, use aria-disabled="true" and leave disabled off, handling the click yourself. A disabled button fires no pointer events in any engine — which is exactly the button whose tooltip would explain why it is disabled.

src/css/components/tooltip.css · npx mostlycss add tooltip