tooltip()

tooltip.mjs

ExportSignatureDescription
tooltip tooltip(element, text?, { placement? }) Attaches a tooltip to an element. Shown on hover, on focus, and on a held press (§8).
tooltip_all tooltip_all(root?) Attaches tooltips to all [title] elements within root. Clean up before attaching tooltips again after a render (§10).
tooltip_timing { show_ms, hide_ms, press_ms } Configurable delays for hover/focus, hiding, and a long press. Tests can set a delay to zero to avoid waiting.

placement: "top" (default) | "bottom" | "left" | "right" · Appears after a 200 ms delay on hover, or after a 400 ms press from a pointer that has no hover (§8) · Hidden on Escape, by the next press elsewhere, or 150 ms after the pointer leaves both the trigger and the chip — the chip itself can be hovered and read (§9) · text is read from the title attribute if omitted; the helper saves this value and restores it during cleanup (§10) · One line up to 30 ch, wrapping past it (§7) — longer text wraps onto additional lines · Returns a cleanup function

1. Icon buttons

Most common case: icon buttons without visible text. Tooltip shown on hover and keyboard focus.

Show code

2. Placement

Tooltip can be placed on different sides with the placement option. It is a preference rather than a guarantee: a chip whose preferred side would put it off the viewport flips to the trigger's other side instead of hanging off the page.

Near a corner the chip also slides along the trigger, because flipping alone is not enough there. A chip is centred on its trigger across the axis it is not placed on, so a wide chip on a trigger close to a side of the window runs off that side whichever way it flips. The chip's fallbacks therefore offer, in order: the preferred side with one edge pinned to the trigger's matching edge, then the opposite side, then the opposite side pinned the same way. The browser uses the first position that fits, minimizing movement.

Define fallback placement areas explicitly. Using only flip-block, flip-inline, flip-block flip-inline provides fewer fallback positions than it appears to. A one-keyword position-area spans the whole opposite axis, and flipping that span has no effect. These three entries therefore produce only one alternative: the opposite side. A tooltip without enough room on both axes can still overflow, and browsers handle this differently: one keeps the flipped option and slides the box into view, another returns it to the preferred side. On a trigger a few pixels from the top of the window, the second behavior can place the tooltip above the visible area.

Show code

3. Disabled button

Use aria-disabled="true" instead of disabled so the button remains tabbable and screen readers can reach the tooltip via aria-describedby. aria-disabled announces only that the control is unavailable, so the tooltip should explain why. The explanation is available to assistive technology and to sighted keyboard users because it appears on focus.

When the explanation changes with the control's state:

Show code

4. Explicit text

Text can be provided directly instead of being read from title.

Show code

5. Cleanup

The cleanup function removes the tooltip and restores the original values — the ARIA attributes, and the trigger's title (§10). Cleanup followed by a fresh tooltip() is also how a tooltip's text changes — see §3.

Show code

6. Keyboard focus

Tab between the buttons — the tooltip is shown automatically on focus. This is the main reason to use tooltip() instead of the title attribute.

Escape dismisses the tooltip wherever focus is — on the control, in a field on the other side of the page, or nowhere at all after a held press (§8). WCAG 2.2 SC 1.4.13 requires users to be able to dismiss the tooltip without moving the pointer or focus. A document-level key listener dismisses the visible tooltip. The event continues to propagate: a dialog, menu or popover behind the tip still closes on that same Escape.

Show code

7. Long text

The chip stays on one line up to 30 ch and wraps past it, so longer hints wrap within the tooltip background. A single word with no break opportunity is broken at the same cap.

Longer hints need more vertical space. Keep hints concise: two keyboard shortcuts can reasonably take two lines, while longer explanations belong in the panel the control opens.

Show code

8. Touch and other pointers without hover

An iPad is a supported device (Browser support) and does not normally provide hover or keyboard focus. Hold the trigger for 400 ms to show the tooltip — the gesture Material specifies for touch, and the one the platform's own interest invokers (interestfor, not Baseline yet) use. The helper implements this established behavior. Try it on a touch device or with device emulation open; nothing changes for a mouse.

Show code

9. Hovering the tooltip

The tooltip stays open when the pointer moves from its trigger onto it. WCAG 2.2 SC 1.4.13 Content on Hover or Focus requires this behavior. It lets users of screen magnification move the magnified area onto the tooltip and read text that was initially outside their view.

Show code

10. Restoring the title attribute

When a tooltip uses the title text, the helper saves that text and empties the attribute to prevent the browser's native tooltip from appearing as well. Cleanup restores the saved text if the attribute is still empty. This lets repeated cleanup-and-attach cycles preserve the original tooltip.

Cleanup handles three possible attribute states. Attaching the tooltip sets the attribute to an empty string:

At cleanup, title is…What it meansWhat cleanup does
""unchanged since attachmentrestores the saved text
absentthe calling code removed itkeeps it absent; no tooltip will be attached
non-emptythe calling code changed the textkeeps the new text for the next attachment

To update an explanation when the state changes, write the new explanation to title, or call removeAttribute('title') when no explanation is needed, then clean up and reattach. Both changes take precedence over the saved text. Updating title without reattaching leaves the custom tooltip unchanged and may also show a native tooltip with different text.

Show code

11. Sharing an anchor

A tooltip places its chip against its trigger through the trigger's inline anchor-name. The trigger can anchor another surface at the same time: a button whose tooltip explains its state can also open a <ui-menu> placed against it. anchor-name takes a comma-separated list, so each surface adds its own name to the list and removes only that name. The tooltip, <ui-menu>, <ui-combobox> and <x-select> all follow this rule. Code that places its own surface against an element should use add_anchor_name(el, name) and remove_anchor_name(el, name) from /ui/lib/anchor-name.mjs.

Do not assign el.style.anchorName directly on an element that another surface may also anchor to. The assignment replaces the whole list and removes the other surface's name. The browser then finds no element holding that name and places the surface in the top-left corner of the viewport.