Tooltip
A tooltip with portal, arrow and directional slide/zoom animations - Tooltip wraps its own Provider, so a single import works out of the box.
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export default function TooltipDemo() {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipContent>Deploys in ~62ms</TooltipContent>
</Tooltip>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/tooltipDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install radix-ui@^1.6.1 tw-animate-css@^1.4.0Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/tooltip.tsx"use client"
import type * as React from "react"
import { Tooltip as TooltipPrimitive } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const tooltipContentVariants = cva(
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md bg-primary font-medium text-primary-foreground shadow-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 motion-reduce:animate-none",
{
variants: {
size: {
sm: "px-2 py-0.5 text-xs",
md: "px-2.5 py-1 text-xs",
},
},
defaultVariants: { size: "md" },
}
)
function TooltipProvider({
delayDuration = 200,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 6,
size,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content> &
VariantProps<typeof tooltipContentVariants>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
data-size={size ?? "md"}
sideOffset={sideOffset}
className={cn(tooltipContentVariants({ size, className }))}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2 translate-y-[calc(-50%_-_1px)] rotate-45 rounded-[1px] bg-primary fill-primary" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
tooltipContentVariants,
}Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the tone colors will be missing.
Usage
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipContent>Deploys in ~62ms</TooltipContent>
</Tooltip>Wrap the trigger with TooltipTrigger asChild to attach the tooltip to any element. The content renders in a portal and carries an arrow automatically.
Examples
Sides
Sizes
Shared provider
Wrap groups of tooltips in an explicit TooltipProvider to share timing - with delayDuration={0} adjacent tooltips open instantly as the pointer moves between triggers.
Props
Every part also forwards its underlying radix props - e.g. open / onOpenChange on Tooltip and side on TooltipContent.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "md" | Padding preset on TooltipContent: sm is px-2 py-0.5, md is px-2.5 py-1. |
sideOffset | number | 6 | Gap in pixels between the trigger and the tooltip (on TooltipContent). |
delayDuration | number | 200 | Hover delay in milliseconds before the tooltip opens (on TooltipProvider). |
ai2 Tooltip: hover and focus hints for React, zero setup
The ai2 Tooltip is a shadcn-compatible tooltip component for React, built on the radix-ui Tooltip primitive and styled with Tailwind CSS v4. It shows a short hint next to a control on hover or keyboard focus, rendered in a portal with an arrow and directional slide and zoom animations. Each Tooltip wraps its own provider with a 200ms delay, so a single import works with no app-level setup.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Tooltip?
It is a four-part composition: TooltipProvider, Tooltip, TooltipTrigger and TooltipContent. The anatomy matches shadcn/ui exactly, so existing snippets, muscle memory and AI agents keep working without changes. In everyday use you only import three parts; the provider is built into the root.
The content renders through a radix portal above everything else, adds a small rotated-square arrow automatically, and animates in from the side it is placed on. Styling uses the primary token pair for a high-contrast bubble, so the tooltip stays readable in both light and dark mode from day one, and a size prop trims the padding for dense toolbars (sm) or keeps the comfortable default inset (md).
Why use it
- Accessible by construction: The radix-ui Tooltip primitive opens on keyboard focus as well as hover, associates the content with the trigger for screen readers, and closes on Escape.
- Zero-setup provider: Tooltip wraps its own TooltipProvider with a 200ms delayDuration, so a single component works anywhere without wrapping your app first.
- Portal plus arrow, handled: Content renders in a portal at z-50 with a built-in arrow, so tooltips are never clipped by overflow containers and always point at their trigger.
- Directional animations: data-side drives a fade, zoom and 1-notch slide from the correct direction, powered by tw-animate-css utilities rather than a JS animation library.
- Agent-readable metadata: The registry item describes its parts and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its dependencies and the @ai2/tokens theme to your project.
- asChild trigger: TooltipTrigger forwards the radix asChild prop, so the tooltip attaches to your own Button, icon or link without an extra wrapper element.
- Four placement sides: TooltipContent accepts side="top" | "right" | "bottom" | "left" plus alignment and collision props from radix, with a 6px default sideOffset.
- Shared delay via provider: Wrap a toolbar in an explicit TooltipProvider to share timing; with delayDuration={0} adjacent tooltips open instantly as the pointer moves along.
- Controlled open state: The root forwards open, onOpenChange and defaultOpen, so you can drive a tooltip from React state when needed.
- Data attributes for styling: Every part exposes data-slot, and the content exposes data-state and data-side, so you can restyle placements from CSS without forking the component.
Production tips
- Keep tooltip text short: A tooltip is a label, not a paragraph. Aim for a few words like "View build logs"; if you need sentences or interactive content, use a Popover instead.
- Never hide essential info in tooltips: Tooltips do not open on tap-and-hold on many touch devices, so anything required to complete a task must also be visible in the UI itself.
- Use asChild on real controls: Attach the trigger to a focusable element like a Button so keyboard users can reach the tooltip. A tooltip on a plain div is invisible to them.
- Share a provider per toolbar: For rows of icon buttons, wrap the group in TooltipProvider with a low delayDuration so hints follow the pointer without a per-button wait.
- Do not duplicate visible labels: If the button already says Redeploy, a tooltip saying Redeploy is noise. Reserve tooltips for icon-only controls and genuine extra context.
Works with the rest of ai2
Tooltips explain compact controls across the registry. Attach one to an icon-only ai2 Button or ai2 Toggle via asChild, and show keyboard shortcuts inside the bubble with ai2 Kbd.
Pair it with an ai2 Avatar to reveal a user's full name on hover, and reach for the ai2 Popover when the content needs to be interactive or stay open. Everything shares one token source, so combinations stay visually consistent in both modes.