Tooltip

Animate UI's own tooltip: the tooltips of a provider share one popup that moves from trigger to trigger instead of closing and reopening.

"use client"

import {
  Link01Icon,
  TextBoldIcon,
  TextItalicIcon,
  TextStrikethroughIcon,
  TextUnderlineIcon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import {
  Tooltip,
  TooltipArrow,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
  type TooltipProps,
} from "@animate-ui/react/primitives/tooltip"

import { Button } from "@/components/ui/button"

const TOOLS = [
  { label: "Bold", shortcut: "⌘B", icon: TextBoldIcon },
  { label: "Italic", shortcut: "⌘I", icon: TextItalicIcon },
  { label: "Underline", shortcut: "⌘U", icon: TextUnderlineIcon },
  { label: "Strikethrough", shortcut: "⌘⇧X", icon: TextStrikethroughIcon },
  { label: "Insert link", shortcut: "⌘K", icon: Link01Icon },
]

export default function TooltipDemo({
  openDelay = 0,
  closeDelay = 300,
  side = "top",
  sideOffset = 10,
  align = "center",
  alignOffset = 0,
  withTransition = true,
}: {
  openDelay?: number
  closeDelay?: number
  side?: TooltipProps["side"]
  sideOffset?: number
  align?: TooltipProps["align"]
  alignOffset?: number
  withTransition?: boolean
}) {
  return (
    <TooltipProvider openDelay={openDelay} closeDelay={closeDelay}>
      <div className="flex items-center gap-1 rounded-xl border p-1">
        {TOOLS.map((tool) => (
          <Tooltip
            key={tool.label}
            side={side}
            sideOffset={sideOffset}
            align={align}
            alignOffset={alignOffset}
          >
            <TooltipTrigger asChild>
              <Button variant="ghost" size="icon" aria-label={tool.label}>
                <HugeiconsIcon icon={tool.icon} strokeWidth={2} />
              </Button>
            </TooltipTrigger>
            <TooltipContent className="rounded-md bg-foreground px-3 py-1.5 text-xs text-background">
              <TooltipArrow
                className="size-2.5 fill-foreground"
                withTransition={withTransition}
              />
              <p className="flex items-center gap-2 whitespace-nowrap">
                {tool.label}
                <span className="opacity-60">{tool.shortcut}</span>
              </p>
            </TooltipContent>
          </Tooltip>
        ))}
      </div>
    </TooltipProvider>
  )
}

Installation

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

The popup is positioned with Floating UI, a peer dependency of this primitive:

npm install @floating-ui/react
pnpm add @floating-ui/react
yarn add @floating-ui/react
bun add @floating-ui/react

Usage

import {
  Tooltip,
  TooltipArrow,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@animate-ui/react/primitives/tooltip';

export function Toolbar() {
  return (
    <TooltipProvider>
      <Tooltip>
        <TooltipTrigger asChild>
          <button aria-label="Bold">B</button>
        </TooltipTrigger>
        <TooltipContent className="rounded-md bg-foreground px-3 py-1.5 text-xs text-background">
          <TooltipArrow className="size-2.5 fill-foreground" />
          <p>Bold</p>
        </TooltipContent>
      </Tooltip>

      <Tooltip side="bottom">
        <TooltipTrigger asChild>
          <button aria-label="Italic">I</button>
        </TooltipTrigger>
        <TooltipContent className="rounded-md bg-foreground px-3 py-1.5 text-xs text-background">
          <TooltipArrow className="size-2.5 fill-foreground" />
          <p>Italic</p>
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  );
}

Unlike the library tooltips, this one is Animate UI's own: no Radix UI, Base UI or React Aria underneath. TooltipContent renders nothing where it is written: it declares what the provider's single popup shows when its trigger is hovered or focused. Its content is laid out in a layer inside the popup (which takes the popup's display, flex and grid layout), and its TooltipArrow is drawn by the popup itself, outside that layer. The layer keeps the content from stretching while the popup resizes between triggers: no layout prop is needed on it (one would correct it twice).

Animation

The motion of Animate UI v1's Tooltip, on a spring a touch calmer than the library tooltips':

  • The popup grows from 0.7 and fades in from the point facing its trigger, like the Radix UI, Base UI and React Aria tooltips, on a spring as quick as theirs but a touch calmer (stiffness 400, damping 31: it goes about 2% past its size before it settles, theirs about 4%), and goes back the same way when it closes.
  • Hovering the next trigger while a tooltip is showing doesn't close it: the popup and its arrow glide to the new trigger and resize to the new content (a shared layout animation), on the same spring. Right after a tooltip closes (within closeDelay), the next one opens without the openDelay.
  • Meanwhile its content changes in the way the popup goes, like a tab panel: the previous label leaves the other way into a 6px blur, fading out and sliding 40px in 0.13s, and the next one slides 40px in from the side the popup goes to, out of the blur, showing from the start of its slide (0.34s, so its slide outlasts the popup's glide, which goes the other way on screen). Moving right, the previous label leaves to the left and the next comes from the right; moving up, the previous one goes down and the next comes from above. It follows the triggers on screen, so right to left and stacked triggers too. Neither label is ever stretched by the popup resizing: both are shown at their size, around the point the popup grows from, and clipped by the popup. The arrow only glides. Going back and forth faster than that, a label coming back goes on from where it is, and the one that was coming in leaves from where it is. Nothing of this on the first open or the close.
  • With asChild, the same happens inside the child, which is the popup.
  • <AnimateUIProvider tempo> paces the spring and the swap.
  • Under reduced motion, the popup only fades, and moving between triggers jumps, its content changing at once.

API Reference

TooltipProvider

Holds the shared popup. Every Tooltip must be inside one.

PropTypeDefault
children
React.ReactNode-
id?
string-
openDelay?
number0
closeDelay?
number300
transition?
Transition{ type: 'spring', stiffness: 400, damping: 31 }

Tooltip

PropTypeDefault
children
React.ReactNode-
side?
"top" | "bottom" | "left" | "right""top"
sideOffset?
number10
align?
"start" | "center" | "end""center"
alignOffset?
number0

TooltipTrigger

Opens the tooltip on hover and focus, and closes it on leave, blur, a press, Escape, scroll and resize. While open, it gets aria-describedby pointing at the popup (role="tooltip").

PropTypeDefault
asChild?
booleanfalse
...props?
HTMLMotionProps<'div'>-

TooltipContent

PropTypeDefault
asChild?
booleanfalse
...props?
HTMLMotionProps<'div'>-

TooltipArrow

Place it inside TooltipContent.

PropTypeDefault
withTransition?
booleantrue
...props?
Omit<React.ComponentProps<typeof motion.create(FloatingArrow)>, 'context'>-

useGlobalTooltip, useTooltip

Return the provider's and the tooltip's state: the current tooltip, showTooltip, hideTooltip, hideImmediate, the transition and the reference element; the tooltip's props, side, alignment, offsets and id.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page