Skip to content
WANCRAFT

B.159Cards & Containers

Smart Tooltip

An accessible tooltip for hover and keyboard focus with flip / shift placement, a pointing arrow and an OS-style delay group.

NewReactMotionTailwindintermediate

Overview

Smart Tooltip does the small things that make tooltips feel native. It opens after a hover delay or straight away on keyboard focus, and once one tooltip is showing its neighbours open instantly, the way they do on desktop operating systems. It follows WCAG 1.4.13: you can move the pointer onto the bubble without it vanishing, Escape hides it without moving focus, and it stays until hover or focus leaves. The bubble renders in the top layer, flips to the opposite side when there is no room, shifts to stay inside the viewport and keeps its arrow pointed at the trigger, and it is wired to the trigger with aria-describedby. On touch screens a press-and-hold shows it. Tooltips are for short, non-interactive hints; anything with links or buttons belongs in a popover.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/smart-tooltip.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/smart-tooltip.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/smart-tooltip.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { SmartTooltip } from "@/components/ui/smart-tooltip";

export function SmartTooltipExample() {
  return (
    <SmartTooltip content="Copy the link to this page">
      <button className="rounded-lg border px-3 py-2">Copy link</button>
    </SmartTooltip>
  );
}

03Props

PropTypeDefaultDescription
contentReactNode—Required. Short, non-interactive tooltip content.
childrenReactElement—Required. One focusable trigger element; it receives aria-describedby.
side"top" | "bottom" | "left" | "right""top"Preferred side; flips when there is no room.
align"start" | "center" | "end""center"Alignment along the trigger; shifted to stay in the viewport.
delaynumber600Hover delay in ms (keyboard focus opens at once).
closeDelaynumber120Grace period in ms so the pointer can move onto the tooltip.
skipDelayDurationnumber300After a tooltip closes, others open instantly for this long (ms).
sideOffsetnumber8Gap between trigger and tooltip in px.
arrowbooleantrueShow the pointing arrow.
variant"contrast" | "surface" | "accent""contrast"Inverted, card-like or accent-filled bubble.
maxWidthnumber240Maximum width in px (also capped to the viewport).
longPressDelaynumber500Touch press-and-hold duration that shows the tooltip, in ms.
openboolean—Controlled open state.
defaultOpenbooleanfalseInitial state when uncontrolled.
onOpenChange(open: boolean) => void—Called when the tooltip wants to open or close.
disabledbooleanfalseNever show (and drop aria-describedby).
accentstring"#FF7448"Fill of the accent variant.
classNamestring—Extra classes for the bubble.

04Features

  • 01Opens on hover after a delay and at once on keyboard focus (not on mouse-click focus)
  • 02OS-style delay group: once one tooltip is open, neighbours open instantly and switch without re-animating
  • 03WCAG 1.4.13: hoverable, dismissible with Escape and persistent until hover / focus leaves
  • 04Top-layer bubble with flip, viewport shift and an arrow that keeps pointing at the trigger
  • 05Touch: press and hold shows it; the next tap elsewhere hides it
  • 06Contrast, surface and accent variants
motion · springinput · hoverinput · focusinput · keyboardinput · press

05Accessibility

  • The bubble is role="tooltip" and stays in the DOM, so the trigger's aria-describedby (merged with any existing value) always resolves.
  • Keyboard focus opens it immediately; Escape hides it without moving focus and it stays hidden until focus or the pointer leaves; blur closes it.
  • While a tooltip is visible, Escape closes only the tooltip (the key press is consumed, so a surrounding dialog stays open); with no tooltip visible, Escape passes through untouched.
  • The pointer can travel from the trigger onto the tooltip (close grace period) and the tooltip never times out while hovered or focused.
  • Clicking the trigger hides the tooltip like a native one; with prefers-reduced-motion it only fades.
  • Tooltips must not hold interactive content, and icon-only triggers still need their own aria-label — the tooltip adds a description, not a name.

06Limitations

  • Without Popover API support the bubble falls back to position: fixed, which transformed ancestors can offset.