Components/Overlay
Tooltip
Tooltips are short hints anchored to a control, shown when the reader hovers over it or moves focus to it. They carry a clarification the interface has no room to state outright, which means they can only ever hold something the reader could manage without.
import { Tooltip } from '@growth-protocol-ai/alpha';When to use
Reach for a tooltip when a control cannot say what it does in the room it has — a button carrying nothing but a glyph, a heading truncated to fit its column, an abbreviation the reader may not know. The hint arrives the moment the pointer or the focus ring lands, with no delay to wait through, so finding out costs nothing; what it must never cost is the information itself, because a reader who does not go looking never sees it.
When not to use
A tooltip is not a place to keep anything. It does not appear on a touch screen, it is gone the moment the trigger loses the pointer, and a reader searching the page will not turn it up — so what someone would be stuck without belongs on the page instead. When the content has to be read or acted on rather than glanced at, that is a Modal, which takes the page and waits until it is dealt with; when there is something inside it to click, it is a Dropdown, because a tooltip never takes the pointer and a link in one can never be reached.
variant3 values
One geometry, three fills, and the arrow is retinted by the same declaration as the body. The regular chip is a dark fill with light text in both themes, which is what makes it read as a layer over the page rather than a panel in it. What picks a different fill is the surface the hint lands on, not the theme; the other two are for the cases where inverting works against you. Nothing below is on screen until it is asked for, so hover a trigger, or tab into the row and watch the hint follow the focus ring.
| variant | When to use |
|---|---|
regular | The default, and the answer unless the surface underneath has already taken this colour. |
inverted | The opposite fill: light in a light theme, near-white in a dark one. For a hint on a dark card or over a modal backdrop, where the regular chip is the colour of what it is sitting on. |
additional | A mid grey, quieter than either. For secondary context beside a hint that matters more — not for a hint that matters less than the page. |
side4 values
Side is where the tooltip sits, which is what every positioning library means by it and the opposite of how the Figma variants are named — those name the direction the arrow points, so Up is the tooltip below its trigger and Down is the one above. Either way it is a preference rather than a guarantee: a trigger with no room on the side you asked for flips to the opposite one, and the arrow goes with it, which the specimens will do if you narrow the window while one is up.
| side | When to use |
|---|---|
top | Above the trigger. The default, and the one that stays out of the way of whatever the control acts on. |
right | To the right. The collapsed-sidebar hint — a rail of glyphs naming themselves without moving the column. |
bottom | Below the trigger. For a toolbar along the top of a screen, where above is off the edge. |
left | To the left. For a control in a right rail, for the same reason. |
| State | When to use |
|---|---|
Open | Closed, a tooltip is not hidden — it is unmounted, so there is nothing to find by search, nothing for a screen reader to reach and nothing at all on a touch screen. Open, the chip is portalled to the end of the document and painted over everything, and it takes no pointer events, so it cannot swallow a click meant for what it is covering. Hover the trigger below, or tab to it. |
One line. The chip is capped in width and will keep wrapping, so a hint that runs to a paragraph is not a hint: put that on the page beside the control, and give anything wanting a heading a page of its own.
Never put anything in a tooltip that is only there. It does not appear on touch, it is gone the moment the pointer leaves, and a reader searching the page will not find it.
The trigger has to be focusable, or the hint is for pointer users only — a bare span takes tabIndex={0}, and anything clickable should have been a button already. The chip itself never takes focus, which is what keeps it out of the tab order and leaves the reader where they were.
An icon-only trigger still needs its own accessible name. The hint is wired as aria-describedby, which is a description of a control that already has one, so a button described as “Copy link” and named nothing is still nameless.
It closes on Escape, when the trigger loses the pointer or the focus, when the page under it scrolls, and when another tooltip opens. Every one of those reports through onOpenChange and none of them moves a state you are holding, so a controlled tooltip that ignores the event cannot be dismissed at all.
Nothing interactive in the label. The chip takes no pointer events and closes when the trigger loses them, so a link inside one can never be reached; a glyph beside the words is the most it should ever hold.
A tooltip on a disabled control is a tooltip nobody can open — a disabled button takes neither the pointer nor the focus ring. If the reason a control is disabled needs explaining, that explanation goes on the page.
Neither of the other two fills is a way to add emphasis. Reach for one when the regular chip is the colour of the surface beneath it, or when a hint is secondary to another one on screen, and never because this hint matters more.
A rail of icon-only controls
The case a tooltip exists for, and the one place to drop the arrow: the hints sit to the right of the column rather than over the content it frames, and a stack of arrows down the edge of a rail reads as noise while saying nothing the position has not already said. Every button still carries its own name, because the hint is a description of a control and not a substitute for one.
Holding the open state yourself
Pass the open state and the tooltip shows what you say rather than what the pointer says, which is how a hint arrives off the back of something else happening. Every route out still reports back and none of them closes anything on its own, so a controlled tooltip that drops the event is one the reader cannot get rid of — press Escape on the one below and it goes, because the state it is told to follow went with it.
| Prop | Type | Default | Notes |
|---|---|---|---|
label | ReactNode | — | Required. What the tooltip says, and what the trigger is described by. A node rather than a string, so a glyph can sit beside the text. |
children | ReactNode | — | The trigger. Exactly one element, and it has to forward a ref and take the props the primitive puts on it — every Alpha control does. |
variant | "regular" | "inverted" | "additional" | "regular" | Which fill. Setting it changes the body and the arrow together, because a fill is a pair of custom properties rather than a pair of rules. |
side | "top" | "right" | "bottom" | "left" | "top" | Where it sits, before collision. Flips when there is no room. |
align | "start" | "center" | "end" | "center" | Where it sits along that side. The arrow tracks it, and both shift rather than overflow the window. |
arrow | boolean | true | Draw the arrow. Turn it off for a label-shaped hint — the name of a glyph needs no pointer to say which glyph it means. |
sideOffset | number | 8 | Gap between the trigger and whatever is nearest it — the arrow tip, or the body when there is no arrow. One value covers both, which is the reason it is set here rather than left to the primitive. |
collisionPadding | number | Partial<Record<TooltipSide, number>> | 8 | How close to the edge of the window it may come before it moves. A number pads every edge; an object pads the ones it names. |
open | boolean | — | Controlled open state. Pass it and the pointer and the focus ring no longer decide, so pair it with onOpenChange or nothing will close. |
defaultOpen | boolean | false | Open on mount, when uncontrolled. For a hint that has to be seen once without taking the state off the component. |
onOpenChange | (open: boolean) => void | — | Fires for every route in and out: the pointer, the focus ring, Escape, a scroll under the trigger, and another tooltip opening. |
tooltip | [data-slot] | — | The chip itself, portalled out of the trigger’s subtree — so this attribute, and the ones mirroring its variants beside it, are the only handle a consuming app is offered on it. |