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 tooltip

Usage

<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 in Tooltip.Text for 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

PropTypeDefaultDescription
openbooleanControlled open state.
onOpenChange(open: boolean) => void
defaultOpenbooleanfalseInitial state when uncontrolled.
openOnTooltipOpenOn'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.
durationnumberDEFAULT_DURATIONHow 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.
labelstringThe 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

PropTypeDefaultDescription
onPress(...args: unknown[]) => void
onLongPress(...args: unknown[]) => void
accessibilityLabelstring
accessibilityHintstring

Tooltip.Content

PropTypeDefaultDescription
classNamestring
placementTooltipPlacement'top'Preferred side of the trigger. Flipped when that side does not fit.
alignTooltipAlign'center'Where the label sits along the trigger's other axis.
offsetnumberDEFAULT_OFFSETGap between the trigger and the label, in pixels.
alignOffsetnumber0Nudge along the alignment axis, in pixels.

Tooltip.Arrow

PropTypeDefaultDescription
classNamestring

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.

On this page