Animated tooltips
Five entrance animations: a pop, a flip, a swing, a type reveal and a glow-in. Each is self-contained, sized and opens on hover or keyboard focus.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/tooltips-animatedDependencies, the @ai2/tokens theme and the component file are installed together.
Copy the source
components/ui/tooltips-animated.tsx"use client"
import type * as React from "react"
import { cn } from "@/lib/utils"
/* Animated tooltip family: 5 bubbles with an entrance animation (CSS, driven by
group-hover/focus). Self-contained (no radix, no new deps). The positioning is on
a static outer layer; the inner layer carries the entrance transform. ALL
transform transitions are disabled with motion-reduce - under reduced motion the
bubble appears instantly/flat. Color comes ONLY from tokens. size = the bubble
padding/text. */
export type StyledSize = "sm" | "md" | "lg" | "xl"
const bubbleSize: Record<StyledSize, string> = {
sm: "px-2 py-1 text-xs",
md: "px-2.5 py-1.5 text-xs",
lg: "px-3 py-2 text-sm",
xl: "px-3.5 py-2.5 text-sm",
}
const positioner = "pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2"
const skin = "border border-border bg-popover text-popover-foreground"
interface Props {
size?: StyledSize
content: React.ReactNode
children: React.ReactNode
className?: string
}
function Shell({ children, bubble }: { children: React.ReactNode; bubble: React.ReactNode }) {
return (
<span data-slot="styled-tooltip" className="group relative inline-flex">
{children}
<span className={positioner}>{bubble}</span>
</span>
)
}
/* Pop: yayli olcekle patlayarak girer. */
export function PopTooltip({ size = "md", content, children, className }: Props) {
return (
<Shell
bubble={
<span
role="tooltip"
className={cn(
"block w-max max-w-56 origin-bottom scale-50 rounded-lg font-medium opacity-0 shadow-md transition-[opacity,transform] duration-(--motion-base) ease-[cubic-bezier(0.34,1.56,0.64,1)] group-hover:scale-100 group-hover:opacity-100 group-focus-within:scale-100 group-focus-within:opacity-100 motion-reduce:transition-none",
bubbleSize[size],
skin,
className
)}
>
{content}
</span>
}
>
{children}
</Shell>
)
}
/* Flip: X ekseninde donerek girer. */
export function FlipTooltip({ size = "md", content, children, className }: Props) {
return (
<Shell
bubble={
<span
role="tooltip"
className={cn(
"block w-max max-w-56 origin-bottom rounded-lg font-medium opacity-0 shadow-md transition-[opacity,transform] duration-(--motion-base) ease-(--motion-ease) [transform:perspective(400px)_rotateX(-85deg)] group-hover:opacity-100 group-hover:[transform:perspective(400px)_rotateX(0deg)] group-focus-within:opacity-100 group-focus-within:[transform:perspective(400px)_rotateX(0deg)] motion-reduce:transition-none",
bubbleSize[size],
skin,
className
)}
>
{content}
</span>
}
>
{children}
</Shell>
)
}
/* Swing: hafif donme ile sallanarak girer. */
export function SwingTooltip({ size = "md", content, children, className }: Props) {
return (
<Shell
bubble={
<span
role="tooltip"
className={cn(
"block w-max max-w-56 origin-top -rotate-6 rounded-lg font-medium opacity-0 shadow-md transition-[opacity,transform] duration-(--motion-base) ease-[cubic-bezier(0.34,1.56,0.64,1)] group-hover:rotate-0 group-hover:opacity-100 group-focus-within:rotate-0 group-focus-within:opacity-100 motion-reduce:transition-none",
bubbleSize[size],
skin,
className
)}
>
{content}
</span>
}
>
{children}
</Shell>
)
}
/* Type: the content opens as if being typed (width + opacity). */
export function TypeTooltip({ size = "md", content, children, className }: Props) {
return (
<Shell
bubble={
<span
role="tooltip"
className={cn(
"block max-w-0 overflow-hidden whitespace-nowrap rounded-lg font-medium opacity-0 shadow-md transition-[opacity,max-width] duration-(--motion-slow) ease-(--motion-ease) group-hover:max-w-56 group-hover:opacity-100 group-focus-within:max-w-56 group-focus-within:opacity-100 motion-reduce:transition-none",
bubbleSize[size],
skin,
className
)}
>
{content}
</span>
}
>
{children}
</Shell>
)
}
/* GlowIn: token isima ile birlikte belirir. */
export function GlowInTooltip({ size = "md", content, children, className }: Props) {
return (
<Shell
bubble={
<span
role="tooltip"
className={cn(
"block w-max max-w-56 rounded-lg font-medium opacity-0 shadow-none transition-[opacity,box-shadow] duration-(--motion-base) ease-(--motion-ease) group-hover:opacity-100 group-hover:shadow-[0_0_18px_-2px_color-mix(in_oklab,var(--color-info)_55%,transparent)] group-focus-within:opacity-100 group-focus-within:shadow-[0_0_18px_-2px_color-mix(in_oklab,var(--color-info)_55%,transparent)] motion-reduce:transition-none",
bubbleSize[size],
skin,
className
)}
>
{content}
</span>
}
>
{children}
</Shell>
)
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Variations
5 takes on the same idea. Each is its own export, and every one accepts a size prop (sm, md, lg, xl) aligned to the base Button scale.
Pop
Pops in with a scale spring.
import { PopTooltip } from "@/components/ui/tooltips-animated"
<PopTooltip content="Pops in">
<button>Hover me</button>
</PopTooltip>Flip
Flips in on the X axis.
import { FlipTooltip } from "@/components/ui/tooltips-animated"
<FlipTooltip content="Flips in">
<button>Hover me</button>
</FlipTooltip>Swing
Swings in with a slight rotate.
import { SwingTooltip } from "@/components/ui/tooltips-animated"
<SwingTooltip content="Swings in">
<button>Hover me</button>
</SwingTooltip>Type
The content reveals like typing.
import { TypeTooltip } from "@/components/ui/tooltips-animated"
<TypeTooltip content="Reveals like typing">
<button>Hover me</button>
</TypeTooltip>Glow in
Fades in with a token glow.
import { GlowInTooltip } from "@/components/ui/tooltips-animated"
<GlowInTooltip content="Fades in with a glow">
<button>Hover me</button>
</GlowInTooltip>ai2 Animated tooltips: 5 styled variations on the token system
The ai2 Animated tooltips are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around tooltip entrance animations. They are free and MIT licensed, and every color comes from a semantic token, so they theme with the rest of ai2 in light and dark.
Motion runs on framer-motion: framer-motion and token CSS transitions drive the pop, flip, swing, type and glow. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, the entrances fall back to a plain fade or instant show.
What is in the ai2 Animated tooltips?
5 exports in one file: Pop, Flip, Swing, Type and Glow in. Each renders a native button and takes a size prop (sm, md, lg, xl) aligned to the base Button. They are separate from the base Button on purpose: the base keeps its clean variant, tone and size axes, while the styled layer carries the effects.
You own the file. Copy the one category file and you have all 5 variations, with no runtime dependency on ai2 itself.
Why use it
- On-system by construction: Every color resolves to an ai2 semantic token, so the buttons follow your theme in light and dark with no extra work.
- Effect without the sprawl: The decorations live in a dedicated styled file, so the base Button keeps its clean, predictable API.
- Accessible and honest: Each renders a real button element, keeps a visible focus ring, and respects prefers-reduced-motion.
Features
- Token-driven color: No hardcoded hex or oklch; the look recolors with your theme tokens.
- framer-motion: framer-motion and token CSS transitions drive the pop, flip, swing, type and glow.
- Reduced-motion aware: Under prefers-reduced-motion, the entrances fall back to a plain fade or instant show.
- Size aligned to the base: Every variation takes sm, md, lg and xl matching the base Button height scale, so styled and base buttons line up in a row.
Production tips
- Use it for emphasis, not everywhere: Styled buttons draw the eye. Reserve them for the one action you want people to take on a screen, and use the base Button for the rest.
- Keep labels as verbs: The decoration adds weight, so a clear action label keeps the button scannable.
- Pick one variation per surface: The variations share a family; using two different ones in the same view competes for attention.
Works with the rest of ai2
The Animated tooltips sit alongside the base Button and the rest of the @ai2 registry. They share the same token file, so a styled action next to a base button or a badge stays visually consistent in both modes.