Tooltip
A small label that names the control under your finger.
A small label that names the thing under your finger.
A popover is a panel you open and deal with; a tooltip is a whisper. It carries a word or two about what a control does, appears without taking the screen, and goes away on its own. That is why it is inverted rather than surface-coloured, why it is not dismissed with a scrim, and why it hides itself after a beat instead of waiting to be told.
On touch there is no hover to open it, so the gesture is a long press by default — the platform's own "tell me more" — with openOn="press" for the cases where a tap should reveal it instead. Placement is a preference, not a promise: placement="top" means above, if above fits, and a trigger near the top edge shows its tooltip below.
Installation
Tooltip ships with the library — no separate install.
import { Tooltip, Button, Text, InfoIcon } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add tooltipUsage
<Tooltip label="Copy link">
<Tooltip.Trigger>
<Button variant="outline">Press and hold</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Arrow />
Copy link
</Tooltip.Content>
</Tooltip>Composition
<Tooltip>
<Tooltip.Trigger>…</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Arrow />
<Tooltip.Text>…</Tooltip.Text>
</Tooltip.Content>
</Tooltip>Tooltip.Trigger— Wraps a single child and reveals the label on it — a long press by default, a press when the root asks for one. It is also what gets measured, so the label knows where to sit.Tooltip.Content— The label. Portaled above everything else, positioned against the trigger, and flipped or slid to stay inside the safe area. A string child is wrapped inTooltip.Textfor you.Tooltip.Arrow— Optional point towards the trigger. Follows the resolved side, so it stays correct after a flip.Tooltip.Text— The label's default text, sized and inverted against the dark surface. Compose it yourself when the label holds more than a single run of text.
Examples
Naming an icon-only control
An icon button has no text for a screen reader to read. Pass label on the root and it becomes the trigger's accessibility label, so the control is announced without anyone having to open the tooltip. openOn="press" reveals it on a tap here, since the icon has no other press to protect.
<Tooltip openOn="press" label="More information">
<Tooltip.Trigger>
<Button variant="ghost" size="icon" accessibilityLabel="Info">
<InfoIcon size={20} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Arrow />
Syncs every 15 minutes
</Tooltip.Content>
</Tooltip>Choosing a side
placement picks the preferred side; it is flipped only when that side genuinely has less room than its opposite. align and alignOffset move the label along the other axis, and offset changes the gap to the trigger.
<Tooltip.Content placement="right" align="start">
<Tooltip.Arrow />
Opens to the right
</Tooltip.Content>Staying up until dismissed
The label hides itself after duration milliseconds. Pass duration={0} to keep it up until a tap outside — or the trigger again — dismisses it, which is what you want when it carries something worth reading twice.
<Tooltip openOn="press" duration={0}>
<Tooltip.Trigger>
<Button variant="secondary">Keyboard shortcut</Button>
</Tooltip.Trigger>
<Tooltip.Content className="flex-row items-center gap-2">
<Tooltip.Arrow />
<Tooltip.Text>Save</Tooltip.Text>
<View className="rounded bg-background/20 px-1.5 py-0.5">
<Text size="xs" weight="semibold" className="text-background">⌘S</Text>
</View>
</Tooltip.Content>
</Tooltip>Controlled
Drive it yourself with open and onOpenChange — to reveal a hint from somewhere other than its trigger, or to coordinate it with the rest of a screen.
const [open, setOpen] = useState(false);
<Tooltip open={open} onOpenChange={setOpen}>
<Tooltip.Trigger>
<Button variant="outline">Save</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Arrow />
Saved just now
</Tooltip.Content>
</Tooltip>API Reference
Tooltip
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state. |
onOpenChange | (open: boolean) => void | — | |
defaultOpen | boolean | false | Initial state when uncontrolled. |
openOn | TooltipOpenOn | 'longPress' | Whether a long press or a plain press reveals the label. Long press is the default because it does not steal a tappable control's own press. |
duration | number | DEFAULT_DURATION | How long the label stays up before hiding itself, in milliseconds. 0 keeps it up until it is dismissed by a tap outside or the trigger again. |
label | string | — | The label's text, mirrored onto the trigger as its accessibility label so a screen reader announces what the tooltip says without opening it. Set it whenever the trigger has no text of its own — an icon-only button. |
Tooltip.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
onPress | (...args: unknown[]) => void | — | |
onLongPress | (...args: unknown[]) => void | — | |
accessibilityLabel | string | — | |
accessibilityHint | string | — |
Tooltip.Content
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
placement | TooltipPlacement | 'top' | Preferred side of the trigger. Flipped when that side does not fit. |
align | TooltipAlign | 'center' | Where the label sits along the trigger's other axis. |
offset | number | DEFAULT_OFFSET | Gap between the trigger and the label, in pixels. |
alignOffset | number | 0 | Nudge along the alignment axis, in pixels. |
Tooltip.Arrow
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
Placement is resolved, not obeyed. The side is flipped only when the preferred one genuinely has less room than its opposite; after that the label is slid along the other axis to stay inside the safe area. That order matters — sliding first would let a badly placed label look like it fits.
The label is measured before it is shown. Its size is not known until it has laid out once, so the first frame is laid out off-screen and the entrance starts from the frame after. That entrance is driven by hand rather than by a preset, because it has to start from whichever side was resolved.
The arrow points at the trigger, not the label. When align shifts the label off-centre, or a clamp slides it back on screen, the arrow tracks the trigger's centre rather than the label's middle.
The trigger is measured on every open, not once on layout — a trigger inside a scroll view has moved since it was laid out, and a stale rectangle anchors the label to where it used to be.
Long press is the default gesture. It does not steal a tappable control's own press, so a button under a tooltip still works as a button. Use openOn="press" only when the trigger has no press of its own to protect, such as an icon that exists to be explained.
Reach for a Popover instead when the content is interactive — a menu, a form, anything with its own controls. A tooltip is a label, not a surface, and it dismisses itself out from under a tap.