Skip to content
Home Theme Gallery

Tooltip

The custom element is <hui-tooltip>.

Overview

A popover that opens on hover after a delay and, always, on keyboard focus. A plain tooltip labels its trigger; the richer form can be moved into and reached by keyboard.

Example

Tooltip
Source
<hui-tooltip>
<hui-button slot="trigger">Hover me</hui-button>
<span>Saves your changes</span>
</hui-tooltip>

The Go template that renders it:

<hui-tooltip>
<hui-button slot="trigger" variant="ghost">{{ .Action.Label }}</hui-button>
<span>{{ .Action.Hint }}</span>
</hui-tooltip>

On a phone

A tooltip opens on hover and on keyboard focus, as Radix’s does, and a tap does not reliably open one: iOS Safari does not focus a button when it is tapped. Put nothing in a tooltip that a reader on a phone needs. A control’s name belongs in aria-label or its text; a longer explanation belongs in a hui-popover, which opens on a tap, as here.

On touch, a popover instead 375 px wide
Source
<hui-popover>
<hui-button slot="trigger" variant="outline">What is a workspace?</hui-button>
<p style="margin:0;font-size:.875rem">A workspace holds your projects and the people who can see them.</p>
</hui-popover>

API

Attributes2
Attributes
NameTypeDefaultDescription
openbooleanfalseReflected open state, inherited from the popover base.
placement"top" | "bottom" | "left" | "right""top"Inherited from the popover base, but forced to top when the tooltip connects.
Properties4
Properties
NameTypeDefaultDescription
openbooleanfalseReflects to the open attribute.
placementOverlayPlacement'top'Reflects to the placement attribute; reset to top on connect.
richbooleanfalseA plain class field, read once in connectedCallback(). It is not a reactive property and has no attribute binding, so set it on the element before it connects.
delaynumber400Milliseconds before hover opens the tooltip. Set from rich on connect, so setting it after connection has no effect.
Methods2
Methods
NameTypeDescription
show(from?)-Opens the tooltip and records from as the element focus returns to. Hover and focus call this with the trigger.
hide()-Closes the tooltip; focus is not moved.
Events3
Events
NameDescription
hui-openThe tooltip entered the top layer. It bubbles and is composed, and carries no detail.
hui-closeThe tooltip left the top layer. It bubbles and is composed, and carries no detail.
pointerenter / focusin on [slot="trigger"]Hover opens it after delay; keyboard focus opens it immediately, cancelling any pending hover timer.
Slots2
Slots
NameDescription
triggerThe element the tooltip describes.
(default)The tooltip content; for a rich tooltip it may contain interactive elements.
CSS custom properties4
CSS custom properties
NameDefaultDescription
--hui-popoveroklch(1 0 0)Panel background.
--hui-popover-foregroundoklch(0.145 0.008 326)Panel text colour.
--hui-borderoklch(0.922 0.005 325.62)Panel border.
--hui-radius-mdcalc(var(--hui-radius, 0.45rem) * 0.8)Panel corner radius.
::part() hooks2
::part() hooks
NameDescription
anchorThe span carrying the CSS anchor-name the panel points at.
panelThe native popover panel, carrying role="tooltip".

States

Rich tooltip

rich is the interactive form: a longer delay, and a grace period that lets the pointer travel into the panel, so the link inside can be reached.

Rich tooltip
Source
<hui-tooltip rich>
<hui-button slot="trigger" variant="outline">Rich</hui-button>
<div style="max-width:16rem">
<strong>Title</strong>
<p>More detail with a <a class="hui-link" href="#more">link</a>.</p>
</div>
</hui-tooltip>

Server-side mechanics

The server owns the trigger and the default-slot content; it is not form-associated and submits nothing. It inherits the popover's reflected open and placement, but the normal path is interaction: it opens on hover after delay and immediately on keyboard focus, and because focus restoration is switched off it never moves focus on close. On open it writes an aria-describedby from the trigger to the content id, generating the id when absent, so a swap that replaces the trigger removes that link with it. An hx-swap can replace the trigger or content while the panel is up; swapping the tooltip element itself closes it through disconnectedCallback(), leaving nothing in the top layer.

Accessibility

  • The panel carries role="tooltip" and the trigger gets an aria-describedby pointing at the content id, generated if absent.
  • It opens on hover after a delay and always on keyboard focus, because a tooltip a keyboard user cannot see is not a tooltip.
  • Escape dismisses it without moving focus; the popover base's focus restoration is switched off for exactly this reason.
  • A rich tooltip has a longer delay and a close grace period so the pointer can travel into its content, which is reachable by keyboard.
  • A tooltip must never be the only route to essential information; that is a documentation rule the host applies.
  • On touch it cannot be relied on to open at all: a tap is neither a hover nor, in iOS Safari, a focus. Content a phone reader needs belongs in the control’s text, its aria-label, or a hui-popover, which opens on a tap. See “On a phone” above.

Keyboard

Keyboard
KeysAction
TabMoving keyboard focus onto the trigger opens the tooltip immediately.
EscapeCloses the tooltip without moving focus, as WCAG 1.4.13 requires.
Tab (rich)The panel follows the anchor in the DOM, so Tab from the trigger reaches interactive content in a rich tooltip without a trap.

Gotchas

  • rich is a plain class field, not a reactive property, so the rich attribute does not set it: <hui-tooltip rich> behaves as a plain tooltip, with a 400 ms hover delay, no travel grace period, and an aria-describedby still written to the trigger. Set it as a property before the element connects, or the attribute is silently ignored.
  • delay is a plain class field too, set from rich in connectedCallback(); assigning it after connection has no effect.
  • placement is forced to top in connectedCallback(), so a host-supplied placement on the element is overwritten when it connects.