1 / 8City bike, Trek FX 2
New York · today, 2:20 PM
$320
Overlays
Tooltip
v1.0.0Added in 1.0.0
A React tooltip for icon buttons and cut-off text: shortcut hints, toolbar groups that skip the delay and a long press on touch.
Rest the pointer on a trigger and after 300ms the label fades in, growing a touch out of the trigger. Inside a TooltipGroup only the first one waits: glide along a toolbar and each next label swaps in at once. Tab to a trigger and its tooltip opens straight away; Escape, a click, a scroll or moving away closes it. The pointer can travel onto the tooltip without closing it. A shortcut shows after the text in the notation of your device (⌘C on a Mac, Ctrl+C elsewhere) and is announced as aria-keyshortcuts. Phones have no hover, so a long press shows the label instead: the button does not fire, the tooltip stays while your finger is down and for 1.5 seconds after, and the next touch dismisses it. Shortcut hints hide on touch screens. With reduced motion it only fades.
Tooltip is a free React component in the React overlay components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add tooltip, together with Kbd, useControllableState, useLongPress, useShortcut, Motion presets and 2 more and the npm packages @radix-ui/react-tooltip, @radix-ui/react-slot and motion.
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add tooltipImport Tooltip from @/components/ui/tooltip.
import { Copy, Heart, Share2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipGroup } from "@/components/ui/tooltip"
export function ListingActions({ onSave, onCopy }: { onSave: () => void; onCopy: () => void }) {
return (
<TooltipGroup>
<div className="flex items-center gap-1">
<Tooltip content="Save to favorites" shortcut="s">
<Button variant="ghost" size="icon" radius="full" aria-label="Save to favorites" onClick={onSave}>
<Heart />
</Button>
</Tooltip>
<Tooltip content="Share">
<Button variant="ghost" size="icon" radius="full" aria-label="Share">
<Share2 />
</Button>
</Tooltip>
<Tooltip content="Copy link" shortcut="mod+c" side="bottom" arrow>
<Button variant="ghost" size="icon" radius="full" aria-label="Copy link" onClick={onCopy}>
<Copy />
</Button>
</Tooltip>
</div>
</TooltipGroup>
)
}
// a value that only shows its tooltip when it is cut off
export function Address({ value }: { value: string }) {
return (
<Tooltip content={value} overflowOnly>
<span tabIndex={0} className="block max-w-40 truncate">
{value}
</span>
</Tooltip>
)
}contentdefault ""ReactNodeThe label: a few words for an icon button, or a sentence with size md. Empty renders the trigger alone, so a tooltip can come and go without re-mounting the button.
shortcutdefault ""stringA combo in mod+k notation ("s", "mod+c", "alt+n", "shift+mod+p"), shown after the text at 60% of its color and set as aria-keyshortcuts on the trigger. Display only: bind the key yourself with useShortcut. Hidden on touch screens.
childrenReactNodeThe trigger, rendered asChild: a Button, an <a>, or a span with tabIndex={0}. It must pass a ref to its element. A disabled <button> gets no pointer events, so wrap it in a focusable span. A plain string gets a focusable span of its own. It keeps its own data-slot and data-state; the tooltip's state is in data-tooltip-state.
tonedefault "neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"neutral is the inverted tooltip: black on a light page, near-white on a dark one. Other tones fill with their color, for a label that carries meaning (out of stock, verified).
colordefault ""stringAny CSS color. Replaces the tone; the text turns black or white by lightness. Leave empty to use the tone.
sizedefault "sm""sm" | "md"sm is one short centered line in 12px medium type, up to 256px wide. md is for a sentence: 13px, more padding, left aligned, up to 288px wide. Both shrink to the room left on screen, so a flipped tooltip wraps on a phone instead of running off the edge.
radiusdefault "md""none" | "sm" | "md" | "lg" | "full"Corner shape, scaled with the size: md is 8px on sm and 10px on md. full is a pill on one line and a soft capsule on a wrapped sentence.
arrowdefault falsebooleanA small arrow with a rounded tip in the tooltip's color, pointing at the center of the trigger and kept clear of the corners.
sidedefault "top""top" | "right" | "bottom" | "left"Where it opens. It flips to the opposite side and slides along the edge when there is no room.
aligndefault "center""start" | "center" | "end"Alignment along the trigger's edge.
sideOffsetdefault 6numberDistance from the trigger in px. The arrow adds its own height on top.
delayDurationdefault 300numberHover time in ms before it opens. Keyboard focus opens it at once. Inside a TooltipGroup the group's delay wins (the playground feeds this value to the group).
longPressdefault truebooleanOn touch screens a long press on the trigger shows the tooltip, with a light vibration where supported. The click that would follow is swallowed, so the button does not fire. Off: touch never shows it.
touchDurationdefault 1500numberHow long in ms it stays after the finger lifts from a long press. The next touch anywhere closes it sooner.
overflowOnlydefault falsebooleanOpens only while the trigger's text is cut off (truncate or line-clamp on the trigger or inside it), so a cell shows its full value only when it needs to. The playground's heart never overflows, so it stays quiet.
defaultOpendefault falsebooleanStarts open. The next hover elsewhere, click or scroll closes it.
disableddefault falsebooleanNever opens and closes if it is open. The trigger itself keeps working.
openbooleanControls it from outside, with onOpenChange. Handy for a confirmation like "Link copied" after a click.
onOpenChange(open: boolean) => voidCalled on hover, focus, long press, Escape, click and scroll. Not called for opens that are refused (disabled, overflowOnly with nothing cut off, a tap on a touch screen).
containerHTMLElement | nullWhere the portal mounts. document.body by default.
classNamesPartial<Record<"content" | "arrow" | "shortcut", string>>Extra classes for the tooltip body, the arrow svg and the shortcut Kbd.
classNamestringExtra classes for the tooltip body (the trigger is your child), merged last. Other Radix content props (alignOffset, collisionPadding, avoidCollisions, onEscapeKeyDown, aria-label) pass through.
Wrap a toolbar: the first tooltip waits delayDuration (300), then moving to the next within skipDelayDuration (300) opens it at once with a quick fade. Tooltips outside a group bring their own provider, so no setup is needed. Props: { delayDuration?: number; skipDelayDuration?: number; disableHoverableContent?: boolean; children }.
State and behavior for the parts: open, defaultOpen, onOpenChange, delayDuration, disabled, longPress, touchDuration, overflowOnly, shortcut, disableHoverableContent. Props: TooltipRootProps.
Wires hover, focus and long press onto its child (asChild) or its own <button>, and sets aria-keyshortcuts. Props: ComponentProps<"button"> & { asChild?: boolean }.
The portaled, animated body: side, align, sideOffset, arrow, tone, color, size, radius, container, classNames and the Radix content props. Renders the Root's shortcut after its children. Props: TooltipContentProps.
Four sides, three alignments, with the arrow. It flips and slides when there is no room.
Neutral is inverted in both themes; a tone or any CSS color for labels that carry meaning.
The combo in the device's notation after the text; the app binds it. Try the keys.
The trigger keeps its own data-state, so a pressed toggle stays pressed under its tooltip. The shortcuts work too.
13px, left aligned, up to 288px wide: the fee explained next to a price summary. Touch screens show the note inline instead.
overflowOnly shows the full text only when truncate actually cut it.
A disabled button gets no pointer events: a focusable span carries the tooltip. Touch screens also show the reason inline.
An icon-only photo toolbar. On touch a long press reads the label; touchDuration keeps it 2s.
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
Run npx wingo-ui@latest add tooltip. It also adds the Wingo UI items it builds on (Kbd, useControllableState, useLongPress, useShortcut, Motion presets and 2 more) and the npm packages @radix-ui/react-tooltip, @radix-ui/react-slot and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/tooltip.json.
1 / 8New York · today, 2:20 PM
$320
Hover the heart, then glide along the toolbar: the next labels open instantly. Press S to save.
Long press a button to read its label; the button does not fire.
delay 300ms · long press 1.5s