Tooltip

Tooltip shows a short hint on hover or focus. Use it to name icon-only controls, never for essential information.

  • Beta
  • Accessibility
  • Figma Tooltips 269:7358
  • packages/ui/src/components/Tooltip

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.

tsx
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

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react

Peer 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

tsx
// 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.

tsx
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

tsx
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 content and exactly one focusable child that forwards its ref, such as a Button. 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 · 20px
xs · 32px
lg · 272px wide
SizeFigmaBoxPaddingRadiusTypeTail (w × h, inset)
2xs2X-Small (24)20px tall2 / 64Paragraph/X Small8 × 4, 8
xsX-Small (34)32px tall4 / 10, gap 66Paragraph/Small (24px line)12 × 6, 12
lgLarge272px wide (96px tall with two lines)12, gap 1212Label/Small title, Paragraph/X Small description 4px below12 × 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
TokenLight (default)Dark
--nu-tooltip-bgBg/WhiteBg/Strong-950
--nu-tooltip-fgText/Strong-950Text/White
--nu-tooltip-sub (lg description)Text/Sub-600Text/Soft-400
--nu-tooltip-icon (lg icon)Icon/Sub-600Icon/Soft-400
--nu-tooltip-dismissIcon/Sub-600Icon/Sub-600 (as Figma; 2.68:1)
--nu-tooltip-ring1px outside Stroke/Soft-200none
--nu-tooltip-shadowregular-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}

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).

Figma Type (where the tail is)Code
Top Left · Top Center · Top Rightside="bottom" with align start · center · end
Bottom Left · Bottom Center · Bottom Rightside="top" with align start · center · end (Bottom Center is the default)
Rightside="left"
Leftside="right"

States and interactions

Each card has a live trigger. Try it with a mouse and with the keyboard.

ClosedNothing is rendered until the tooltip opens. Content is portalled to the end of the page.
Open on hoverOpens after delayDuration (700ms by default). Within 300ms of leaving one trigger, the next opens without the delay.
Open on focusTab to this button: keyboard focus opens the tooltip straight away. It closes when focus leaves.
HoverableMove the pointer from the trigger onto the tooltip: it stays open, so magnified text can be read (WCAG 1.4.13).
DismissedEscape closes any tooltip and focus stays on the trigger. lg can add a × control (dismissible), a pointer shortcut for Escape.
Not shown on touch
TouchTooltips don't open on touch screens, by design. On touch, people see only the trigger, so the trigger and visible text must carry the meaning.

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.

Spaced repetition

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.

Tooltip props
PropTypeDefaultDescription
contentReactNode—Simple API: the tooltip text (the description for lg). When set, children must be one focusable element that forwards its ref.
childrenrequiredReactNode—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).
arrowbooleantrueFigma Tail: the pointer toward the trigger.
titleReactNode—lg only. The bold first line; content becomes the description.
iconReactNode—lg only. Leading 20px icon (Figma default global-line). Rendered aria-hidden.
dismissiblebooleanfalselg 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.
sideOffsetnumbertail height + 4Distance from the trigger in px: 8 (2xs) or 10 with the tail, 4 without.
openboolean—Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void—Called when the tooltip wants to open or close (hover, focus, Escape, dismiss).
delayDurationnumberfrom provider (700)Hover delay in ms before opening. Focus always opens immediately.
repeatsLabelbooleanfalseSimple 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.
contentPropsOmit<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

TooltipContent props
PropTypeDefaultDescription
size'2xs' | 'xs' | 'lg''xs'As on Tooltip.
tone'light' | 'dark''light'As on Tooltip.
arrowbooleantrueAs on Tooltip.
titleReactNode—lg only. Replaces the native title attribute.
iconReactNode—lg only. Rendered aria-hidden.
dismissiblebooleanfalselg only. Closes the enclosing Tooltip.
side'top' | 'right' | 'bottom' | 'left''top'As on Tooltip.
align'start' | 'center' | 'end''center'As on Tooltip.
sideOffsetnumbertail height + 4As on Tooltip.
arrowPaddingnumbertail inset (8 / 12 / 16)Closest the tail gets to the corner, from Figma.
collisionPaddingnumber | Partial<Record<Side, number>>8Space kept from the viewport edge.
childrenReactNode—The text (or the description for lg).
refRef<HTMLDivElement>—Forwarded to the content element.
…restRadix 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

TooltipTrigger props
PropTypeDefaultDescription
asChildbooleanfalseMerges trigger behaviour into the single child (for example a Button) instead of rendering a <button>.
refRef<HTMLButtonElement>—Forwarded to the button (or the child with asChild).
…restButtonHTMLAttributes—Any native button attribute. The trigger gets data-state and, while open, aria-describedby.
TooltipProvider props
PropTypeDefaultDescription
childrenrequiredReactNode—The part of the app whose tooltips share timing.
delayDurationnumber700Hover delay in ms for every tooltip inside. A Tooltip’s own delayDuration wins.
skipDelayDurationnumber300Time in ms to reach another trigger and open it without the delay.
disableHoverableContentbooleanfalseWhen 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

KeyBehaviour
TabFocusing the trigger opens the tooltip immediately; moving focus away closes it.
EscapeCloses the tooltip. Focus stays on the trigger.
Enter / SpaceActivates 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 propertyFigma valuesCode
Size2X-Small (24) · X-Small (34) · Largesize: 2xs · xs · lg (default xs, Figma default 2X-Small)
Dark ModeOff · Ontone: light · dark
TypeTop / Bottom × Left / Center / Right · Right · Leftside + align (see Placement)
TailOff · Onarrow
Edit TextTextcontent (2xs, xs) or title (lg)
Edit DescriptionText (Large)content with size="lg"
Left Icon · Pick LeftOff · On · icon swapicon
Dismiss IconOff · Ondismissible

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

Share lesson
DoKeep it to a few words. A tooltip is read at a glance, on the way to clicking.
Share this lesson with everyone in your study space, including students who join later.
Don'tDon't write paragraphs. Long text is hard to read before it closes. Put it on the page.
Quiz closes Friday at 17:00
DoPut essential details in visible text. Touch users never see tooltips, and nobody should have to hover to find a deadline.
Closes Friday at 17:00
Don'tDon't hide what people need. The deadline only exists for people who hover over the icon.
Download lesson
DoName the action, as the label does. The tooltip and the aria-label say the same thing, so every user gets the same name.
Download lesson. View formats
Don'tDon't put actions inside. Tooltip content can't take focus, so a link inside it can't be reached by keyboard.

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-label from an icon-only button.
  • Setting disableHoverableContent, so the tooltip vanishes when people move the pointer to read it.