Rich tooltips
Five multi-part tooltips: a title, an icon, a keyboard chip, a media thumbnail and a menu list. 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-richDependencies, the @ai2/tokens theme and the component file are installed together.
Copy the source
components/ui/tooltips-rich.tsx"use client"
import type * as React from "react"
import { cn } from "@/lib/utils"
/* Rich tooltip family: 5 bubbles with multi-part content. 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.5 text-xs",
md: "px-2.5 py-2 text-xs",
lg: "px-3 py-2.5 text-sm",
xl: "px-3.5 py-3 text-sm",
}
const bubbleBase =
"pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 w-max max-w-64 -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>
)
}
/* Title: kalin baslik satiri + altinda icerik. */
export function TitleTooltip({ size = "md", content, children, className, title }: Props & { title?: React.ReactNode }) {
return (
<Wrapper
bubble={
<span role="tooltip" className={cn(bubbleBase, bubbleSize[size], "flex flex-col gap-0.5 border border-border bg-popover text-popover-foreground", className)}>
{title ? <span className="font-semibold text-foreground">{title}</span> : null}
<span className="text-muted-foreground">{content}</span>
</span>
}
>
{children}
</Wrapper>
)
}
/* Icon: icerigin yaninda one alinan ikon yuvasi. */
export function IconTooltip({ size = "md", content, children, className, icon }: Props & { icon?: React.ReactNode }) {
return (
<Wrapper
bubble={
<span
role="tooltip"
className={cn(bubbleBase, bubbleSize[size], "inline-flex items-center gap-1.5 border border-border bg-popover text-popover-foreground [&_i]:text-base [&_i]:leading-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-info", className)}
>
{icon ? <span className="inline-flex text-info">{icon}</span> : null}
{content}
</span>
}
>
{children}
</Wrapper>
)
}
/* Kbd: icerik + token klavye-tusu cipi. */
export function KbdTooltip({ size = "md", content, children, className, keys }: Props & { keys?: string }) {
return (
<Wrapper
bubble={
<span role="tooltip" className={cn(bubbleBase, bubbleSize[size], "inline-flex items-center gap-2 border border-border bg-popover text-popover-foreground", className)}>
{content}
{keys ? (
<kbd className="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[0.65rem] text-muted-foreground">
{keys}
</kbd>
) : null}
</span>
}
>
{children}
</Wrapper>
)
}
/* Media: kucuk token kucuk-resim blogu + icerik. */
export function MediaTooltip({ size = "md", content, children, className, thumb }: Props & { thumb?: React.ReactNode }) {
return (
<Wrapper
bubble={
<span role="tooltip" className={cn(bubbleBase, bubbleSize[size], "inline-flex items-center gap-2.5 border border-border bg-popover text-popover-foreground", className)}>
<span className="inline-flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-md border border-border bg-[linear-gradient(135deg,var(--color-surface-2),var(--color-surface-3))] text-muted-foreground [&_svg]:size-4">
{thumb}
</span>
{content}
</span>
}
>
{children}
</Wrapper>
)
}
/* Menu: icerik kucuk dikey liste olarak gosterilir. */
export function MenuTooltip({ size = "md", content, children, className, items }: Props & { items?: string[] }) {
return (
<Wrapper
bubble={
<span role="tooltip" className={cn(bubbleBase, bubbleSize[size], "flex flex-col gap-1 border border-border bg-popover text-popover-foreground", className)}>
{content ? <span className="font-semibold text-foreground">{content}</span> : null}
{items && items.length > 0 ? (
<span className="flex flex-col gap-0.5">
{items.map((item, i) => (
<span key={i} className="inline-flex items-center gap-1.5 text-muted-foreground">
<span className="size-1 shrink-0 rounded-full bg-info" />
{item}
</span>
))}
</span>
) : null}
</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.
Title
A bold title line above the content.
import { TitleTooltip } from "@/components/ui/tooltips-rich"
<TitleTooltip content="Tooltip body">
<button>Hover me</button>
</TitleTooltip>Icon
A leading icon next to the content.
import { IconTooltip } from "@/components/ui/tooltips-rich"
<IconTooltip content="Tooltip body">
<button>Hover me</button>
</IconTooltip>Keyboard
A token keyboard-key chip.
import { KbdTooltip } from "@/components/ui/tooltips-rich"
<KbdTooltip content="Tooltip body">
<button>Hover me</button>
</KbdTooltip>Media
A small token thumbnail with content.
import { MediaTooltip } from "@/components/ui/tooltips-rich"
<MediaTooltip content="Tooltip body">
<button>Hover me</button>
</MediaTooltip>Menu
Content shown as a small list.
import { MenuTooltip } from "@/components/ui/tooltips-rich"
<MenuTooltip content="Tooltip body">
<button>Hover me</button>
</MenuTooltip>ai2 Rich tooltips: 5 styled variations on the token system
The ai2 Rich tooltips are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around multi-part tooltip content. 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 fade and rise 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 Rich tooltips?
5 exports in one file: Title, Icon, Keyboard, Media and Menu. 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 fade and rise 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 Rich 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.