Tooltips
components specs/components/tooltips.kmd
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).
Quando esta spec se aplica
Triggers primários
- Add a tooltip
Todos os triggers
- Reveal label on an icon-only button
- Show a hint about a control without crowding the layout
- Provide help context (rich tooltip with link)
Corpo da especificação
Spec — Tooltips
Facet Visual of Koder Design. Material parity: https://m3.material.io/components/tooltips.
2 variants
| Variant | Use | Content |
|---|---|---|
| Plain | Icon button label, short hint | 1-3 word label |
| Rich | 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)
└───┘
- Background:
inverse-surface(high contrast) - Text:
inverse-on-surface,body-small(12/16, weight 400) - Padding: 8 px horizontal, 4 px vertical
- Corner radius: 4 px
- Elevation: 0 (no shadow — relies on contrast)
- Max width: 200 dp; wraps at width
- Arrow (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
└─────────────────────────────────────────┘
▲
┌──────┐
│ 🎤 │
└──────┘
- Background:
surface-container(lighter / neutral, not inverse) - Title:
title-small(14/20, weight 500),on-surface - Body:
body-small(12/16, weight 400),on-surface-variant - Action: text button at bottom-right
- Padding: 16 px
- Corner radius: 12 px
- Elevation: 2 dp (shadow + tonal overlay)
- Max 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 |
|---|---|
| Top (above trigger) | Default — won't conflict with click target |
| Bottom | When top would overflow viewport |
| Right / Left | 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-this-does help, deprecated warnings 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
- Enter: fade-in + scale 0.95 → 1 from anchor edge (motion-fast, ~150 ms)
- Exit: fade-out (motion-fast)
- Position 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 (long-press fallback mandatory)
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— fade-in / fade-out timinginteraction/states.kmd— trigger hover / focused / pressed conflicts with tooltip visibilityfoundations/elements.kmd— Marker family
Requirements (testable)
Requirement: Multi-modal trigger interactions {#req-tooltip-triggers}
A tooltip SHALL be reachable by all three input modes — mouse hover, keyboard focus, and touch long-press — and SHALL NOT be mouse-only; each mode shows the tooltip on its documented gesture and hides it on the corresponding release, blur, tap-elsewhere, or Esc.
Scenario: Mouse hover shows after delay and hides on leave
- GIVEN an icon-only button with a plain tooltip and a mouse pointer
- WHEN the pointer hovers over the trigger for 500 ms
- THEN the tooltip becomes visible
- AND WHEN the pointer leaves the trigger
- THEN the tooltip hides
Scenario: Keyboard focus shows the tooltip
- GIVEN a trigger reachable via Tab
- WHEN the trigger receives keyboard focus
- THEN the tooltip is shown immediately (0 ms focus delay)
- AND WHEN the trigger is blurred
- THEN the tooltip hides
Scenario: Touch long-press shows the tooltip
- GIVEN the trigger is rendered on a touch surface with no pointer hover available
- WHEN the user long-presses the trigger for 500 ms
- THEN the tooltip is shown
- AND WHEN the press is released
- THEN the tooltip hides
Requirement: Viewport-aware positioning {#req-tooltip-positioning}
A tooltip SHALL default to the Top position above its trigger, SHALL auto-flip at runtime to any alternate side when the preferred side would overflow the viewport, and SHALL remain within 8 px of the viewport edge while preserving the 8 px offset gap from the trigger.
Scenario: Tooltip flips to bottom when top would overflow
- GIVEN a trigger positioned near the top edge of the viewport such that a top-anchored tooltip would render off-screen
- WHEN the tooltip is shown
- THEN the tooltip renders below the trigger (Bottom position)
- AND the tooltip stays within 8 px of the viewport edge
- AND the tooltip keeps an 8 px offset gap from the trigger
Requirement: At most one tooltip visible {#req-tooltip-single-instance}
At any moment no more than one tooltip SHALL be visible on a surface; moving from one trigger to another SHALL hide the previous tooltip immediately before the new one appears.
Scenario: Moving between toolbar triggers hides the previous tooltip
- GIVEN a toolbar with trigger A showing its tooltip
- WHEN the pointer moves to adjacent trigger B and B's tooltip becomes eligible to show
- THEN A's tooltip is hidden immediately
- AND at no point are two tooltips visible simultaneously
Requirement: Trigger-state visibility behavior {#req-tooltip-states}
Tooltip visibility SHALL follow the trigger's interaction state: the tooltip SHALL be hidden while the trigger is actively pressed to avoid overlap, and it SHALL still be allowed to show on a disabled trigger so the reason for the disabled state can be conveyed.
Scenario: Tooltip hides during an active press
- GIVEN a trigger whose tooltip is visible on hover
- WHEN the trigger enters the active/pressed state
- THEN the tooltip is hidden for the duration of the press
Scenario: Disabled trigger can still show its tooltip
- GIVEN a disabled trigger that carries a tooltip explaining why it is disabled
- WHEN the trigger is hovered or focused
- THEN the tooltip is shown
Requirement: Accessibility and Esc dismissal {#req-tooltip-a11y}
A tooltip SHALL be exposed to assistive technology — a plain tooltip's content via the trigger's aria-label or an aria-describedby reference, and a rich tooltip via role="tooltip" on the container referenced by the trigger's aria-describedby — SHALL be removed from the focus order unless a rich tooltip's single action is present, and SHALL close on Esc; critical information SHALL NOT live only in a tooltip.
Scenario: Trigger references the tooltip for screen readers
- GIVEN a trigger with an associated tooltip element
- WHEN the accessibility tree is inspected
- THEN the trigger exposes the tooltip content via
aria-labeloraria-describedby - AND a rich tooltip container carries
role="tooltip"
Scenario: Esc closes any open tooltip
- GIVEN a tooltip is currently visible
- WHEN the user presses Esc
- THEN the tooltip closes immediately
Scenario: Non-action tooltip is not tabbable
- GIVEN a plain tooltip with no action is open
- WHEN the user presses Tab
- THEN focus moves past the tooltip and the tooltip element itself is not part of the focus order
Requirement: Reduced-motion animation {#req-tooltip-motion}
A tooltip's default enter animation SHALL be a fade-in with a 0.95→1 scale over motion-fast timing, but when the user requests reduced motion the tooltip SHALL appear and disappear instantly with no scale.
Scenario: Reduced motion disables scale and fade
- GIVEN the user has reduced-motion enabled
- WHEN a tooltip is shown and later hidden
- THEN the tooltip appears instantly with no scale transform
- AND it disappears instantly with no fade
Referências
specs/foundations/elements.kmdspecs/themes/color-roles.kmdspecs/themes/elevation.kmdspecs/themes/typography.kmdspecs/themes/motion.kmdspecs/interaction/states.kmd