Canvas Design System
Main Site Tokens

Tooltip

Supplementary text on hover or focus. Unusually, this helper returns attributes rather than an element.

When to Use

Use when: A focusable control needs a short clarification — naming an icon button, expanding a truncated label, explaining a form field. Keep the text to a few words.
Don't use when: The information is required to finish the task — put it inline, or use Alert. Never attach one to a non-focusable element, and never put links or buttons inside; there's no way to reach them.

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

StateBehavior
HiddenDefault. The tooltip exists only as a CSS pseudo-element, so there's nothing in the DOM to inspect.
HoverShown via :hover on the trigger. No delay — it appears immediately.
FocusShown via :focus, so keyboard users get the same information as mouse users. This only works if the trigger is genuinely focusable.
DismissedEsc 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.
TouchNo 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.

OptionTypeDefaultPurpose
$textstring''Positional tooltip text. Escaped for both the data attribute and the label.
positionstring'top'top | bottom | left | right
variantstring'dark'dark | light
arrowbooltrueShow the pointer arrow. Only false emits an attribute.
labelbooltrueEmit 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.

ConcernBehavior
ARIAWith 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 describingThe 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.
KeyboardAppears on Tab focus and hides on blur, both from CSS. Esc dismisses without moving focus.
Focusable triggers onlyOn 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".
TouchNot available without hover. Never let a tooltip be the only place important information lives.
ContrastDark 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.

SelectorPurpose
[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

FilePurpose
includes/components/helpers.phpws_tooltip() helper function
includes/components/tooltip.phpTemplate — builds the attribute string
includes/components/components.cssPseudo-element styling ([data-tooltip] rules)
includes/components/components.jsEsc dismissal for WCAG 2.2 SC 1.4.13, and the flag cleanup on mouseleave/blur