Skip to content
RU
Scale
Density
Theme
Accent

Overlays

Tooltip

A short label appearing on hover and on focus. The one overlay of the library made without the Popover API: by ordinary positioning, so that it works the same everywhere.

The component
Refresh One full sweep of the map by an agent
Markup
<span class="inst-tooltip">
  <button class="inst-btn inst-btn--icon inst-btn--ghost" type="button" aria-label="Refresh" aria-describedby="tt-refresh">
    <svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
  </button>
  <span class="inst-tooltip-text" role="tooltip" id="tt-refresh">Refresh</span>
</span>
<span class="inst-tooltip">
  <button class="inst-btn inst-btn--ghost" type="button" aria-describedby="tt-pass">What is a pass?</button>
  <span class="inst-tooltip-text" role="tooltip" id="tt-pass">One full sweep of the map by an agent</span>
</span>

The label is centred on the trigger and does not flip: ordinary positioning has no spare places. At the edge of a narrow region the nearest overflow: hidden clips it — there a popover is wanted.

Contract

What Required Why
A role="tooltip" on the label yes Otherwise it is just hidden text
An id on the label yes The trigger refers to it
An aria-describedby on the trigger yes It points at the id of the label and is the only thing tying it to the control. Without it a tooltip does not exist for a screen reader
The trigger is focusable yes The showing goes through :focus-within. A non-focusable element wants a tabindex="0"

Accessibility

The keyboard A tooltip appears on :focus-within, that is, on tabbing to the trigger. Without that it would be available to the mouse alone
A touchscreen There is no hover there, and the focus arrives together with the press. Do not put into a tooltip anything that cannot be done without
It does not replace a name An aria-describedby is a description. The accessible name of an icon button is given by an aria-label rather than by a tooltip
Overlapping A tooltip stands --space-3 above the trigger: it does not cover what it explains
Reduced motion The transition of opacity collapses along with all the others
Print It is not printed on paper

Variants

Example
19:38:04 Opening a modal is the one line of script in the whole overlays section; closing, the ground and blocking the scroll do without it.
Markup
<span class="inst-tooltip">
  <button class="inst-btn inst-btn--sm" type="button" aria-describedby="tt-short">Short</button>
  <span class="inst-tooltip-text" role="tooltip" id="tt-short">19:38:04</span>
</span>
<span class="inst-tooltip" style="margin-inline-start:9rem">
  <button class="inst-btn inst-btn--sm" type="button" aria-describedby="tt-wide">In detail</button>
  <span class="inst-tooltip-text inst-tooltip-text--wide" role="tooltip" id="tt-wide">Opening a modal is the one line of script in the whole overlays section; closing, the ground and blocking the scroll do without it.</span>
</span>

A base tooltip is one line (white-space: nowrap). That is right for a label of two words and breaks on a sentence: the line stretches past the edge of the screen. So a long text wants an explicit inst-tooltip-text--wide — wrapping and a width ceiling of 22rem.

The plate is centred over its trigger, and on the last control in a row that runs off the edge: a nowrap plate is several times wider than the glyph it annotates, and the half that is clipped is the half being read. inst-tooltip-text--end puts its trailing edge over the trigger's — the right edge in LTR, the left in RTL, one rule for both. The caller knows their control is last; the kit does not.

Behavior

The event The selector
hover by mouse .inst-tooltip:hover
focus inside the wrapper .inst-tooltip:focus-within

The label is not removed from the flow but dimmed by opacity, and the mouse is removed from it (pointer-events: none): a cursor moving towards the tooltip is not intercepted by it and does not drop the hover.

A tooltip is clipped inside a container with overflow: hidden. It is positioned position: absolute against its own wrapper and does not live in the top layer. That is the known price of a tooltip that works the same everywhere and does not depend on support for anchor positioning.

Where clipping is possible — inside a panel, a cell of a table, an accordion — take a popover: it is in the top layer, and nothing will cut it off.

API

Classes
NameWhat it does
class
inst-tooltipThe wrapper of the trigger. position: relative, the position is counted from it
inst-tooltip-textThe label itself. Above the trigger, centred
modifier
inst-tooltip-text--endThe plate's trailing edge over the trigger's, instead of centred. For the last control in a row, where a centred plate runs past the container
inst-tooltip-text--wideLine wrapping and a width ceiling of 22rem
Tokens
NameValueWhere from
token
--z-popover100one across every cell
--surface-overlaylight-dark(var(--n-0), var(--n-11))base +1: theme
--shadow-popover0 4px 12px -2px var(--shadow-color-far), 0 2px 4px -2px var(--shadow-color-near)one across every cell
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))one across every cell
--hairline1pxbase +1: screen
--radius-sm6pxbase +4: scale
--space-24pxone across every cell
--space-36pxone across every cell
--text-xs0.78125rembase +4: scale
--leading-ui1.4one across every cell
--dur-180msone across every cell
--ease-outcubic-bezier(0.22, 0.61, 0.36, 1)one across every cell

Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument