← Overview

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.

Twelve characters, printed on the sheet you saved when the account was created.

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-visible on 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: hidden clips 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 by showPopover(), and no CSS-only component can call it. Where a hint has to escape its container, use popover.