A Radix tooltip with an arrow, side offset and fade and zoom transitions that opens on hover and keyboard focus. Works standalone or under a shared provider.
import { Bold, Italic, Link } from "lucide-react"
import { buttonVariants } from "@/components/ballmac/button"
import { Kbd, KbdGroup } from "@/components/ballmac/kbd"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ballmac/tooltip"
const tools = [
{ label: "Bold", icon: Bold, keys: ["⌘", "B"] },
{ label: "Italic", icon: Italic, keys: ["⌘", "I"] },
{ label: "Insert link", icon: Link, keys: ["⌘", "K"] },
]
export default function TooltipDemo() {
return (
<TooltipProvider>
<div role="toolbar" aria-label="Formatting" className="flex items-center gap-1 rounded-lg border bg-card p-1">
{tools.map(({ label, icon: Icon, keys }) => (
<Tooltip key={label}>
<TooltipTrigger className={buttonVariants({ variant: "ghost", size: "icon-sm" })} aria-label={label}>
<Icon />
</TooltipTrigger>
<TooltipContent>
{label}
<KbdGroup>
{keys.map((key) => (
<Kbd key={key} size="sm">
{key}
</Kbd>
))}
</KbdGroup>
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/tooltipInstall the dependencies.
$ pnpm add radix-uiCopy the source into your project.
components/ballmac/tooltip.tsx// Ballmac UI: Tooltip. https://ui.ballmac.com/components/tooltip // Based on shadcn/ui's Tooltip (MIT, Copyright (c) 2023 shadcn), restyled with an arrow and tw-animate-css transitions. "use client" import * as React from "react" import { Tooltip as TooltipPrimitive } from "radix-ui" import { cn } from "@/lib/utils" type TooltipProviderProps = React.ComponentProps<typeof TooltipPrimitive.Provider> & { /** Milliseconds from pointer enter until the tooltip opens. */ delayDuration?: number } function TooltipProvider({ delayDuration = 200, ...props }: TooltipProviderProps) { return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} /> } /** A tooltip. Wraps itself in a TooltipProvider, so it works standalone; add one provider higher up to share delays. */ 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} /> } type TooltipContentProps = React.ComponentProps<typeof TooltipPrimitive.Content> & { /** Show the pointer arrow. */ arrow?: boolean /** Distance in px between the trigger and the tooltip. */ sideOffset?: number } function TooltipContent({ className, sideOffset = 6, arrow = true, children, ...props }: TooltipContentProps) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content data-slot="tooltip-content" sideOffset={sideOffset} className={cn( "z-50 flex w-fit max-w-[min(20rem,calc(100vw-2rem))] origin-(--radix-tooltip-content-transform-origin) items-center gap-2 text-balance rounded-md bg-foreground px-2.5 py-1.5 text-xs leading-snug text-background shadow-[0_4px_12px_-2px_rgb(0_0_0/0.2)]", "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 motion-reduce:animate-none", "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", className )} {...props} > {children} {arrow ? ( <TooltipPrimitive.Arrow data-slot="tooltip-arrow" width={10} height={5} className="fill-foreground" /> ) : null} </TooltipPrimitive.Content> </TooltipPrimitive.Portal> ) } export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type TooltipContentProps, type TooltipProviderProps }Update the import paths to match your project setup.
Usage
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ballmac/tooltip"The full example is in the Code tab above.
API reference
<TooltipProvider>
| Prop | Type | Default |
|---|---|---|
delayDurationMilliseconds from pointer enter until the tooltip opens. | number | 200 |
<TooltipContent>
| Prop | Type | Default |
|---|---|---|
arrowShow the pointer arrow. | boolean | true |
sideOffsetDistance in px between the trigger and the tooltip. | number | 6 |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab (focus trigger) | Opens the tooltip |
| Escape | Closes the tooltip |
Use with AI
Short text hint for a control, shown on hover and focus. Compose Tooltip > TooltipTrigger (style it with buttonVariants and give icon triggers an aria-label) + TooltipContent. Put Kbd inside the content for shortcut hints. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Tooltip (@ballmac/tooltip) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Naming icon-only buttons (still give the button an aria-label)
- Shortcut hints next to a label
- Clarifying truncated text
Not for
- Essential information or anything interactive (use a popover)
- Touch-first UIs where hover doesn't exist
- Error messages (show them inline)
Registry JSON: https://ui.ballmac.com/r/tooltip.json
Credits
Based on shadcn/ui Tooltip, adapted by Ballmac. Free to use in personal and commercial projects.
- npm
- radix-ui
- Registry
- shadcn/utils
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Kbd
Keyboard key and shortcut display built on the semantic <kbd> element, in three sizes, with KbdGroup for combinations like ⌘ ⇧ K. Adapts inside tooltips.
Avatar
A Radix avatar with image and initials fallback, three sizes, an optional presence dot, and AvatarGroup for overlapping stacks with a +N overflow counter.
Accordion
Vertically stacked disclosure sections on Radix Accordion, single or multiple open, with hairline dividers, a rotating plus and height animation.