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.

<script setup lang="ts">
import type { TooltipProps } from '@animate-ui/vue/primitives/tooltip'
import {
  Link01Icon,
  TextBoldIcon,
  TextItalicIcon,
  TextStrikethroughIcon,
  TextUnderlineIcon,
} from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/vue'
import {
  Tooltip,
  TooltipArrow,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@animate-ui/vue/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 },
]

withDefaults(
  defineProps<{
    openDelay?: number | string
    closeDelay?: number | string
    side?: TooltipProps['side']
    sideOffset?: number | string
    align?: TooltipProps['align']
    alignOffset?: number | string
    withTransition?: boolean | string
  }>(),
  {
    openDelay: 0,
    closeDelay: 300,
    side: 'top',
    sideOffset: 10,
    align: 'center',
    alignOffset: 0,
    withTransition: true,
  },
)

const on = (value: boolean | string) => value === true || value === 'true'
</script>

<template>
  <TooltipProvider :open-delay="Number(openDelay)" :close-delay="Number(closeDelay)">
    <div class="flex items-center gap-1 rounded-xl border p-1">
      <Tooltip
        v-for="tool in TOOLS"
        :key="tool.label"
        :side="side"
        :side-offset="Number(sideOffset)"
        :align="align"
        :align-offset="Number(alignOffset)"
      >
        <TooltipTrigger as-child>
          <Button variant="ghost" size="icon" :aria-label="tool.label">
            <HugeiconsIcon :icon="tool.icon" :stroke-width="2" />
          </Button>
        </TooltipTrigger>
        <TooltipContent class="rounded-md bg-foreground px-3 py-1.5 text-xs text-background">
          <TooltipArrow class="size-2.5 fill-foreground" :with-transition="on(withTransition)" />
          <p class="flex items-center gap-2 whitespace-nowrap">
            {{ tool.label }}
            <span class="opacity-60">{{ tool.shortcut }}</span>
          </p>
        </TooltipContent>
      </Tooltip>
    </div>
  </TooltipProvider>
</template>

Installation

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

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

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

Usage

<script setup lang="ts">
import {
  Tooltip,
  TooltipArrow,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@animate-ui/vue/primitives/tooltip';
</script>

<template>
  <TooltipProvider>
    <Tooltip>
      <TooltipTrigger as-child>
        <button aria-label="Bold">B</button>
      </TooltipTrigger>
      <TooltipContent class="rounded-md bg-foreground px-3 py-1.5 text-xs text-background">
        <TooltipArrow class="size-2.5 fill-foreground" />
        <p>Bold</p>
      </TooltipContent>
    </Tooltip>

    <Tooltip side="bottom">
      <TooltipTrigger as-child>
        <button aria-label="Italic">I</button>
      </TooltipTrigger>
      <TooltipContent class="rounded-md bg-foreground px-3 py-1.5 text-xs text-background">
        <TooltipArrow class="size-2.5 fill-foreground" />
        <p>Italic</p>
      </TooltipContent>
    </Tooltip>
  </TooltipProvider>
</template>

Unlike the library tooltips, this one is Animate UI's own: no reka-ui 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 reka-ui tooltip, on a spring as quick as its own but a touch calmer (stiffness 400, damping 31: it goes about 2% past its size before it settles, reka-ui's 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 close-delay), the next one opens without the open-delay.
  • 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 as-child, 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
#default
slot-
id?
string-
open-delay?
number0
close-delay?
number300
transition?
Transition{ type: 'spring', stiffness: 400, damping: 31 }

Tooltip

PropTypeDefault
#default
slot-
side?
"top" | "bottom" | "left" | "right""top"
side-offset?
number10
align?
"start" | "center" | "end""center"
align-offset?
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
as-child?
booleanfalse
...props?
HTMLMotionProps<'div'>-

TooltipContent

PropTypeDefault
as-child?
booleanfalse
...props?
HTMLMotionProps<'div'>-

TooltipArrow

Place it inside TooltipContent.

PropTypeDefault
with-transition?
booleantrue
...props?
SVGAttributes-

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 content, getters for its side, alignment and offsets, and its id.

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

Last updated: 10/23/2026