Skip to content
Nizivo UI

Tooltip

A short label shown on hover and keyboard focus, on an inverse surface. The trigger can be any focusable element, a server Button included.

Client componentEngine: react-aria

Import

import { Tooltip } from '@nizivo/react/tooltip';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/kbd.css
import '@nizivo/react/css/tooltip.css';

Static or interactive

Client. It always ships React Aria's tooltip and overlay code (+19,625 B gzip under Turbopack for one Tooltip on the Next.js fixture), once per page however many tooltips it has. Information a user needs belongs in visible text; for an icon-only action, IconButton pairs the tooltip for you. How to choose.

The trigger

Compose a Button into the trigger; it is still the recommended look:

<Tooltip.Trigger render={<Button variant="tertiary">Export</Button>} />

For an icon-only action, use an IconButton: it pairs itself with a Tooltip that shows its label, so it needs no Tooltip.Root.

A bare <Tooltip.Trigger> is a plain, unstyled <button>, but it has a minimum size of its own: at least 24 by 24 CSS pixels around a short text or a single glyph, the minimum target of WCAG 2.2 success criterion 2.5.8 (Target Size, Minimum), so two side by side still meet it. It also carries nz-hit-target, so under a coarse pointer it gets the same 44 px hit area as every standalone pressable. A trigger you render yourself through render keeps its own size and gets neither.

A trigger inside running text, such as a word in a sentence that shows a tooltip, is the exception: render your own element through render there. The 24 px minimum would make its line taller than the others, and under a coarse pointer the 44 px hit area would reach into the lines above and below and take taps meant for a link there. WCAG 2.2 2.5.8 exempts a target inside a sentence, so the inline element needs neither.

Examples

Basic

Compose a Button (or an IconButton) into Tooltip.Trigger: it brings the control look and the 40 px control height. The bare part is an unstyled <button> with only the WCAG 2.2 2.5.8 minimum of 24 x 24 px (and the 44 px hit area under a coarse pointer; M27).

import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Tooltip } from '@nizivo/react/tooltip';

export function TooltipExample(): JSX.Element {
  return (
    <Tooltip.Root>
      <Tooltip.Trigger render={<Button variant="tertiary">Export</Button>} />
      <Tooltip.Popup side="end">Download the report as CSV</Tooltip.Popup>
    </Tooltip.Root>
  );
}

Shortcut

content renders the Popup for you, and shortcut draws the keys with Kbd: ⌘E on Apple devices once ThemeScript has run, Ctrl+E elsewhere.

import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Tooltip } from '@nizivo/react/tooltip';

export function TooltipShortcutExample(): JSX.Element {
  return (
    <Tooltip.Root content="Export the report" shortcut="mod+e">
      <Tooltip.Trigger render={<Button variant="secondary">Export</Button>} />
    </Tooltip.Root>
  );
}

Open

Open on the server; the trigger is a composed Button, as everywhere.

Source only

It opens on page load (defaultOpen), which would move focus on this page, so it is shown as source only.
import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Tooltip } from '@nizivo/react/tooltip';

export function TooltipOpenExample(): JSX.Element {
  return (
    <Tooltip.Root defaultOpen delay={0}>
      <Tooltip.Trigger render={<Button variant="secondary">Refresh</Button>} />
      <Tooltip.Popup>Refresh data</Tooltip.Popup>
    </Tooltip.Root>
  );
}

Parts

Use the parts through the namespace (Tooltip.Root) or by their flat names (TooltipRoot); both work from Server Components.

Tooltip.Root

Client component Renders no element of its own. Props: TooltipRootProps. Source: src/tooltip/tooltip.client.tsx.

NameTypeDefaultDescription
childrenrequiredReactNode–
contentReactNode–

Convenience for the common case: renders a Tooltip.Popup with this content after children, so <Tooltip.Root content="Export"> needs no Popup part. Leave it out to compose Tooltip.Popup yourself (for side, align or sideOffset).

defaultOpenboolean–
delaynumber400

Hover delay before opening, in ms (Figma: 400). Keyboard focus opens immediately.

disabledboolean–
onOpenChange(open: boolean, details: ChangeDetails<OpenReason>) => void–

details.reason is 'unknown' with React Aria, which reports no cause.

openboolean–
shortcutstring–

A shortcut for content, drawn by Kbd ("mod+s" is ⌘S on Apple platforms, Ctrl+S elsewhere).

Tooltip.Trigger

Without render, its own <button>: nz-hit-target gives it the 44 px coarse-pointer hit area of every standalone pressable, and tooltip.css gives that bare button a 24 x 24 px minimum (WCAG 2.2 2.5.8; M27). A composed Button or IconButton keeps its own size and classes.

Client component Renders data-slot="tooltip-trigger" with the class nz-tooltip-trigger. Props: TooltipTriggerProps. Source: src/tooltip/tooltip.client.tsx.

NameTypeDefaultDescription
childrenReactNode–
renderRenderProp<TooltipTriggerState>–

Any focusable element, for example render={<Button variant="tertiary">Save</Button>}.

  • Also takes every attribute of <button> except children. (Omit<ComponentPropsWithRef<"button">, "children">)

render takes an element, or, in client code, a function of the props and this state (TooltipTriggerState):

NameTypeDefaultDescription
openrequiredboolean–

Tooltip.Popup

Client component Renders data-slot="tooltip-popup" with the class nz-tooltip-popup. Props: TooltipPopupProps. Source: src/tooltip/tooltip.client.tsx.

NameTypeDefaultDescription
alignAlign
"start""center""end"
'center'
childrenrequiredReactNode–
containerElement | null–

Portal container. Defaults to the parent overlay, else document.body (§12.1 L2).

shortcutstring–

A keyboard shortcut drawn after the text with Kbd, in the keys syntax Kbd takes ("mod+k", "mod+shift+d"). It shows the Apple spelling where ThemeScript sets data-nz-platform="apple". It is a visual hint: the app implements the shortcut itself.

sideTooltipSide
"top""bottom""start""end"
'top'
sideOffsetnumber8

Distance from the trigger in px.

  • Also takes every attribute of <div> except children, role. (Omit<ComponentPropsWithRef<"div">, "children" | "role">)

Other exports

TooltipLabels type

import type { TooltipLabels } from '@nizivo/react/tooltip'

Strings the engine renders (§9.3 rule 7): React Aria's Tooltip renders none.

type TooltipLabels = Record<string, never>

TooltipPopupProps type

import type { TooltipPopupProps } from '@nizivo/react/tooltip'

The props of Tooltip.Popup, listed above.

TooltipPopupState type

import type { TooltipPopupState } from '@nizivo/react/tooltip'

NameTypeDefaultDescription
enteringrequiredboolean–
exitingrequiredboolean–
siderequiredSide
"top""right""bottom""left"
–
TooltipRootProps type

import type { TooltipRootProps } from '@nizivo/react/tooltip'

The props of Tooltip.Root, listed above.

TooltipSide type

import type { TooltipSide } from '@nizivo/react/tooltip'

Logical side; the resolved physical side is data-side on the popup.

type TooltipSide = "top" | "bottom" | "start" | "end"

TooltipTriggerProps type

import type { TooltipTriggerProps } from '@nizivo/react/tooltip'

The props of Tooltip.Trigger, listed above.

TooltipTriggerState type

import type { TooltipTriggerState } from '@nizivo/react/tooltip'

The render state of Tooltip.Trigger, listed above.

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
tooltip-popup
.nz-tooltip-popup
Tooltip.Popup
root (no render); forced colours: border
  • data-align: "start""center""end" ← align
    Resolved alignment
  • data-entering (present or absent)
    Enter transition running; the node stays mounted until it ends
  • data-exiting (present or absent)
    Exit transition running; the node stays mounted until it ends
  • data-nz-scheme: "inverse"
    Inverse surface scheme for the subtree
  • data-open (present or absent)
    Open
  • data-side: "top""right""bottom""left"
    Resolved physical side after flip and RTL; the side prop takes logical values
tooltip-trigger
.nz-tooltip-trigger
Tooltip.Trigger
root (takes render); interactive, focus: self
  • data-open (present or absent)
    Open

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-tooltip-bg--nz-bg-surface-raised
  • background on .nz-tooltip-popup
--nz-tooltip-fg--nz-text-primary
  • color on .nz-tooltip-popup
--nz-tooltip-radius--nz-radius-sm
  • border-radius on .nz-tooltip-popup
--nz-tooltip-shadow--nz-elevation-3
  • box-shadow on .nz-tooltip-popup
--nz-tooltip-shortcut-fg--nz-text-tertiary
  • color on .nz-tooltip-popup .nz-tooltip-shortcut

Layers: nz.components. Classes: .nz-hit-target, .nz-tooltip-popup, .nz-tooltip-shortcut, .nz-tooltip-trigger. Runtime variables: --nz-transform-origin.

Tokens this stylesheet reads (15)

--nz-bg-surface-raised, --nz-distance-lift, --nz-duration-fade, --nz-duration-fast, --nz-easing-enter, --nz-easing-exit, --nz-elevation-3, --nz-font-label-m, --nz-radius-sm, --nz-space-2, --nz-space-3, --nz-text-primary, --nz-text-tertiary, --nz-tracking-label-m, --nz-z-overlay

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • tooltip-trigger: draws the focus outline itself on keyboard focus.

In forced-colours mode these states keep a visible system colour: tooltip-popup (border).

Behaviour comes from the react-aria engine through Nizivo's adapter: focus management, ARIA wiring and keyboard handling are the engine's.

Pattern: WAI-ARIA Authoring Practices.

Keyboard

KeysAction
TabFocusing the trigger with the keyboard shows the tooltip at once; the trigger points at it with aria-describedby.
EscapeHides the tooltip; focus stays on the trigger.

Source: react-aria-components 1.22.1 TooltipTrigger (react-aria 3.53.1 useTooltipTrigger) through _engines/react-aria/tooltip.client.tsx. Showing on keyboard focus and hiding on Escape are checked by the Next fixture and by apps/docs/scripts/smoke.ts.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page