Tooltip
Supplementary text on hover or focus. Unusually, this helper returns attributes rather than an element.
When to Use
Variants
Positions
Four positions. There's no collision detection — the position you pass is the position you get, so pick one that won't run off the viewport or under a sticky bar.
<button <?= ws_tooltip('Helpful info', ['position' => 'top']) ?>>Hover me</button>
<button <?= ws_tooltip('Helpful info', ['position' => 'right']) ?>>Hover me</button>Colour
Dark by default. Use light only against a dark surface where the dark tooltip would disappear.
<button <?= ws_tooltip('Light style', ['variant' => 'light']) ?>>Light</button>Without the arrow
Drop the arrow when the tooltip sits far enough from its trigger that the pointer would be misleading.
<button <?= ws_tooltip('No arrow', ['arrow' => false]) ?>>Hover me</button>States
| State | Behavior |
|---|---|
| Hidden | Default. The tooltip exists only as a CSS pseudo-element, so there's nothing in the DOM to inspect. |
| Hover | Shown via :hover on the trigger. No delay — it appears immediately. |
| Focus | Shown via :focus, so keyboard users get the same information as mouse users. This only works if the trigger is genuinely focusable. |
| Dismissed | Esc sets data-tooltip-dismissed="true" on the trigger and CSS hides it, without moving focus — this is the WCAG 2.2 SC 1.4.13 requirement. The flag clears on the next mouseleave or blur, so the tooltip works again afterwards. |
| Touch | No hover and often no focus, so the tooltip effectively never appears. Assume touch users will not see it. |
Real-World Usage
The archetypal case — an icon-only toolbar button whose meaning isn't obvious from the glyph. Here the tooltip text also becomes the button's accessible name, which is exactly what you want when there's no visible label.
<!-- Icon-only: let the tooltip supply the accessible name -->
<button class="ws-btn ws-btn--icon-only" <?= ws_tooltip('Duplicate workshop') ?>>
<span class="material-symbols-outlined">content_copy</span>
</button>
<!-- Labelled control: suppress the aria-label so the visible text wins -->
<button class="ws-btn" <?= ws_tooltip('Saves to your library', ['label' => false]) ?>>
Save workshop
</button>Options
ws_tooltip() returns a string of HTML attributes, not an element. Echo it inside a tag, never as a child of one.
| Option | Type | Default | Purpose |
|---|---|---|---|
$text | string | '' | Positional tooltip text. Escaped for both the data attribute and the label. |
position | string | 'top' | top | bottom | left | right |
variant | string | 'dark' | dark | light |
arrow | bool | true | Show the pointer arrow. Only false emits an attribute. |
label | bool | true | Emit aria-label with the tooltip text. Set this to false on any control that already has visible text — see below. |
<!-- The helper outputs attributes like: --> data-tooltip="Duplicate workshop" data-tooltip-position="top" data-tooltip-variant="dark" aria-label="Duplicate workshop"
Accessibility
The implementation is CSS-only, so it doesn't follow the WAI-ARIA Tooltip pattern literally — a pseudo-element can't carry role="tooltip" and there's no node for aria-describedby to point at. It reaches the same outcome through aria-label instead.
| Concern | Behavior |
|---|---|
| ARIA | With label: true (the default) the trigger gets aria-label carrying the tooltip text. There is no role="tooltip" and no aria-describedby — the visual tooltip is a pseudo-element and invisible to assistive tech. |
| Naming vs describing | The critical distinction. aria-label replaces the accessible name. On an icon-only button that's ideal. On a button with visible text it overrides that text, which breaks WCAG 2.5.3 Label in Name and desyncs voice control. Pass label => false there. |
| Keyboard | Appears on Tab focus and hides on blur, both from CSS. Esc dismisses without moving focus. |
| Focusable triggers only | On a <span> or <div> the tooltip is hover-only — unreachable by keyboard and unannounced. Attach only to buttons, links, and inputs, or add tabindex="0". |
| Touch | Not available without hover. Never let a tooltip be the only place important information lives. |
| Contrast | Dark and light variants each pair tested foreground and background tokens. |
Tokens
Tooltip styling lives on attribute selectors rather than a class block, so its declarations sit outside the usual .ws-* rules. It draws on the shared ink, surface, radius, and type scales — see Token Reference for current values, and read the [data-tooltip] rules in components.css for the exact set.
CSS Classes
There are none — this component is styled entirely through attribute selectors, which is why it composes onto any element without touching its class list.
| Selector | Purpose |
|---|---|
[data-tooltip] | Base styles. Content comes from content: attr(data-tooltip). |
[data-tooltip-position="top|bottom|left|right"] | Placement |
[data-tooltip-variant="light"] | Light colour scheme |
[data-tooltip-arrow="false"] | Suppresses the arrow |
[data-tooltip-dismissed="true"] | Set by the Esc handler; hides the tooltip until pointer or focus leaves |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_tooltip() helper function |
includes/components/tooltip.php | Template — builds the attribute string |
includes/components/components.css | Pseudo-element styling ([data-tooltip] rules) |
includes/components/components.js | Esc dismissal for WCAG 2.2 SC 1.4.13, and the flag cleanup on mouseleave/blur |