Tooltip
A short hint for a control, shown when the pointer rests on it or it has focus. It adds a description, never the only name: a control that needs a label has one of its own.
The preview needs JavaScript. The code is in the Code tab.
<template>
<div>
<Tooltip>
<Button tone="neutral" emphasis="outline">Save</Button>
<span slot="content">Saves your changes as a draft</span>
</Tooltip>
<Tooltip placement="bottom">
<Icon name="circle-help" label="Help" />
<span slot="content">What the fields mean</span>
</Tooltip>
</div>
</template>
Installation
npx @pixelscriptio/tessera add Tooltippnpm exec tessera add Tooltipyarn tessera add Tooltipbunx @pixelscriptio/tessera add Tooltip- Also installs Button, which it uses.
- Works with Tessera 0.9.0 or newer.
Copy and paste the code into your project
Put it in
system/components. Add Button the same way.system/components/Tooltip.tess <contract> { "name": "Tooltip", "role": "generic", "description": "A short hint for a control, shown when the pointer rests on it or it has focus. It adds a description, never the only name: a control that needs a label has one of its own.", "since": "0.8.0", "parts": ["root", "tip"], "variants": { "placement": ["top", "bottom", "right", "left"] // first value is the default }, "states": ["hover", "focus-visible"], "slots": { "default": "what the tip describes: a button, a link or a field", "content": "the hint, one short line of plain text" }, "preview": { "slots": { "default": { "component": "Button", "props": { "tone": "neutral", "emphasis": "outline" }, "slots": { "default": "Hover or focus me" } }, "content": "Saves your changes as a draft" } } } </contract> <script> const { delay } = props({ delay: 400 }) const uid = `t-tooltip-${Math.random().toString(36).slice(2, 9)}` const box = state(null) const tip = state(null) const shown = state(false) const anchored = typeof CSS !== 'undefined' && CSS.supports('position-area', 'bottom') // What the tip describes is the first control in the slot. It is pointed at the tip, so a screen reader reads the hint // after the name. With no control in the slot, the wrapper itself can be reached with the keyboard, so the hint is not // for the pointer alone. effect(() => { const root = box.value if (!root) return const found = [...root.querySelectorAll('button, a[href], input, select, textarea, [tabindex]')] const control = found.find((el) => !(tip.value && tip.value.contains(el))) const target = control ?? root if (!control) root.setAttribute('tabindex', '0') target.setAttribute('aria-describedby', uid + '-tip') }) let timer = null function reveal() { const el = tip.value const root = box.value if (!el || !el.showPopover || el.matches(':popover-open')) return el.showPopover() if (!anchored && root) { const rect = root.getBoundingClientRect() el.style.top = rect.bottom + 6 + 'px' el.style.left = rect.left + 'px' } shown.value = true } function conceal() { const el = tip.value if (el && el.hidePopover && el.matches(':popover-open')) el.hidePopover() shown.value = false } // Focus shows it at once. The pointer waits a moment, so passing over does not flash it, and leaving waits a moment // too, so the pointer can reach the tip itself (it stays open while it is over it). function enter(event) { clearTimeout(timer) timer = setTimeout(reveal, delay.value) } function focused() { clearTimeout(timer) reveal() } function leave() { clearTimeout(timer) timer = setTimeout(conceal, 100) } function key(event) { if (event.key === 'Escape') { clearTimeout(timer) conceal() } } onCleanup(() => clearTimeout(timer)) </script> <template> <span class="root" ref="box" :style="'anchor-name: --' + uid" @mouseenter="enter($event)" @mouseleave="leave()" @focusin="focused()" @focusout="leave()" @keydown="key($event)" > <slot /> <span class="tip" ref="tip" popover="manual" role="tooltip" :id="uid + '-tip'" :style="'position-anchor: --' + uid" ><slot name="content" /></span> </span> </template> <style> .root { display: inline-flex; } .tip { position: fixed; inset: auto; margin: $space.2; max-width: 20rem; padding: $space.1 $space.3; border-radius: $radius.sm; background: $color.surface.inverse; color: $color.surface.default; font-family: var(--t-font-family-sans); font-size: $font.size.sm; line-height: 1.4; position-area: top; position-try-fallbacks: flip-block, flip-inline; } .root[data-placement='bottom'] .tip { position-area: bottom; } .root[data-placement='right'] .tip { position-area: right; } .root[data-placement='left'] .tip { position-area: left; } .tip:not(:popover-open) { display: none; } </style>Check your design tokens
Your design system needs these.
tessera addreports the ones that are missing; by hand, runnpx @pixelscriptio/tessera check.color.surface.defaultcolor.surface.inversefont.size.smradius.smspace.1space.2space.3
Usage
Using Tooltip
A short hint for a control, shown when the pointer rests on it or it has focus.
- Put the control in the default slot and the hint in
content. The hint is one short line of plain text. If it needs more, use a Popover. - A tooltip describes, it does not name. A control with only an icon needs its own label (
labelon an Icon, or text); the tooltip may repeat it for sighted people but is not the only source. Screen readers read the hint after the name. - It shows when the pointer rests on the control for a moment (
delay, 400 ms), and at once on keyboard focus. It stays while the pointer is over it, and Escape hides it, so it never traps what is under it. - If the slot has no button, link or field, the wrapper itself takes focus, so keyboard users can reach the hint too.
- Never put a link, a button or anything to press in a tooltip: it disappears when focus moves.
placementis the side it prefers; the browser moves it to another side when there is no room.
The preview needs JavaScript. The code is in the Code tab.
<template>
<div>
<Tooltip>
<Button tone="neutral" emphasis="outline">Save</Button>
<span slot="content">Saves your changes as a draft</span>
</Tooltip>
<Tooltip placement="bottom">
<Icon name="circle-help" label="Help" />
<span slot="content">What the fields mean</span>
</Tooltip>
</div>
</template>
API reference
Variants
| Variant | Values |
|---|---|
placement | top default bottom right left |
Slots
| Slot | Content |
|---|---|
default | what the tip describes: a button, a link or a field |
content | the hint, one short line of plain text |
Anatomy
| Role | generic |
| Parts | root tip |
| States | hover focus-visible |
| Since | 0.8.0 |
Design tokens it uses
Your design system needs these. tessera add checks that it has them, and can add the ones it lacks.
color.surface.default color.surface.inverse font.size.sm radius.sm space.1 space.2 space.3
Checksum
705cecd323a7850cf5913a8b975065e867143993e402dda6abbcfd131e8f1399