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.
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 |
| It is not printed on paper |
Variants
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
| Name | What it does |
|---|---|
| class | |
inst-tooltip | The wrapper of the trigger. position: relative, the position is counted from it |
inst-tooltip-text | The label itself. Above the trigger, centred |
| modifier | |
inst-tooltip-text--end | The 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--wide | Line wrapping and a width ceiling of 22rem |
| Name | Value | Where from |
|---|---|---|
| token | ||
--z-popover | 100 | one across every cell |
--surface-overlay | light-dark(var(--n-0), var(--n-11)) | base +1: theme |
--shadow-popover | 0 4px 12px -2px var(--shadow-color-far), 0 2px 4px -2px var(--shadow-color-near) | one across every cell |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | one across every cell |
--hairline | 1px | base +1: screen |
--radius-sm | 6px | base +4: scale |
--space-2 | 4px | one across every cell |
--space-3 | 6px | one across every cell |
--text-xs | 0.78125rem | base +4: scale |
--leading-ui | 1.4 | one across every cell |
--dur-1 | 80ms | one across every cell |
--ease-out | cubic-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