Tooltip
Tooltip shows a short hint on hover or focus. Use it to name icon-only controls, never for essential information.
Beta until design confirms the default size, adds a shadow effect style and fixes the dark dismiss icon's 2.68:1 contrast (all pending a decision). Everything on this page is implemented and tested.
Preview
Hover or focus the button to open the tooltip. Change the controls to try every size, tone and placement; choose “pinned open” to inspect it without hovering. The code updates with the controls.
import { Button, Tooltip } from '@nuton/ui'
import { RiInformationLine } from '@remixicon/react'
<Tooltip
content="Reviews come back just before you would forget them."
>
<Button iconOnly appearance="stroke" aria-label="About spaced repetition">
<RiInformationLine />
</Button>
</Tooltip>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.
1. Add the packages
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactPeer dependencies: react and react-dom 18 or later. Tooltip is built on Radix Tooltip (D5), which @nuton/ui installs for you.
2. Load tokens and component styles once
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css' // component styles (uses the tokens)3. Optional: one provider near the root
Every Tooltip works on its own: without a provider it creates one. Add a single TooltipProvider when you want tooltips to share timing, so moving from one trigger to the next opens the next tooltip without the delay.
import { TooltipProvider } from '@nuton/ui'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<TooltipProvider delayDuration={500}>{children}</TooltipProvider>
</body>
</html>
)
}Usage
import { Button, Tooltip } from '@nuton/ui'
import { RiShareLine } from '@remixicon/react'
export function ShareLesson({ onShare }: { onShare: () => void }) {
return (
<Tooltip content="Share with your study space">
<Button iconOnly appearance="stroke" aria-label="Share lesson" onClick={onShare}>
<RiShareLine />
</Button>
</Tooltip>
)
}- The simple API takes
contentand exactly one focusable child that forwards its ref, such as aButton. The child is the trigger. - The default is xs, light, top, centre, with a tail. It opens after 700ms on hover and straight away on keyboard focus.
- Keep the text to a few words in sentence case, with no full stop for a fragment: “Share lesson”, “Download as PDF”.
- A tooltip describes its trigger. It never names it: icon-only buttons still need
aria-label. - Tooltips don't open on touch screens. Anything people must know goes in visible text.
Variants and sizes
Size, tone, tail and placement are independent props on Tooltip (and on TooltipContent in the compound API). Hover or focus each trigger, or use “Show all tooltips”.
Sizes
2xs · 20pxxs · 32pxlg · 272px wide<Tooltip size="2xs" side="right" content="Bookmark">
<Button iconOnly size="sm" appearance="stroke" aria-label="Bookmark lesson"><RiBookmarkLine /></Button>
</Tooltip>
<Tooltip size="xs" side="right" content="Download as PDF">
<Button iconOnly size="sm" appearance="stroke" aria-label="Download lesson"><RiDownload2Line /></Button>
</Tooltip>
<Tooltip size="lg" side="right" title="Offline access" icon={<RiInformationLine />} content="Downloaded lessons open without a connection.">
<Button iconOnly size="sm" appearance="stroke" aria-label="About offline access"><RiInformationLine /></Button>
</Tooltip>| Size | Figma | Box | Padding | Radius | Type | Tail (w × h, inset) |
|---|---|---|---|---|---|---|
2xs | 2X-Small (24) | 20px tall | 2 / 6 | 4 | Paragraph/X Small | 8 × 4, 8 |
xs | X-Small (34) | 32px tall | 4 / 10, gap 6 | 6 | Paragraph/Small (24px line) | 12 × 6, 12 |
lg | Large | 272px wide (96px tall with two lines) | 12, gap 12 | 12 | Label/Small title, Paragraph/X Small description 4px below | 12 × 6, 16 |
Box sizes exclude the tail and match Figma's geometry; Figma's labels (24, 34) include it. lg has a 20px icon, a 180px text column and a 24 × 24 dismiss control. One Figma variant (Top Left, Large) is 280px wide; code uses 272px like the others (pending a decision). Text wraps at 272px for 2xs and xs. Figma's default variant is 2X-Small, but code defaults to xs for legibility (pending a decision).
Tone
tone is Figma's Dark Mode property. It's a style choice per tooltip and doesn't follow the page theme (D8).
light (default)dark<Tooltip tone="light" content="Copy invite link">…</Tooltip>
<Tooltip tone="dark" content="Copy invite link">…</Tooltip>| Token | Light (default) | Dark |
|---|---|---|
--nu-tooltip-bg | Bg/White | Bg/Strong-950 |
--nu-tooltip-fg | Text/Strong-950 | Text/White |
--nu-tooltip-sub (lg description) | Text/Sub-600 | Text/Soft-400 |
--nu-tooltip-icon (lg icon) | Icon/Sub-600 | Icon/Soft-400 |
--nu-tooltip-dismiss | Icon/Sub-600 | Icon/Sub-600 (as Figma; 2.68:1) |
--nu-tooltip-ring | 1px outside Stroke/Soft-200 | none |
--nu-tooltip-shadow | regular-shadow/x-small + regular-shadow/medium | |
Figma's shadow is two unbound drop shadows (0 1 2 at 3%, 0 12 24 at 6%) with no effect style, so code composes the two nearest tokens. Design should add an effect style (pending a decision).
Tail
arrow (default)arrow={false}<Tooltip content="Bookmark">…</Tooltip>
<Tooltip arrow={false} content="Bookmark">…</Tooltip>With the tail, the tooltip sits 4px plus the tail height from the trigger (8px for 2xs, 10px otherwise); without it, 4px. Figma doesn't specify this gap (pending a decision).
Placement
side picks the edge of the trigger and align slides the tooltip along it. If there isn't room, the tooltip flips to the opposite side and stays 8px inside the viewport (collisionPadding).
<Tooltip side="top" content="Opens above">…</Tooltip>
<Tooltip side="right" content="Opens to the right">…</Tooltip>
<Tooltip side="bottom" content="Opens below">…</Tooltip>
<Tooltip side="left" content="Opens to the left">…</Tooltip>| Figma Type (where the tail is) | Code |
|---|---|
| Top Left · Top Center · Top Right | side="bottom" with align start · center · end |
| Bottom Left · Bottom Center · Bottom Right | side="top" with align start · center · end (Bottom Center is the default) |
| Right | side="left" |
| Left | side="right" |
States and interactions
Each card has a live trigger. Try it with a mouse and with the keyboard.
delayDuration (700ms by default). Within 300ms of leaving one trigger, the next opens without the delay.lg can add a × control (dismissible), a pointer shortcut for Escape.Tooltips on disabled buttons never open, because disabled elements get no pointer or focus events. Explain why an action is unavailable in visible text instead.
Composition examples
Icon-only actions with shared timing
The most common use: icon-only buttons each get a tooltip with the same words as their aria-label. One TooltipProvider around the group shortens the delay and lets people sweep across the row.
While open, the tooltip text is also linked as the button's description, so a screen reader may read “Edit lesson” twice. Whether matching tooltips should be hidden from assistive technology is pending a decision.
Explaining a term
A lg tooltip next to a heading gives a short definition without leaving the page. Keep it to a title and one sentence.
Compound API, controlled
Compose TooltipTrigger and TooltipContent when you need to control the open state or change the content. Here the tooltip confirms a copy; a visually hidden status message announces it, because tooltips aren't announced as they change.
API reference
Tooltip props
The root. With content it builds the trigger and content for you (simple API); without it, its children are the compound parts.
| Prop | Type | Default | Description |
|---|---|---|---|
content | ReactNode | — | Simple API: the tooltip text (the description for lg). When set, children must be one focusable element that forwards its ref. |
childrenrequired | ReactNode | — | The trigger (simple API) or TooltipTrigger + TooltipContent (compound API). |
size | '2xs' | 'xs' | 'lg' | 'xs' | Figma Size: 20px, 32px or the 272px rich tooltip. |
tone | 'light' | 'dark' | 'light' | Figma Dark Mode Off / On. Independent of the page theme (D8). |
arrow | boolean | true | Figma Tail: the pointer toward the trigger. |
title | ReactNode | — | lg only. The bold first line; content becomes the description. |
icon | ReactNode | — | lg only. Leading 20px icon (Figma default global-line). Rendered aria-hidden. |
dismissible | boolean | false | lg only. Shows the × control, a pointer shortcut for Escape. Hidden from assistive technology and skipped by Tab. |
side | 'top' | 'right' | 'bottom' | 'left' | 'top' | Preferred edge of the trigger. Flips when there isn’t room. |
align | 'start' | 'center' | 'end' | 'center' | Alignment along that edge. |
sideOffset | number | tail height + 4 | Distance from the trigger in px: 8 (2xs) or 10 with the tail, 4 without. |
open | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Initial open state when uncontrolled. |
onOpenChange | (open: boolean) => void | — | Called when the tooltip wants to open or close (hover, focus, Escape, dismiss). |
delayDuration | number | from provider (700) | Hover delay in ms before opening. Focus always opens immediately. |
repeatsLabel | boolean | false | Simple API only: the tooltip just repeats the trigger's accessible name (icon-only buttons, collapsed nav items), so it isn't linked as a description and the name is read once. |
contentProps | Omit<TooltipContentProps, visual props | 'children'> | — | Simple API only: extra props for the content, such as className, aria-label or collisionPadding. |
Tooltip renders no element of its own, so it takes no ref or native attributes.
TooltipContent props
| Prop | Type | Default | Description |
|---|---|---|---|
size | '2xs' | 'xs' | 'lg' | 'xs' | As on Tooltip. |
tone | 'light' | 'dark' | 'light' | As on Tooltip. |
arrow | boolean | true | As on Tooltip. |
title | ReactNode | — | lg only. Replaces the native title attribute. |
icon | ReactNode | — | lg only. Rendered aria-hidden. |
dismissible | boolean | false | lg only. Closes the enclosing Tooltip. |
side | 'top' | 'right' | 'bottom' | 'left' | 'top' | As on Tooltip. |
align | 'start' | 'center' | 'end' | 'center' | As on Tooltip. |
sideOffset | number | tail height + 4 | As on Tooltip. |
arrowPadding | number | tail inset (8 / 12 / 16) | Closest the tail gets to the corner, from Figma. |
collisionPadding | number | Partial<Record<Side, number>> | 8 | Space kept from the viewport edge. |
children | ReactNode | — | The text (or the description for lg). |
ref | Ref<HTMLDivElement> | — | Forwarded to the content element. |
…rest | Radix Tooltip.Content props | — | Every other Radix content prop and div attribute: aria-label (replaces the announced text), alignOffset, avoidCollisions, sticky, hideWhenDetached, onEscapeKeyDown, onPointerDownOutside, forceMount, className… |
TooltipTrigger and TooltipProvider props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merges trigger behaviour into the single child (for example a Button) instead of rendering a <button>. |
ref | Ref<HTMLButtonElement> | — | Forwarded to the button (or the child with asChild). |
…rest | ButtonHTMLAttributes | — | Any native button attribute. The trigger gets data-state and, while open, aria-describedby. |
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | — | The part of the app whose tooltips share timing. |
delayDuration | number | 700 | Hover delay in ms for every tooltip inside. A Tooltip’s own delayDuration wins. |
skipDelayDuration | number | 300 | Time in ms to reach another trigger and open it without the delay. |
disableHoverableContent | boolean | false | When true, tooltips close as the pointer leaves the trigger. Leave it false: hoverable content is required by WCAG 1.4.13. |
Styling hooks
The content renders .nu-tooltip with data-size, data-tone, and Radix's data-side, data-align and data-state. The parts are .nu-tooltip__icon, __text, __title, __description, __dismiss and __arrow. Use props rather than styling these. The supported override points are the component tokens in the Tone table plus --nu-tooltip-max-width, --nu-tooltip-lg-width(both 272px) and --nu-tooltip-lg-label-width (180px).
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Focusing the trigger opens the tooltip immediately; moving focus away closes it. |
| Escape | Closes the tooltip. Focus stays on the trigger. |
| Enter / Space | Activates the trigger as normal and closes the tooltip. |
Screen readers
While open, the content has role tooltip and is linked to the trigger with aria-describedby, so it's read as a description after the name. The lg icon and the × control are hidden.
Focus
Tooltip content never takes focus, so it can't hold links or buttons. The trigger keeps its own focus indicator (for Button, see C5).
Contrast and colour
Text passes AA in both tones (light description Sub-600 6.69:1; dark description Soft-400 7.19:1). The dark × icon stays Icon/Sub-600 on Strong-950, 2.68:1, below 3:1 (pending a decision). Forced-colours mode draws a system border.
Touch and pointer
Tooltips don't open on touch. The pointer can move onto the tooltip without it closing (WCAG 1.4.13). The × control is 24 × 24, the WCAG 2.5.8 minimum. Reduced motion turns off the opening animation.
You must: keep an aria-label on icon-only triggers; use a focusable trigger that forwards its ref; never put essential information or interactive content only in a tooltip.
Figma mapping
| Figma property | Figma values | Code |
|---|---|---|
| Size | 2X-Small (24) · X-Small (34) · Large | size: 2xs · xs · lg (default xs, Figma default 2X-Small) |
| Dark Mode | Off · On | tone: light · dark |
| Type | Top / Bottom × Left / Center / Right · Right · Left | side + align (see Placement) |
| Tail | Off · On | arrow |
| Edit Text | Text | content (2xs, xs) or title (lg) |
| Edit Description | Text (Large) | content with size="lg" |
| Left Icon · Pick Left | Off · On · icon swap | icon |
| Dismiss Icon | Off · On | dismissible |
Radius binds Figma's remote 04-Radius library; code uses the local --nu-radius-* twins (C19/C22 principle).
Guidelines and related components
Use Tooltip for
- Repeating the name of an icon-only button for sighted mouse and keyboard users.
- A short, optional hint: a keyboard alternative, a file format, what a setting affects.
- A one-sentence definition of a term (
lg).
Don't use Tooltip for
- Information people need to finish a task, such as a deadline or a validation rule. Use visible text or a field hint.
- Links, buttons or forms. Use a Modal or a Dropdown menu.
- Explaining why a button is disabled. The tooltip can't open on a disabled button.
Do and don't
Common mistakes
- Wrapping plain text or an icon in a tooltip. The trigger must be focusable, or keyboard users never see it.
- Wrapping a component that doesn't forward its ref. The tooltip can't position itself or attach its listeners.
- Relying on the tooltip as the accessible name and dropping
aria-labelfrom an icon-only button. - Setting
disableHoverableContent, so the tooltip vanishes when people move the pointer to read it.