Tooltip & Hint Systems: Chat Prompts
Tooltips are the smallest UI surface in your app and one of the easiest to ship badly. They sit somewhere between "label-but-too-long-for-a-button" and "explanation-but-too-short-for-help-docs." Done well, they reduce cognitive load and unblock users without interruption. Done poorly, they pop up at the wrong time, hide critical UI, are unreachable on mobile, fail accessibility, or display "undefined" when the underlying data is null.
This is the chat-prompt playbook for shipping a tooltip system that's accessible, mobile-friendly, performance-aware, and scales beyond ad-hoc per-component implementations.
When You Need a Tooltip
Use tooltips for:
- Disambiguating icon-only buttons (the trash icon in the header — "Delete row")
- Explaining technical jargon ("ARR — Annual Recurring Revenue")
- Showing full-text on truncated content (long names cut to "Lorem ipsum dolor s...")
- Surfacing keyboard shortcuts ("Save (⌘S)")
- Quick context for "i" info icons
- Reasons UI is disabled ("Upgrade to Pro to use this")
Don't use tooltips for:
- Critical information users must read (use inline text or modal)
- Long content (3+ sentences → use a popover instead)
- Mobile-essential context (touch devices have weak tooltip support)
- Information shown to first-time users (use onboarding tour or in-product help)
- Replacing labels (an icon-only button without a visible label fails accessibility for users who can't hover)
A tooltip is supplementary. If users need the info to complete the task, surface it visibly.
Picking a Library
In 2026, the dominant choices:
- Radix Tooltip — primitive; composable; accessible by default. The default for shadcn/ui apps.
- Floating UI (the underlying positioning lib) — when you need more control than Radix exposes.
- shadcn/ui Tooltip — Radix-based with sensible defaults pre-styled.
I'm building a Next.js app with shadcn/ui. I want a tooltip system. Pick the right approach.
Recommend:
- Use shadcn/ui's Tooltip (built on Radix) for 90% of cases — accessible, composable, well-styled
- Drop to raw Radix Tooltip when I need more customization
- Use Floating UI directly only for non-tooltip floating UI (e.g. complex menus / dropdowns / popovers I'm building from scratch)
Don't use:
- react-tooltip (older lib; less accessible)
- @tippyjs/react (good but Radix is more aligned with the modern stack)
- Custom DIY (no — you'll get accessibility wrong)
Stack: Next.js 16 + Tailwind + shadcn/ui.
Basic Tooltip Component
Build a `<Tooltip>` component using shadcn/ui:
API:
```tsx
<Tooltip content="Delete this row">
<button><TrashIcon /></button>
</Tooltip>
Behavior:
- Hover (desktop): show after 500ms
- Focus (keyboard): show immediately
- Click on touch (mobile): toggle on tap (since hover doesn't exist on touch)
- Hide: on blur, on Escape, on mouse leave (with 100ms delay to avoid flicker)
- Arrow pointing at the trigger
- Auto-position: above the trigger by default; flip below if not enough room
- Boundary: don't go off-screen — use viewport edges as boundary
Style:
- Small (12-13px font); muted background (gray-900 / dark mode gray-100)
- White text on dark background; flipped for light mode
- Small max-width (~250px) — wrap if longer
Accessibility:
- ARIA: aria-describedby on the trigger pointing to the tooltip
- Hidden from screen readers when collapsed (via aria-hidden on the tooltip element when not visible)
- Focusable trigger
- Escape closes
- Keyboard navigable
Implement using shadcn/ui's
Stack: Next.js + Tailwind + shadcn/ui.
## Touch / Mobile Tooltips
The hard problem. Mobile devices don't have hover. Default browser tooltip behavior on mobile is broken.
My tooltip works on desktop but on mobile, hovering doesn't work. Pick a strategy:
Option 1: Tap-to-toggle
- On touch device, tap the trigger → show tooltip; tap again → hide
- Tap elsewhere also closes
- Risk: conflicts with the trigger's primary action (e.g. tooltip on a button — does tap show tooltip or click button?)
Option 2: Long-press
- Long-press (500ms+) shows tooltip on touch
- Tap (short) does the primary action
- More natural; preserves button semantics
Option 3: Skip tooltip on mobile entirely
- Show inline label or alternative help instead on mobile
- Cleanest for buttons where the primary action matters
Option 4: Replace tooltip with popover on touch
- On touch, the trigger opens a small popover with the same content + close button
Implement option 2 (long-press) as the primary path. Show me:
- Detection:
@media (hover: none)to detect touch devices - Long-press handler:
onTouchStart→ setTimeout 500ms → show tooltip;onTouchEndbefore timeout → cancel + run primary action - Dismiss on outside-tap
Stack: React + Radix (or your chosen library).
## Disabled-Reason Tooltips
A common pattern: a button is disabled; user hovers; tooltip explains why.
I have buttons that are sometimes disabled — e.g. "Upgrade your plan to enable this", "You don't have permission", "Save your draft first".
Build a <DisabledTooltip reason={...}> wrapper:
- When disabled: show tooltip on hover/focus explaining why
- When enabled: no tooltip; primary tooltip if any
- The disabled button must still be hover-able (HTML disabled attribute prevents hover events) — wrap in a span with tabIndex=0; or use aria-disabled instead of disabled
Implementation:
- A wrapper that swaps the disabled attr for aria-disabled when reason is provided
- Tooltip content = the reason
- The button visually appears disabled but is keyboard-focusable + hover-able
- Click handler short-circuits if aria-disabled
Edge cases:
- Don't break form submission (aria-disabled buttons still submit forms — handle in the click handler)
- Provide aria-disabled="true" so screen readers announce "disabled, [reason]"
Show me the component + usage pattern.
Stack: shadcn/ui Tooltip + Tailwind.
This is the most common tooltip ask. Get it right; it removes hours of "why doesn't X work?" support tickets.
## Truncated-Text Tooltips
When a name / title is truncated with ellipsis, hovering should show the full text.
I have UI where long names get truncated:
<span className="truncate">{item.name}</span>
When truncated (text doesn't fit), show a tooltip with the full text on hover. When not truncated, no tooltip.
Build a <TruncatedText> component:
- Render the text with truncate / overflow-hidden styling
- Use a ResizeObserver or check
scrollWidth > clientWidthto detect overflow - Only render tooltip if text is actually truncated
- Tooltip shows the full text
Implement:
- The component
- The truncation-detection hook
- Resilience: don't crash if ResizeObserver isn't supported (older browsers)
Stack: React + Radix Tooltip.
## Keyboard-Shortcut Tooltips
Buttons in my app have keyboard shortcuts (e.g. Save = ⌘S). Show the shortcut in the tooltip.
Build a <ShortcutTooltip label="Save" shortcut="⌘S"> pattern:
- Tooltip shows: "Save" on first line, "⌘S" in a styled on second line
- Properly render Mac vs Windows shortcuts (⌘ vs Ctrl) based on platform detection
- Accessible: shortcut is in aria-keyshortcuts attribute on the trigger
Style the kbd:
- Small, monospace, with subtle border + background
- Inline with the label
Stack: shadcn/ui + Tailwind.
## Hint System (vs Tooltip)
A "hint" is a slightly fatter tooltip — multi-line, sometimes with an icon. Used for "info" surfaces.
For info-icon hints (the "i" circle next to a field that explains what it does), I want richer than a basic tooltip.
Build a <Hint> component:
- Trigger: a small "ⓘ" icon
- Content: 2-5 sentences; can include a link
- Behavior: hover/focus shows; click toggles for keyboard / touch
- Width: ~300px max
- Includes a small arrow / pointer
Use Radix Popover for richer content (vs Tooltip for simple).
Show me:
- The Hint component
- Usage:
<label>Tax ID <Hint>Your business tax ID number...</Hint></label> - Style: subtle, doesn't compete with the field label
Stack: shadcn/ui Popover.
The line between tooltip and hint:
- **Tooltip** = quick label / short clarification (single line, no interactive content, dismiss on hover-out)
- **Hint / Popover** = explanation with possibly a link (multi-line, may have interactive content, dismiss on click-outside)
- **Help docs** = full explanation (separate page or in-product help)
## Tooltip Performance at Scale
A page with 200 rows + 5 tooltip-attached icons each = 1000 Radix Tooltip instances. That hurts.
I have a data table with 500 rows; each row has 4 icons with tooltips. Performance is bad — initial render is slow.
Strategies:
- Don't render tooltip content until the trigger is hovered/focused — use Radix's lazy mounting
- Single global tooltip portal; reuse one positioning context
- Virtualize rows so off-screen rows don't render anything
Show me option 1 (the most common):
- Use Radix Tooltip's
<TooltipProvider>wrapped at app level (delayDuration shared) - Tooltip content lives inside
<TooltipContent>which Radix mounts only on hover - Avoid: rendering 500 popover components even if hidden
Verify by checking React DevTools that hidden tooltip content is not in the tree.
Stack: Radix Tooltip in Next.js.
## Internationalization
Tooltips often have user-facing text. Don't hardcode strings.
My tooltips have English-only labels currently. I need to internationalize them.
Pattern:
- Tooltip content comes from a translation key
- Use next-intl, react-i18next, or your i18n library to look up the localized string
- Account for variable text length — Japanese is shorter; German is longer
- Test with longest known languages (Russian, German) — does the tooltip overflow?
Implement:
- Replace hardcoded
<Tooltip content="Delete">with<Tooltip content={t('actions.delete')}> - Handle: tooltips that need to show different text for different languages (rare — usually just translation)
- Handle: RTL languages (Arabic, Hebrew) — ensure positioning flips correctly
Stack: next-intl + Radix Tooltip.
## Accessibility Deep-Dive
Make my tooltip system fully accessible. Requirements:
- Trigger is keyboard-focusable (button or [tabIndex=0])
- Tooltip appears on focus, not just hover
- Escape dismisses
- ARIA: trigger has
aria-describedby="<tooltip-id>"; tooltip hasid="<tooltip-id>"androle="tooltip" - Hidden from screen readers when not visible (aria-hidden + display:none)
- Color contrast: tooltip text vs background meets WCAG AA (4.5:1 minimum)
- Doesn't rely on color alone for any meaning conveyed in tooltip
Verify with:
- Keyboard-only navigation through the page (works?)
- Screen reader (NVDA / VoiceOver) — announces tooltip on focus?
- Hover-disabled (disable mouse and try) — can I still discover tooltip content?
Implement updates to my existing tooltip component to meet these. Radix gets most of these right by default; verify and harden.
Stack: Radix + Tailwind.
## Common Pitfalls
**Tooltip on disabled buttons that don't fire hover.** HTML `disabled` blocks hover events. Use `aria-disabled` + visual disabled state instead.
**Tooltip blocks essential UI.** A tooltip in the wrong position covers the next button. Use auto-positioning + collision detection.
**Tooltip-as-instructions.** "Click here to save your work" — should be a button label, not a tooltip. Tooltip = supplemental, not essential.
**Tooltip with no delay.** Pops up the instant the mouse passes over. Annoying. Use a 300-700ms delay before showing.
**Tooltip with no exit delay.** User moves mouse 1px outside the trigger; tooltip vanishes. They re-enter; it appears again. Flicker. Use a 100-200ms exit delay.
**Tooltip covers itself on long content.** Wrapping is fine for 1-2 lines; for longer content, switch to a popover.
**Tooltip on mobile that fights the tap.** Tap shows tooltip + fires the button click simultaneously — both happen. Use long-press or touch-aware logic.
**Tooltip in modal that's clipped by overflow:hidden.** Modal has overflow: hidden; tooltip near the edge gets cut off. Use a portal that renders outside the modal.
**Tooltip with `<a>` or interactive content.** Pure tooltips shouldn't be interactive (you can't move your cursor to them on hover). If users need to click inside, use a popover instead.
**Tooltip with stale content.** Tooltip shows "3 unread"; the real count changes; tooltip text doesn't update. Bind tooltip content to live data.
**Tooltip shown on touch devices for icon-only buttons.** No hover; tooltip never appears; users can't discover what the icon does. Always include accessible label fallback (aria-label) for icon-only buttons.
**Tooltip text that's identical to the visible label.** Redundant — adds noise without value. Remove.
**Tooltip with no max-width.** A 500-character tooltip becomes a paragraph blob. Cap at ~250-300px.
**Forgotten dark-mode tooltip styling.** Tooltip is dark in light mode; light in dark mode. Make sure both modes look polished.
**Hardcoded English strings.** Internationalize tooltip content alongside other UI strings.
**Tooltip that breaks Radix's portal.** Tooltip rendered inside a `position: relative` container with `overflow: hidden` clipped at the boundary. Radix portals to body by default — don't override unless you need to.
**Animation that delays interaction.** Fancy "fade in over 500ms" causes users to wait for the tooltip. Keep animations under 200ms.
## Beyond Tooltips: When to Use What
| Need | Use |
|---|---|
| Quick label on icon-only button | Tooltip |
| Define a term ("ARR") | Tooltip |
| Why a button is disabled | Tooltip with reason |
| Multi-line explanation with link | Popover / Hint |
| Step-by-step product introduction | Onboarding tour |
| Persistent context-help button → opens panel | Popover or in-product help drawer |
| Critical info user MUST read | Inline text or modal |
| Empty-state explanation | Inline empty-state component |
| Validation error on form field | Inline error message under field |
| New-feature announcement | In-product release-note pattern |
## See Also
- [Onboarding Tour Implementation](./onboarding-tour-implementation-chat.md) — multi-step guided tours
- [In-Product Help Center / Knowledge Base](./in-product-help-center-knowledge-base-chat.md)
- [In-Product Release Notes / What's New](./in-product-release-notes-whats-new-chat.md)
- [Empty States, Loading & Error States](./empty-states-loading-error-states-chat.md)
- [Form Validation UX](./form-validation-ux-chat.md)
- [Toast Notifications UI](./toast-notifications-ui-chat.md)
- [In-App Status Banners & System Notifications](./in-app-status-banners-system-notifications-chat.md)
- [In-App Notifications](./in-app-notifications-chat.md)
- [Sidebar Navigation Implementation](./sidebar-navigation-implementation-chat.md)
- [Keyboard Shortcuts & Command Palette](./keyboard-shortcuts-command-palette-chat.md)
- [Settings & Account Pages](./settings-account-pages-chat.md)
- [Activity Feed / Timeline Implementation](./activity-feed-timeline-implementation-chat.md)
- [Inline Editing Patterns](./inline-editing-patterns-chat.md)
- [Date Pickers / Range Selection](./date-pickers-range-selection-chat.md)
- [Internationalization](./internationalization-chat.md)
- [Dark Mode Implementation](./dark-mode-implementation-chat.md)
- [Accessibility (VibeReference)](https://viberef.dev/product-and-design/accessibility.md)
- [shadcn/ui (VibeReference)](https://viberef.dev/frontend/shadcn.md)
- [Radix (VibeReference)](https://viberef.dev/frontend/radix.md)
- [Tailwind (VibeReference)](https://viberef.dev/frontend/tailwind.md)
- [Components (VibeReference)](https://viberef.dev/frontend/components.md)