Skip to content

Tooltip

TooltipShows while the pointer rests on the button, and at once when the keyboard focuses it.
Thymeleaf source
<sl:tooltip>
<sl:button variant="outline">Hover</sl:button>
<sl:tooltip-content>Add to library</sl:tooltip-content>
</sl:tooltip>
Rendered HTML
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline">Hover</button>
<span class="tooltip-content" role="tooltip" popover="manual">
Add to library
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>

<sl:tooltip> wraps the element it describes, the trigger, and a <sl:tooltip-content> after it. The trigger is the first element inside: a <sl:button>, a link, anything that takes focus.

<sl:tooltip>
<sl:button variant="outline">Hover</sl:button>
<sl:tooltip-content>Add to library</sl:tooltip-content>
</sl:tooltip>

The tooltip shows when the pointer rests on the trigger for a moment (delay, 300 ms by default), and at once when the keyboard focuses it or when another tooltip hid a moment ago. It stays while the pointer moves onto it, so it can be read when zoomed in, and hides when the pointer leaves both, when the trigger loses focus or is pressed, and on Escape. Escape then does nothing else, so a dialog or menu around the trigger stays open. Showing a tooltip never closes an open menu or popover either, so an item in a dropdown menu can have one. One tooltip shows at a time, and touch screens never show one.

<sl:tooltip> itself has no box, so the trigger keeps its place in the layout, in a toolbar or in running text.

The tooltip describes its trigger (aria-describedby, added to any description the trigger already has), so a screen reader reads it after the trigger’s name. It does not replace the name: an icon-only button keeps its aria-label.

With a keyboard shortcutAn icon button keeps its aria-label as its name; the tooltip describes it, shortcut included.
Thymeleaf source
<sl:tooltip>
<sl:button variant="outline" size="icon" aria-label="Save"><sl:icon name="save"/></sl:button>
<sl:tooltip-content>
Save changes <sl:kbd-group><sl:kbd>Ctrl</sl:kbd><sl:kbd>S</sl:kbd></sl:kbd-group>
</sl:tooltip-content>
</sl:tooltip>
Rendered HTML
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline" data-size="icon" aria-label="Save">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"></path>
<path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"></path>
<path d="M7 3v4a1 1 0 0 0 1 1h7"></path>
</svg>
</button>
<span class="tooltip-content" role="tooltip" popover="manual">
Save changes
<kbd class="kbd-group">
<kbd class="kbd">Ctrl</kbd>
<kbd class="kbd">S</kbd>
</kbd>
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>

A tooltip that only shows an icon button’s name (the same words as its aria-label) would make a screen reader say them twice: “Edit, button, Edit”. mode="label" says the tooltip repeats the name: it is hidden from assistive technology (aria-hidden on the content) and describes nothing. It still shows on hover and keyboard focus.

<sl:tooltip mode="label">
<sl:button variant="outline" size="icon" aria-label="Edit"><sl:icon name="pencil"/></sl:button>
<sl:tooltip-content>Edit</sl:tooltip-content>
</sl:tooltip>
Repeating the namemode="label" is for a tooltip that says what the trigger's name already says, such as an icon button's aria-label: it is hidden from assistive technology and describes nothing, so a screen reader does not hear the name twice.
Thymeleaf source
<sl:tooltip mode="label">
<sl:button variant="outline" size="icon" aria-label="Edit"><sl:icon name="pencil"/></sl:button>
<sl:tooltip-content>Edit</sl:tooltip-content>
</sl:tooltip>
<sl:tooltip mode="label">
<sl:button variant="outline" size="icon" aria-label="Delete"><sl:icon name="trash"/></sl:button>
<sl:tooltip-content>Delete</sl:tooltip-content>
</sl:tooltip>
Rendered HTML
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline" data-size="icon" aria-label="Edit">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"></path>
<path d="m15 5 4 4"></path>
</svg>
</button>
<span class="tooltip-content" role="tooltip" popover="manual" aria-hidden="true">
Edit
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline" data-size="icon" aria-label="Delete">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M10 11v6"></path>
<path d="M14 11v6"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path>
<path d="M3 6h18"></path>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
<span class="tooltip-content" role="tooltip" popover="manual" aria-hidden="true">
Delete
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>

The sidebar’s menu buttons use this for their labels while it is collapsed to icons.

Keep the text short and plain. A tooltip cannot hold links or buttons (it hides when the pointer or focus leaves), and nobody on a touch screen sees it, so never put information in it that is not available another way. For richer content that the user can interact with, use a popover.

A disabled button fires no pointer events and cannot take focus, so its tooltip never shows. Wrap it in an element that takes focus (<span tabindex="0">) when the tooltip explains why it is disabled, or better, say why next to it.

The tooltip shows above its trigger, centred on it. side="top|right|bottom|left" and align="start|center|end" on <sl:tooltip-content> change that, and it flips to the other side when there is no room, its arrow with it. right and left are the end and the start of a line.

Sidesside puts it on a side of its trigger; it flips when there is no room there.
Thymeleaf source
<sl:tooltip>
<sl:button variant="outline">Top</sl:button>
<sl:tooltip-content>Shown above</sl:tooltip-content>
</sl:tooltip>
<sl:tooltip>
<sl:button variant="outline">Right</sl:button>
<sl:tooltip-content side="right">Shown after</sl:tooltip-content>
</sl:tooltip>
<sl:tooltip>
<sl:button variant="outline">Bottom</sl:button>
<sl:tooltip-content side="bottom">Shown below</sl:tooltip-content>
</sl:tooltip>
<sl:tooltip>
<sl:button variant="outline">Left</sl:button>
<sl:tooltip-content side="left">Shown before</sl:tooltip-content>
</sl:tooltip>
Rendered HTML
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline">Top</button>
<span class="tooltip-content" role="tooltip" popover="manual">
Shown above
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline">Right</button>
<span class="tooltip-content" role="tooltip" popover="manual" data-side="right">
Shown after
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline">Bottom</button>
<span class="tooltip-content" role="tooltip" popover="manual" data-side="bottom">
Shown below
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>
<span class="tooltip" x-data="slTooltip">
<button class="button" type="button" data-variant="outline">Left</button>
<span class="tooltip-content" role="tooltip" popover="manual" data-side="left">
Shown before
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>

It is placed with CSS anchor positioning, which every current browser supports (Chrome and Edge 133, Safari 26, Firefox 147). In older ones the tooltip’s small Alpine component places it from the trigger’s position instead.

AttributeValuesDefaultDescription
modedescription labeldescriptionWhat the text is to a screen reader: description adds it to the trigger as a description; label is for a text that repeats the trigger's name, such as an icon button's aria-label: it is hidden from assistive technology and describes nothing, so the name is not said twice.
delaynumber300How long the pointer rests on the trigger before the tooltip shows, in milliseconds. Keyboard focus shows it at once, and so does moving to another trigger right after a tooltip hid.

Every prop can also be set from an expression: th:mode="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
sidetop right bottom lefttopThe side of the trigger it shows on. right and left are the end and the start of a line.
alignstart center endcenterHow it lines up with the trigger along that side.

Every prop can also be set from an expression: th:side="${...}". Any other attribute passes through to the rendered element, and class is appended.

The tooltip is shown and hidden by a small Alpine component, slTooltip: <sl:tooltip> renders as <span class="tooltip" x-data="slTooltip">.

  • It needs Alpine. Browsers cannot show a popup on hover by themselves yet, so without Alpine the trigger still works but no tooltip shows. Alpine is included by default; see Alpine.
  • It needs no Alpine plugin. If you load Alpine yourself (shadleaf.assets.alpine=external), plain Alpine is enough.
  • Keep your own x-data off <sl:tooltip>. An element has one x-data, and attributes on <sl:tooltip> end up on that span, so yours would replace slTooltip and the tooltip would stop working. Put it on the trigger, or on an element around <sl:tooltip>.