Tooltip
The <x-wirekit:: component shows contextual information on hover, focus, and long-press (touch). Positioned via Floating UI with automatic flip/shift.
Tooltips should never be the only source of critical information. For essential details, use visible labels or hint text. On touch devices, tooltips are triggered by long-press (500ms).
Usage
Text Tooltip
Rich Content Tooltip
Inside a Paragraph
A tooltip roots in a <div>, and a <div> is not allowed inside a <p>. Pass as="span" and
both the wrapper and the trigger root in a <span> instead, so the tooltip stays part of the
sentence it belongs to.
Your plan renews monthly.
Width & Layout
Plain-text tooltips (text prop) auto-size to their content with a max-width for readability. Rich tooltips (slot content) size to their content — add explicit widths if needed:
<x-wirekit::tooltip>
<x-slot:content>
<div class="w-64">Wider tooltip with more detailed content.</div>
</x-slot:content>
<x-wirekit::button intent="neutral" surface="ghost" size="sm">Details</x-wirekit::button>
</x-wirekit::tooltip>
Placement
Control where the tooltip appears relative to its trigger:
Available placements: top, top-start, top-end, bottom, bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end.
Surface
A tooltip is inverted by default: its background takes the text color and its text takes the page color, so it stands out on a light theme and on a dark one alike. surface="elevated" gives it the surface menus and popovers use instead, the elevated background with the body text and the theme's border.
The elevated surface draws its edge from --border-wk-width and --color-wk-border, the way a popover does. On a theme that removes borders and shadows, such as Minimal, it sets itself apart from the page by its background alone, as the theme's menus do.
Styling Variants
The tooltip's surface, text color, border radius, and opacity all derive from CSS variables — override them per-tooltip via inline style (scopes to the trigger wrapper) or globally via theme :root. Hover each button below to see a different style applied without changing the component itself.
Per-tooltip overrides win because the inline style cascades onto the hover-trigger wrapper, which the tooltip panel reads via var(--color-wk-tooltip-*). For an app-wide tooltip palette change, set the same tokens on :root {} in app.css instead of per-instance.
Custom Delays
Control how quickly tooltips appear and disappear. In the preview below, the left button shows instantly and hides slowly; the right button waits before showing and hides quickly:
Trigger Behavior
| Device | Show Trigger | Hide Trigger | Auto-Dismiss |
|---|---|---|---|
| Mouse (desktop) | mouseenter + delay |
mouseleave + delay |
No |
| Keyboard | focusin, when the focus is visible (:focus-visible) |
focusout |
No |
| Touch | Long-press (500ms) | Finger release | Yes, after 1500ms |
Theming
Tooltips use inverted colors — dark background on light mode, light background on dark mode. The tokens that do it, and everything else the tooltip reads, are in Design Tokens below.
Behavior
- Floating UI (bundled) positions the tooltip with automatic flip and shift
- Transitions use opacity fade for smooth show/hide
- Livewire SPA navigation closes open tooltips
- Touch devices use long-press (500ms) to trigger, auto-dismiss after 1.5s
- Focus from a script does not open it. A tooltip opens on focus only when the browser draws
that focus (
:focus-visible), which is the focus a keyboard gives. A dialog focusing its first control when it opens, after a click, shows no tooltip; tabbing to the same control does. A mouse still gets the tooltip on hover - Inside a dialog it stands above the dialog. A tooltip whose trigger is in a modal or a drawer is lifted over that dialog's layer, and so is a dropdown or a popover opened from inside one
Switching a Tooltip Off
A tooltip on a control that has become inert should go quiet. disabled does that:
pointer-events: none on the wrapper is not a way to do this, however much it looks like one. mouseenter is delivered to every ancestor of the element the pointer actually hit, whatever those ancestors' own pointer-events value — so the handler still fires and the panel still appears, over a control that is supposed to be dead.
Following live state
The prop answers for the render that produced it. When the answer changes while the page is open — a sidebar collapses, a row enters a loading state — bind the attribute the component reads instead. No extra prop is involved; it is the same switch, read at the moment the tooltip would open:
{{-- 1. `quiet` is your own Alpine state — anything reactive works. --}}
<div x-data="{ quiet: true }">
{{-- 2. The component reads this attribute when a hover or focus would open
the panel, so it follows the value rather than a snapshot of it. --}}
<x-wirekit::tooltip text="Only while the row is idle"
x-bind:data-wk-tooltip-disabled="quiet ? 'true' : 'false'">
<x-wirekit::button intent="neutral" surface="outline">Retry</x-wirekit::button>
</x-wirekit::tooltip>
{{-- 3. Nothing re-renders and nothing re-initializes when this flips. --}}
<x-wirekit::button x-on:click="quiet = false">Allow it</x-wirekit::button>
</div>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
text |
string|null |
null |
Plain-text tooltip content |
placement |
string |
'top' |
Floating UI placement |
offset |
int |
6 |
Distance in px between trigger and tooltip |
delay-show |
int |
300 |
Delay in ms before showing (desktop hover) |
delay-hide |
int |
100 |
Delay in ms before hiding (desktop mouseleave). This is the window the pointer uses to cross the offset gap onto the panel, so a value too small to make that trip gives up the hoverable half of WCAG 1.4.13 — see the warning under Accessibility |
focusableTrigger |
bool|null |
null |
Whether the trigger is a tab stop of its own, so the tooltip shows on focus and not just on hover (WCAG 2.1.1). null decides from the slot: around a button, a link or a field the reader already reaches, the trigger stays out of the tab order and that control's focus shows the tooltip; around an icon, a word or a disabled button, the trigger is the stop. true and false override the choice. The stop is a button that Enter and Space open; a stop forced with true around a control the keyboard already reaches takes no role, since it would nest one control in another. |
describes |
bool |
true |
Link the tooltip to its trigger through aria-describedby. Set false when the tooltip text is already the trigger's accessible name, such as an icon-only button with aria-label set to the same words, so a screen reader does not read it twice. |
disabled |
bool |
false |
Switch the tooltip off without removing it. See below for the live form. |
surface |
'inverted'|'elevated' |
'inverted' |
The panel's surface: the inverted default, or the elevated surface menus and popovers use. See Surface. An invalid value is reported through strict validation and falls back to 'inverted' |
scope |
string|null |
null |
Scoped personalization key |
as |
string |
'div' |
The element the wrapper and the trigger root in. Set as="span" to use a tooltip inside a text-level component — see Pitfalls. |
Slots
| Slot | Purpose |
|---|---|
| default | The trigger element (hovered/focused to show tooltip) |
content |
Rich HTML tooltip content (alternative to text prop) |
Accessibility
- Trigger:
aria-describedby="{tooltipId}" - A trigger the tooltip makes a tab stop of its own, around an icon, a word or a disabled control, is a
role="button":EnterorSpaceshows the tooltip, as a toggletip does. Its words name it; around an icon alone the tooltip's text names it, and the description is left off so a screen reader does not read the text twice. Around a disabled control it carriesaria-disabled="true", so the reader hears that the control is unavailable and then why. Anaria-labeloraria-labelledbyof yours names it first - Tooltip:
role="tooltip", matchingid - Unique ID generated per tooltip instance
- The panel is hoverable: moving the pointer from the trigger onto the tip keeps it open, so a long tip can be read at high zoom or with an unsteady hand. The
delay-hidewindow is what bridges theoffsetgap between the two — settingdelay-hide="0"switches that behavior off, because the panel closes before the pointer arrives. - The panel is dismissible with
Escapefrom anywhere on the page, whether the tooltip was opened by hovering or by focusing. A pendingdelay-showis canceled too, so a tip cannot appear just after the key that dismissed it.
Together those two are WCAG 1.4.13 (Content on Hover or Focus, AA); the third clause, persistent, is met by the tip staying up until the pointer or focus leaves, Escape is pressed, or the developer switches it off.
delay-hide="0" gives up the hoverable half of 1.4.13The panel closes on mouseleave before the pointer can cross the offset gap to reach it, so a tip that takes more than a glance to read cannot be read at all — at 200% zoom, or with an unsteady hand, it is gone by the time the pointer arrives. Avoid 0 whenever the tip carries text rather than a one-word label.
WireKit does not clamp the value, and that is deliberate: the prop stays yours. What goes with it is the AA claim above, so make it a decision rather than a default you inherited from an example.
Keyboard Interaction
| Key | Action |
|---|---|
Tab (focusing the trigger) |
Show the tooltip |
Tab (away from trigger) |
Hide the tooltip |
Enter / Space (on a trigger the tooltip made a stop of its own) |
Show the tooltip, also after Escape hid it |
Escape |
Hide the tooltip, and cancel one that is about to open. Works wherever focus is — a tooltip opened by hovering is dismissed without moving the pointer. A modal or a drawer around it stays open |
Pitfalls
- Don't put interactive content inside a tooltip. Tooltips are descriptive — buttons, links, and form controls inside them are not reachable by keyboard. The panel does take pointer events (that is what makes it hoverable), so a control placed there would be clickable with a mouse and unreachable without one, which is worse than one that plainly does not work. Use
<x-wirekit::orpopover> <x-wirekit::instead.hover-card> - Inside a paragraph, set
as="span". The wrapper and the trigger root in a<div>by default, and a<div>inside a<p>is invalid HTML — the browser closes the paragraph and lifts the tooltip out of it. The trigger then stops being a child of its text and becomes its sibling, which in a grid gives it a cell of its own and lands it next to the wrong row. Nothing reports this: the markup is all present, and the only symptom is where the tooltip sits on screen. So whenever the tooltip sits inside<x-wirekit::or any other element that holds phrasing content, passtext> as="span". - And only when the slot is phrasing content too.
as="span"is invalid in the other direction if the slot carries a block element, so it stays opt-in rather than the default. Wrap an icon, a word or a short label; keepas="div"for anything with a block inside. - Don't use a tooltip as the primary label. Touch users have no hover, and screen readers don't announce tooltip content reliably across browsers. The visible label MUST stand on its own.
Design Tokens
| Token | Used for |
|---|---|
--font-wk-sans |
Tooltip font family |
--text-wk-sm |
Tooltip font size |
--color-wk-tooltip-bg |
Tooltip background — defaults to the text color, which is what inverts it |
--color-wk-tooltip-text |
Tooltip text — defaults to the background color |
--radius-wk-sm |
Border radius |
--shadow-wk-md |
Drop shadow |
--size-wk-tooltip-max |
Maximum width (default 20rem) |
--padding-wk-x-sm / --padding-wk-y-xs |
Padding |
--z-wk-tooltip |
Stacking context (default 60) |
Customization
All four timing and placement props read a project-wide default, so a house tooltip style is set once rather than on every call site:
// config/wirekit.php
'components' => [
'tooltip' => [
'placement' => 'top',
'offset' => 6,
'delay-show' => 300,
'delay-hide' => 100,
],
],
The two delays are in milliseconds and the offset in pixels. Note the kebab-case keys —
delay-show in the file, delay-show on the tag, $delayShow inside the component.
delay-hide to zeroThe hide delay is what makes the tooltip hoverable: it is the window in which the pointer can travel from the trigger onto the panel without the panel disappearing. WCAG 2.1 SC 1.4.13 (Content on Hover or Focus) requires exactly that, and setting it here switches it off for every tooltip in the application at once — which is why the value belongs in review rather than in a quick config edit. See the Pitfalls above.
Usage & Conventions
Need a click-triggered hint? A tooltip is hover/focus only by design (the WAI-ARIA tooltip pattern). For a "click the ? icon to reveal a note" affordance — or any content with buttons, links, or form fields — use Popover (click-toggled, full focus management). For rich content that previews on hover, use Hover Card.
Text cut off with an ellipsis? <x-wirekit:: shows the whole text in this tooltip while, and only while, the text is cut. See Truncation With a Tooltip.
Further Reading
- WAI-ARIA Tooltip Pattern — the authoring pattern this component implements
- MDN:
role="tooltip" - Floating UI — positioning engine (bundled)
- Inclusive Components: Tooltips & Toggletips