Tooltip

Developer Experience (DX)

How developers use the Tooltip, with one consistent, typed API that you wrap around any trigger and that handles when to open, where and for whom.

Next, let's look at how developers use the Tooltip. You give it its text and wrap it around the trigger, which takes a few props from it, and it handles when to open, where and for whom.

import { Button } from "@/components/Button";
import { Tooltip } from "@/components/Tooltip";

export function SettingsButton() {
  return (
    <Tooltip label="Settings" type="label">
      {(trigger) => <Button {...trigger} label="Settings" variant="ghost" iconOnly leading={<SettingsIcon />} />}
    </Tooltip>
  );
}

Consistent

The Tooltip uses the same names as every other component in Open Components, so there's little new to learn:

NameKindWhat it means
labelPropThe text content, when it's plain text, as on the Button
typePropWhether its text is the trigger's label or a description
delayPropHow long the pointer rests on the trigger before it opens, in ms
placementPropThe side of the trigger it opens on: top, bottom, start or end
arrowPropDraws an arrow that points at the trigger

We recommend type="description", delay={500}, placement="top" and an arrow as the defaults. The Tooltip also leaves a few things out, on purpose:

  • There's no slot for its content, only label. A slot would invite links, buttons and icons, which nobody could reach (see Keep it short and plain).
  • There's no open prop. When a tooltip opens is what makes it accessible, so the Tooltip keeps it to itself, and something you'd want to show on your own terms is a Popover.
  • There's no color or size. A tooltip has no intent of its own, and its text is always small and short.
  • There's no disabled either. To have no tooltip, render the trigger without one.

Type-safe

Props and the trigger's bindings are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like placement="left", which isn't one of the Tooltip's placements, or a tooltip without a label.

PropTypeDefaultDescription
labelstringThe text it shows. Required, and plain text only
type"label" | "description""description"Whether it shows the trigger's own name, or describes it
delaynumber500How long the pointer rests on the trigger before it opens, in milliseconds
placement"top" | "bottom" | "start" | "end""top"The side of the trigger it opens on, before it flips
arrowbooleantrueDraws an arrow that points at the trigger, on whichever side it opens
SlotDescription
defaultThe trigger, which binds trigger on its focusable element

The trigger's bindings are typed as TooltipTrigger: aria-describedby for a description, the anchor's name in style, and the pointer and focus listeners. Bind them all, on the element that takes focus. The Tooltip fires no events of its own. We also export TooltipProps, which comes in handy for wrappers:

import { Tooltip, type TooltipProps } from "@/components/Tooltip";

export function Hint(props: TooltipProps) {
  return <Tooltip delay={300} {...props} />;
}

Composable

The Tooltip adds no wrapper around its trigger. It renders the trigger you give it, then the tooltip right after it, so your layout, your flex gaps and your trigger's own attributes stay as they are. How the trigger takes its props is the one thing that differs from framework to framework:

FrameworkThe trigger
ReactA function child, which gets trigger to spread on the element
VueThe default slot, with trigger to bind with v-bind
SvelteA children snippet, which gets trigger to spread
AngularThe element the appTooltip directive is on, which adds the tooltip after it, with tooltipType, tooltipDelay, tooltipPlacement, tooltipArrow and tooltipClass as its other inputs
SolidA function child, like React's
AstroThe default slot, which a <script> wires up in the browser, as the Button's Astro examples do
VanillaThe element right before the tooltip, which a script wires up

Bind the props on the element that takes focus, which is usually the trigger's root. Components like the Button pass them straight through, attributes and listeners alike, so binding them on the Button is enough. If your trigger's root isn't the focusable element, like a card with a button inside it, bind them on the button instead.

With other components

  • An icon-only Button keeps its visually hidden label, and its tooltip shows that same label, with type="label".
  • A Button with focusableWhenDisabled stays focusable while it's disabled, so its tooltip can still say why (see Disabled triggers).
  • An Input is described by its hint, which stays on the page rather than in a tooltip, since people need it while they type.
  • A Dialog or Popover around a Tooltip keeps its own Escape handling. The Tooltip only takes Escape while it's open, and stops it there.
  • A toggletip, which shows a hint when a button is pressed, and anything with links or buttons inside, will be a Popover, on our Roadmap.

Controllable

The Tooltip keeps its open state to itself, since when it opens is what makes it work for every input. Everything else is within reach:

WhatHow you control it
Textlabel
Typetype
Delaydelay, for the pointer. Focus always opens it at once
Placementplacement, before it flips
Arrowarrow, on by default, and its size with --tooltip--arrow--size
ShownRender the Tooltip around the trigger, or render the trigger alone
LookIts variables, from a class
OpenRead data-state, which is open or closed

DX Checklist

RuleLevelScopeRequirementBasisCheck
tooltip/trigger-propsMustComponentThe trigger is the element you render, which binds the props the Tooltip hands it, with no wrapper elementOpen ComponentsUnit test
tooltip/label-propShouldComponentIts text comes from label, the name every component uses for plain textOpen ComponentsType check
tooltip/attributes-on-tooltipShouldComponentClasses and attributes you pass land on the tooltip, so a class can set its variablesOpen ComponentsUnit test
Copyright © 2026