Interactive tooltips
Five tooltips with richer trigger reactions: a scale, an underline, a lift, a fade-scale and a delay. 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-interactiveDependencies, the @ai2/tokens theme and the component file are installed together.
Copy the source
components/ui/tooltips-interactive.tsx"use client"
import type * as React from "react"
import { cn } from "@/lib/utils"
/* Interactive tooltip family: 5 variations with richer trigger/bubble responses.
Self-contained (no radix, no new deps) - it opens on hover/focus with CSS, and the
transform transitions are protected with motion-reduce. Color comes ONLY from
tokens. size = the bubble padding/text. children = the trigger, content = the
tooltip 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 bubbleBase =
"pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 w-max max-w-56 -translate-x-1/2 translate-y-1 rounded-lg font-medium opacity-0 shadow-md transition-[opacity,transform] duration-(--motion-base) ease-(--motion-ease) group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none"
interface Props {
size?: StyledSize
content: React.ReactNode
children: React.ReactNode
className?: string
}
function Wrapper({ children, bubble }: { children: React.ReactNode; bubble: React.ReactNode }) {
return (
<span data-slot="styled-tooltip" className="group relative inline-flex">
{children}
{bubble}
</span>
)
}
/* Scale: acikken tetikleyici hafifce buyur. */
export function ScaleTooltip({ size = "md", content, children, className }: Props) {
return (
<span data-slot="styled-tooltip" className="group relative inline-flex">
<span className="inline-flex transition-transform duration-(--motion-base) ease-(--motion-ease) group-hover:scale-105 group-focus-within:scale-105 motion-reduce:transition-none">
{children}
</span>
<span role="tooltip" className={cn(bubbleBase, bubbleSize[size], "border border-border bg-popover text-popover-foreground", className)}>
{content}
</span>
</span>
)
}
/* Underline: the trigger gains a token underline while open. */
export function UnderlineTooltip({ size = "md", content, children, className }: Props) {
return (
<span data-slot="styled-tooltip" className="group relative inline-flex">
<span className="inline-flex underline decoration-transparent decoration-2 underline-offset-4 transition-[text-decoration-color] duration-(--motion-base) ease-(--motion-ease) group-hover:decoration-info group-focus-within:decoration-info motion-reduce:transition-none">
{children}
</span>
<span role="tooltip" className={cn(bubbleBase, bubbleSize[size], "border border-border bg-popover text-popover-foreground", className)}>
{content}
</span>
</span>
)
}
/* Lift: balon daha yuksek bir yoldan kalkarak girer. */
export function LiftTooltip({ size = "md", content, children, className }: Props) {
return (
<Wrapper
bubble={
<span
role="tooltip"
className={cn(
"pointer-events-none absolute bottom-full left-1/2 z-10 mb-3 w-max max-w-56 -translate-x-1/2 translate-y-4 rounded-lg font-medium opacity-0 shadow-lg transition-[opacity,transform] duration-(--motion-base) ease-(--motion-ease) group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none",
bubbleSize[size],
"border border-border bg-popover text-popover-foreground",
className
)}
>
{content}
</span>
}
>
{children}
</Wrapper>
)
}
/* FadeScale: balon opaklik + olcekle birlikte belirir. */
export function FadeScaleTooltip({ size = "md", content, children, className }: Props) {
return (
<Wrapper
bubble={
<span
role="tooltip"
className={cn(
"pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 w-max max-w-56 -translate-x-1/2 scale-90 rounded-lg font-medium opacity-0 shadow-md transition-[opacity,transform] duration-(--motion-base) ease-(--motion-ease) group-hover:scale-100 group-hover:opacity-100 group-focus-within:scale-100 group-focus-within:opacity-100 motion-reduce:transition-none",
bubbleSize[size],
"border border-border bg-popover text-popover-foreground",
className
)}
>
{content}
</span>
}
>
{children}
</Wrapper>
)
}
/* Delay: the transition starts later, for a considered feel. */
export function DelayTooltip({ size = "md", content, children, className }: Props) {
return (
<Wrapper
bubble={
<span
role="tooltip"
className={cn(
bubbleBase,
"duration-(--motion-slow) group-hover:delay-300 group-focus-within:delay-300",
bubbleSize[size],
"border border-border bg-popover text-popover-foreground",
className
)}
>
{content}
</span>
}
>
{children}
</Wrapper>
)
}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.
Scale
The trigger scales slightly while open.
import { ScaleTooltip } from "@/components/ui/tooltips-interactive"
<ScaleTooltip content="Scales the trigger">
<button>Hover me</button>
</ScaleTooltip>Underline
The trigger gets a token underline while open.
import { UnderlineTooltip } from "@/components/ui/tooltips-interactive"
<UnderlineTooltip content="Underlines the trigger">
<button>Hover me</button>
</UnderlineTooltip>Lift
The bubble lifts with a bigger travel.
import { LiftTooltip } from "@/components/ui/tooltips-interactive"
<LiftTooltip content="Lifts higher">
<button>Hover me</button>
</LiftTooltip>Fade scale
The bubble fades and scales in.
import { FadeScaleTooltip } from "@/components/ui/tooltips-interactive"
<FadeScaleTooltip content="Fades and scales in">
<button>Hover me</button>
</FadeScaleTooltip>Delay
The bubble reveals after a short delay.
import { DelayTooltip } from "@/components/ui/tooltips-interactive"
<DelayTooltip content="Considered reveal">
<button>Hover me</button>
</DelayTooltip>ai2 Interactive tooltips: 5 styled variations on the token system
The ai2 Interactive tooltips are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around tooltips with trigger reactions. 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: the bubbles and triggers respond on token CSS transitions when hovered or focused. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, the transitions are disabled and the bubble appears instantly.
What is in the ai2 Interactive tooltips?
5 exports in one file: Scale, Underline, Lift, Fade scale and Delay. 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: the bubbles and triggers respond on token CSS transitions when hovered or focused.
- Reduced-motion aware: Under prefers-reduced-motion, the transitions are disabled and the bubble appears instantly.
- 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 Interactive 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.