Tooltips
Brief contextual label revealing the name or description of a UI element on hover, focus, or long-press. Material parity (`/components/tooltips`). Covers plain tooltips (label-only) and rich tooltips (multi-line + optional action).
Spec — Tooltips
Facet *isual*of Koder Design. Material parity: https://m3.material.io/components/tooltips.
2 variants
| Variant | Use | Content |
|---|---|---|
| *lain* | Icon button label, short hint | 1-3 word label |
| *ich* | Help / explanation, longer context | Title + body + optional action |
Pick plain by default. Rich tooltip is opt-in for help / docs.
Anatomy (plain)
┌──────────┐
│ Settings │ ← tooltip
└──────┬───┘
▼ ← optional arrow (Material 3 default has no arrow)
┌───┐
│ ⚙ │ ← trigger (icon button)
└───┘- *ackground*
inverse-surface(high contrast) - *ext*
inverse-on-surface,body-small(12/16, weight 400) - *adding* 8 px horizontal, 4 px vertical
- *orner radius* 4 px
- *levation* 0 (no shadow — relies on contrast)
- *ax width* 200 dp; wraps at width
- *rrow*(optional): triangle pointing to trigger, 8 px base
Anatomy (rich)
┌─────────────────────────────────────────┐
│ Voice search │ ← title
│ ────────────────────────────────────── │
│ Tap the mic to dictate. Hold for │ ← body
│ push-to-talk mode. │
│ [Learn more]│ ← optional action
└─────────────────────────────────────────┘
▲
┌──────┐
│ 🎤 │
└──────┘- *ackground*
surface-container(lighter / neutral, not inverse) - *itle*
title-small(14/20, weight 500),on-surface - *ody*
body-small(12/16, weight 400),on-surface-variant - *ction* text button at bottom-right
- *adding* 16 px
- *orner radius* 12 px
- *levation* 2 dp (shadow + tonal overlay)
- *ax width* 320 dp
R1 — Trigger interactions
| Surface | Trigger to show | Trigger to hide |
|---|---|---|
| Mouse | Hover after 500 ms delay | Mouse leave OR Esc |
| Keyboard | Focus | Blur OR Esc |
| Touch | Long-press (500 ms) | Release OR tap elsewhere |
| Programmatic | API call | API call |
Tooltips MUST be triggered by ALL three input modes — never mouse-only.
R2 — Positioning
| Position | When |
|---|---|
| *op*(above trigger) | Default — won't conflict with click target |
| *ottom* | When top would overflow viewport |
| *ight*/ *eft* | Compact spaces (toolbar at top of screen) |
Position auto-flips at runtime to stay inside viewport (within 8 px of edge).
Offset from trigger: 8 px gap (with or without arrow).
R3 — Plain tooltip rules
- Single line preferred; wraps to 2 lines max
- Don't repeat what's already visible (button has text "Save" → no
tooltip "Save")
- For icon-only buttons: tooltip is THE label
- For text buttons: tooltip adds detail only when useful
("Save" tooltip "Save to local drive (Ctrl+S)")
R4 — Rich tooltip rules
- Used for help / explanation, not basic labels
- Stays visible while user moves into the tooltip area (hover bridge)
- Single optional action (link, "Learn more", "Try it")
- Can be dismissed by Esc or clicking outside
- Useful for: feature discovery, what
thisdoes help, deprecatedwarnings with migration link
R5 — States (trigger)
Tooltip itself doesn't have states — the trigger does:
| Trigger state | Tooltip behavior |
|---|---|
| Idle | Tooltip hidden |
| Hover / Focus | Show after delay |
| Active / Pressed | Hide tooltip during press (avoids overlap) |
| Disabled | Tooltip still shows (explains WHY disabled is useful) |
R6 — Timing
| Event | Delay |
|---|---|
| Hover-in show | 500 ms (Material 3 default; configurable 300-700 ms) |
| Hover-out hide | 300 ms (gives user time to move to tooltip if rich) |
| Long-press show | 500 ms (system gesture) |
| Focus show | 0 ms (immediate for keyboard) |
| Esc hide | 0 ms (immediate) |
R7 — Multiple tooltips on same surface
- Only ONE tooltip visible at a time
- Moving to another trigger: previous hides immediately; new one
shows after its delay
- Long horizontal toolbar: skip the show-delay AFTER first tooltip
appears (subsequent hovers within 500 ms are "instant" — feels responsive)
R8 — Accessibility
- Plain tooltip's content goes into trigger's accessible name via
aria-labelORaria-describedbypointing to the tooltip element - Rich tooltip:
role="tooltip"on tooltip container; trigger hasaria-describedbypointing to it - Tooltip is hidden from focus order (cannot be tabbed to) UNLESS
rich tooltip has an action — then the action is part of focus order while tooltip is open
- Esc closes any tooltip
- Screen reader announces tooltip content when trigger receives focus
- Don't put critical information ONLY in tooltips (must be available
to all users; tooltip is supplemental)
R9 — Animation
- *nter* fade
in + scale 0.95 → 1 from anchor edge (motionfast,~150 ms)
- *xit* fade
out (motionfast) - *osition flip during scroll* smooth re-anchor
- Reduced motion: instant in / out; no scale
R10 — Density
| Density | Plain padding | Plain font | Rich padding |
|---|---|---|---|
| Compact | 6 px / 3 px | 11 px | 12 px |
| Default | 8 px / 4 px | 12 px | 16 px |
| Comfortable | 12 px / 6 px | 14 px | 20 px |
R11 — Per-preset variation
| Preset | Plain | Rich |
|---|---|---|
material3 |
Inverse surface, no arrow | Surface-container, 12 px radius, optional action |
material2 |
Dark gray, optional arrow | Solid white card with shadow |
ios_cupertino |
Toast-style, light blur | Popover style with arrow |
gnome |
Adwaita compact rounded | Adwaita popover style |
windows_11 |
Compact pill, no arrow | Fluent flyout |
brutalist |
Solid black box, sharp corners, white text | Sharp card with 2 px border |
terminal_classic |
(?) suffix expands inline |
Bracketed help block below trigger |
R12 — Forbidden patterns
- ❌ Tooltip-only labels for critical actions (must have visible
label or icon recognized by screen readers)
- ❌ Tooltips longer than 3 lines (use rich tooltip; if rich, escalate
to inline help or dialog)
- ❌ Tooltip on disabled element WITHOUT explaining why
- ❌ Multiple tooltips visible at once (always ≤ 1)
- ❌ Tooltips with interactive content other than a single action
(use popover or dialog)
- ❌ Tooltips that show on click (use popover for click-triggered)
- ❌ Tooltips that obstruct the trigger element
- ❌ Tooltips that appear without the user having interacted at all
(intrusive — use banners for proactive info)
- ❌ Hover
only tooltips on touch surfaces (longpress fallbackmandatory)
Cross-link
themes/color-roles.kmd—inverse-surface(plain) /surface-container(rich)themes/elevation.kmd— rich tooltip 2 dp recipethemes/typography.kmd—body-smallplain /title-smallrichthemes/motion.kmd— fadein / fadeout timinginteraction/states.kmd— trigger hover / focused / pressedconflicts with tooltip visibility
foundations/elements.kmd— Marker family