tooltip
A short hint about a control, on hover and on keyboard focus — shown by CSS alone, described and dismissible.
The stylesheet does the showing. Every hint below
appears on :hover and on keyboard focus without any
JavaScript — hover a control, or Tab through the page and
watch them appear on focus. What the module adds is the
aria-describedby that lets a screen reader read the hint
out, Esc to take it away, and the measurement that keeps it
inside the window.
Default (above the trigger)
Hover the button, then press Esc without moving the pointer: the hint goes and stays gone until you ask for it again. That is WCAG 1.4.13's “dismissible”, and it is the one thing here the CSS cannot do on its own.
Keeps the editor open.
Placement (data-tooltip-position)
Above the trigger — the default. Centred to the right of the trigger. Below the trigger. Centred to the left of the trigger.
An icon-only control
The button already has an accessible name via
aria-label — the hint is its
description, and the two are different jobs. A tooltip
never replaces the name: a control whose only label is a hint
that appears on hover has no label at all for anyone who does not
hover.
Invoices are issued on the first of each month. A link works too Jumps to the explainer at the bottom of this page.
On a form field
The wrapper is inline-flex by default; here it is
switched with --tooltip-display: block so it does not
shrink-wrap the field. That makes the wrapper wider than the
control, and the hint still sits under the field rather
than under the wrapper — the module measures the trigger, not the
box it is positioned in. The hint also joins whatever the field is
already described by: aria-describedby is extended,
never overwritten.
At the edge of the window
Narrow the window until the panel touches both
sides, then hover the outer two buttons: the hint slides along
the trigger instead of leaving the screen, and the arrow stays
pointing at the control. Both are the module's measurement,
written as --tooltip-shift and
--tooltip-arrow-shift and read by the stylesheet.
A hint long enough to run past the edge of a narrow window. A hint long enough to run past the edge of a narrow window.
A long hint beside its control
Narrow the window and scroll until this button
sits near the lower edge. A hint placed left or
right can only be slid up and down — the shift is on
the other axis — so the only thing that keeps it out of the
window's margin is the width bound the module measures and writes
as --tooltip-available. It wraps into more lines
instead of reaching out; and because wrapping makes it taller
than it was when it was measured, the module reads it a second
time and lifts the taller box back inside the window, arrow still
on the button.
Sends the draft to everyone on the review list and keeps a copy in the outbox until each of them has confirmed receipt.
Out of room above — the flip
This one asks for top. Scroll until
the button is near the upper edge of the window: with no room
left above it, the module writes
data-tooltip-flipped and the stylesheet renders the
same hint below instead — arrow and all. Scroll back and it
returns.
No room above, so this went below.
What a tooltip owes the reader
The platform already has a tooltip — the title
attribute — and it is the reason this component exists. It never
appears on a touch device, appears only inconsistently on keyboard
focus, cannot be styled, cannot be reached with the pointer, and
arrives after a delay nobody can change. A hint worth writing is
worth showing to everyone, so it goes into the markup:
<span class="c-tooltip" data-js-behavior="tooltip">
<button type="button" data-tooltip-trigger>Save draft</button>
<span class="c-tooltip__bubble" role="tooltip" data-tooltip-bubble>
Keeps the editor open.
</span>
</span>
WCAG 1.4.13, in three words
- Hoverable: the pointer has to be able to travel to the hint. The gap to the trigger is a strip of “nothing” that would take the hint away mid-transit, so the bubble extends invisibly across it.
-
Dismissible: a hint covering what the reader was
looking at has to go without them moving the pointer or the focus
first — Esc. That key is deliberately not swallowed: a
tooltip is a passive hint, not a layer that owns it, so a dialog
underneath still closes. It also has to reach a hint the module
never saw appear: click a control with the mouse and then touch
the keyboard, and the engine turns
:focus-visibleon without reporting anything — the stylesheet shows the hint, and the keystroke that was meant to dismiss it is the very thing that brought it up. So the module reads the two show conditions back off the DOM instead of tracking them, and listens for the key for as long as it lives rather than only while it thinks a hint is showing. - Persistent: nothing takes the hint away on a timer.
Name, or description?
A tooltip is a description. The control still needs its own
name — visible text, or an aria-label on an
icon-only button. That is why the module wires
aria-describedby and never
aria-labelledby, and why it adds its id to whatever the
control was already described by instead of assigning over it.
The hint is hidden with visibility, not
display, and that is an accessibility decision as much
as a layout one: a node aria-describedby points at
directly contributes its text whether it is visible or not,
so the description is read out on focus even before the bubble has
faded in.
tooltip, or popover?
Both anchor a small surface to a control. The split is what the surface is for — and the practical consequence is the last row:
- Opened by hover and keyboard focus, versus a click.
- Holds a sentence of description, versus anything at all, controls included.
- Focus never reaches a tooltip; a popover can be tabbed into.
-
Renders in the normal flow — so an ancestor with
overflow: hiddenclips it — versus the top layer, which nothing clips. That is the price of a hint that needs no script to appear at all: the top layer can only be entered byshowPopover(), and no CSS-only component can call it. Where a hint has to escape its container, use popover.