A short ink label that names an icon or adds a hint, shown on hover or keyboard focus, never on touch.

Basic

A short ink label that names an icon button or adds a hint to a control. It shows after the pointer rests on its trigger for 400ms, and at once on keyboard focus. It sits 12px above the trigger by default, with a small arrow, and flips to the other side when there is no room. Long text wraps at 20rem.

Example

Follows the page theme and corners
import { IconButton, Tooltip, TooltipTrigger } from '@snapshot-ui/react';
import { CircleHelp, Copy, Pencil, Share2 } from 'lucide-react';
import type { ReactElement } from 'react';

export default function TooltipBasic(): ReactElement {
  return (
    <div className="row" style={{ justifyContent: 'center', gap: 12 }}>
      <TooltipTrigger>
        <IconButton aria-label="Edit" variant="outline">
          <Pencil />
        </IconButton>
        <Tooltip>Edit</Tooltip>
      </TooltipTrigger>
      <TooltipTrigger>
        <IconButton aria-label="Duplicate" variant="outline">
          <Copy />
        </IconButton>
        <Tooltip>Duplicate</Tooltip>
      </TooltipTrigger>
      <TooltipTrigger>
        <IconButton aria-label="Share" variant="outline">
          <Share2 />
        </IconButton>
        <Tooltip>Share</Tooltip>
      </TooltipTrigger>
      <TooltipTrigger>
        <IconButton aria-label="Help" variant="outline" href="/docs">
          <CircleHelp />
        </IconButton>
        <Tooltip>Help</Tooltip>
      </TooltipTrigger>
    </div>
  );
}

With a shortcut

shortcut shows a key after the text, such as Mod K. Mod is Cmd on Apple platforms and Ctrl elsewhere. placement="end" puts the tip after the trigger, as a collapsed navigation rail shows its names.

Example

Follows the page theme and corners
import { IconButton, Tooltip, TooltipTrigger } from '@snapshot-ui/react';
import { Search, Settings } from 'lucide-react';
import type { ReactElement } from 'react';

export default function TooltipShortcut(): ReactElement {
  return (
    <div style={{ display: 'grid', gap: 12, justifyItems: 'start' }}>
      <TooltipTrigger>
        <IconButton aria-label="Search">
          <Search />
        </IconButton>
        <Tooltip placement="end" shortcut="Mod K">
          Search
        </Tooltip>
      </TooltipTrigger>
      <TooltipTrigger>
        <IconButton aria-label="Settings">
          <Settings />
        </IconButton>
        <Tooltip placement="end" shortcut="Mod ,">
          Settings
        </Tooltip>
      </TooltipTrigger>
    </div>
  );
}

Usage

Wrap the trigger and the tip in TooltipTrigger. The trigger must be focusable and take React Aria's hover and focus, as Button, IconButton and Link do.

  • A static element, such as an icon in a sentence, needs React Aria's Focusable around it.
  • A disabled control cannot take focus, so it gets no tip.
  • A tooltip never shows for touch, so nothing a touch user needs may live only in a tip.

The tip opens after 400ms of hover (delay) and closes 120ms after the pointer leaves (closeDelay), so the pointer can cross onto it. Once one tip has opened, the next opens at once for about half a second, so moving along a row of buttons shows each name without a wait. Escape, a press of the trigger, a scroll of a box around it, or a window resize closes it. isOpen, defaultOpen and onOpenChange control it. Set isDisabled while the label is visible anyway, as an expanded navigation rail does.

TooltipTrigger and Tooltip are client components, and a server component can render them, as the examples here do. A tip forced open with isOpen or defaultOpen renders only in the browser. Until the page hydrates, the server's HTML gives the trigger a description that points at a tip it does not hold.

A tip takes the theme, shape and density of its trigger, so one opened from a dark card is dark while the page stays light. See Theming.

Props

ref and className go on the tip, and className is merged after the library's classes. The rest of React Aria's tooltip props pass through.

closeDelay
number

Milliseconds after the pointer leaves before it closes, so the pointer can cross onto the tip.

Default: 120

defaultOpen
boolean

Whether the overlay is open by default (uncontrolled).

delay
number

Milliseconds of hover before it opens.

Default: 400

isDisabled
boolean

Whether the tooltip should be disabled, independent from the trigger.

isOpen
boolean

Whether the overlay is open by default (controlled).

onOpenChange
((isOpen: boolean) => void)

Handler that is called when the overlay's open state changes.

trigger
"focus" | "hover"

By default, opens for both focus and hover. Can be made to open only for focus.

Default: 'hover'

children
ReactNode

The text. It is the trigger's accessible description while open.

className
string | ((values: TooltipRenderProps & { defaultClassName: string | undefined; }) => string)

Goes on the tip, merged after the library's classes.

offset
number

The gap to the trigger, the arrow included.

Default: 12

placement
Placement

Default: 'top'

shortcut
string

A key hint after the text, such as "K" or "Mod K", drawn as a key on the tip.

Accessibility

A tooltip is supplementary. While open, it is the trigger's description, read after the trigger's name. It is never the only name of anything, so an icon button keeps its aria-label. The tip never takes focus.

On an icon button the tip repeats the aria-label, which a screen reader may read twice. This is not yet checked in VoiceOver and NVDA. If it reads badly, such tips will be hidden from assistive technology, since a repeated name adds nothing.

TabOpens the tip on focus.
EscapeCloses it.

Design rules

What keeps tooltips in the system.

Keep a tip to one short line.

Name the action on an icon button.

Put a key hint in shortcut, not in the text.

Never put essential information in a tip. Touch never shows it.

Never put a tip on a disabled control. It cannot take focus.

Never put interactive content inside a tip.

esc

↑ ↓ to move↵ to open