Tooltip
A contextual hint that appears on hover (desktop) or long-press (mobile). Ink background, paper text, 10px radius. Used sparingly — only for information that doesn't fit inline and isn't critical enough for a label. Max 200px wide, max 2 lines.
Variants
Four placements: top (default), bottom, left, right. All share the same visual treatment. Choose placement based on available space — the tooltip should never clip off-screen or obscure the trigger element.
Hover over the buttons above to see each placement.
Sizes
| Property | Value | Notes |
|---|---|---|
| Max width | 200px | Text wraps within this. Never wider. |
| Border radius | 10px | Between card (14px) and tag (pill) |
| Padding | 8px 12px | Compact — tooltip should feel like a label, not a card |
| Font size | 13px | One step below body — contextual, not primary |
| Max lines | 2 | If you need more, the hint needs to be a label or help text |
| Offset from trigger | 8px | Consistent gap on all placements |
States
| State | Opacity | Transform | Trigger |
|---|---|---|---|
| Hidden | 0 | scale(0.96) | Default |
| Visible | 1 | scale(1) | Hover (desktop), long-press 500ms (mobile), focus-within |
| Exit | 0 → via transition | scale(1) → scale(0.96) | Pointer leaves, focus leaves, touch ends |
On dark surfaces
Tooltip appearance is the same on dark surfaces — ink background with paper text is self-contained and readable against both light and dark canvas. No dark-mode variant is needed. The shadow intensifies naturally because --shadow-md uses the dark-mode override from tokens.
Anatomy
| Part | Element | Notes |
|---|---|---|
| Wrapper | .tooltip-wrap | Relative-positioned container. The trigger and tooltip are both children of this wrapper. |
| Trigger | Any focusable element | Usually a button, icon button, or element with tabindex="0". Provides aria-describedby pointing to the tooltip. |
| Bubble | .tooltip | Absolute positioned. Ink bg, paper text. Opacity + scale transition on show/hide. |
| Arrow | .tooltip::after | CSS triangle using borders. Color must match the bubble background. |
Usage
Accessibility
id and add aria-describedby="that-id" to the trigger element. Add role="tooltip" to the bubble. Screen readers will announce the tooltip content after the trigger's own label.
:focus-within on the wrapper). It dismisses when focus leaves. This means keyboard users always have access — no mouse required. Never remove the focus trigger.
prefers-reduced-motion: reduce, skip the scale animation — only fade (opacity). The transition duration remains the same; only the transform is removed.
Code
HTML
<!-- Top placement (default) --> <div class="tooltip-wrap"> <button aria-describedby="tip-cap">Cap meter</button> <div class="tooltip tooltip-top" id="tip-cap" role="tooltip"> Resets on the 1st of each month </div> </div> <!-- Right placement --> <div class="tooltip-wrap"> <button aria-describedby="tip-upi">Send request</button> <div class="tooltip tooltip-right" id="tip-upi" role="tooltip"> UPI request — no bank login needed </div> </div>
JavaScript — mobile long-press
// Long-press to reveal tooltip on mobile document.querySelectorAll('.tooltip-wrap').forEach(wrap => { let timer; wrap.addEventListener('touchstart', () => { timer = setTimeout(() => wrap.classList.add('lp-active'), 500); }, { passive: true }); wrap.addEventListener('touchend', () => clearTimeout(timer)); wrap.addEventListener('touchmove', () => clearTimeout(timer)); document.addEventListener('touchstart', e => { if (!wrap.contains(e.target)) wrap.classList.remove('lp-active'); }); });
Design tokens used
| Token | Value | Role |
|---|---|---|
--ink | #171513 | Bubble background + arrow |
--s-paper | #FBF8F0 | Bubble text |
--shadow-md | 0 8px 24px -8px rgba(23,21,19,.12) | Bubble elevation |
--d-fast | 140ms | Show/hide transition |
--ease-out | cubic-bezier(0.23,1,0.32,1) | Scale + opacity easing |
--z-tooltip | 300 | Stack order — above toasts |