Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/tooltip.jsonInstalls to @/components/ui/tooltip.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0Copy the source into your project:
'use client'
import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'
import { cn } from '@/lib/utils'
function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({ className, ...props }: TooltipPrimitive.Trigger.Props) {
return (
<TooltipPrimitive.Trigger
data-slot="tooltip-trigger"
className={cn('focus-visible:ring-3 focus-visible:ring-ring focus-visible:outline-hidden', className)}
{...props}
/>
)
}
function TooltipContent({
className,
side = 'top',
sideOffset = 4,
align = 'center',
alignOffset = 0,
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<TooltipPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
'z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground ring-1 ring-border has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-popover fill-popover data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }A tooltip component that displays additional information when hovering over or focusing on an element. Built on top of Base UI Tooltip, it follows WAI-ARIA design patterns and provides a consistent way to show supplementary information.
Features
- Accessible tooltip implementation
- Customizable open and close delay
- Multiple positioning options
- Keyboard navigation support
- Screen reader announcements
- Custom content support
- Controlled and uncontrolled modes
- Focus management
- Hover and focus interactions
- Popover-coloured surface with a subtle border and an arrow
- Inline keyboard shortcuts with
Kbd - Requires a
TooltipProviderancestor (typically once at the app root)
Usage
Tooltip no longer renders its own TooltipProvider. Wrap your application (or the relevant subtree) in a single
TooltipProvider; it defaults to delay={0}.
Pass the trigger element with the render prop (Base UI has no asChild):
<TooltipTrigger render={<Button variant="outline" />}>Save draft</TooltipTrigger>.
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { Button } from '@/components/ui/button'
import { Kbd } from '@/components/ui/kbd'
import { SaveIcon } from 'lucide-react'
// Basic usage
export function BasicTooltip() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<Button variant="outline" />}>Save draft</TooltipTrigger>
<TooltipContent>
<p>Drafts are kept for 30 days</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
// With custom delay (per trigger, or for all tooltips via TooltipProvider)
export function DelayedTooltip() {
return (
<Tooltip>
<TooltipTrigger delay={1000} render={<Button variant="outline" />}>Print</TooltipTrigger>
<TooltipContent>
<p>Prints the case summary</p>
</TooltipContent>
</Tooltip>
)
}
// With keyboard shortcut
export function ShortcutTooltip() {
return (
<Tooltip>
<TooltipTrigger render={<Button variant="outline" size="icon-sm" aria-label="Save" />}>
<SaveIcon />
</TooltipTrigger>
<TooltipContent side="bottom">
Save changes <Kbd>⌘S</Kbd>
</TooltipContent>
</Tooltip>
)
}
// With rich content
export function RichTooltip() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<Button variant="outline" />}>Sofia Berg</TooltipTrigger>
<TooltipContent>
<div className="space-y-2">
<p className="font-medium">Sofia Berg</p>
<p className="text-muted-foreground">Case handler, building permits</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}Examples
Default Tooltip
A short hint on an outline button.
With Icon
A case toolbar of icon-only buttons. Each has an aria-label, and the tooltip shows the same text to sighted users.
Custom Delay
delay per trigger (or for all tooltips on TooltipProvider). The provider defaults to 0.
Rich Content
Several lines of content – the name, role and availability of each handler on a case.
Plain Tooltip
A plain-text trigger inside running text that explains an abbreviation. Long content wraps at max-w-xs.
Sides
Place the tooltip with side: top, right, bottom or left.
With Keyboard Shortcut
Kbd and KbdGroup inside the content get tooltip-specific styling. Mirror the shortcut in aria-keyshortcuts.
Disabled Button
A disabled button can't be focused or hovered, so its tooltip never shows. Use aria-disabled="true" instead: the button
stays focusable, the tooltip explains why the action is unavailable, and the click handler does nothing.
Field Hint
A small info button next to a field label, with guidance on where to find the value.
Props
TooltipProvider
delay- Delay in ms before tooltips open (default0)closeDelay- Delay in ms before tooltips closetimeout- Time in ms during which moving to another trigger opens its tooltip instantly
Tooltip
open/defaultOpen/onOpenChange- Open statedisabled- Stops the tooltip from opening
TooltipTrigger
render- The element to render as the trigger, e.g.render={<Button variant="outline" />}(replacesasChild)delay/closeDelay- Override the provider delays for this triggercloseOnClick- Whether clicking the trigger closes the tooltip (defaulttrue)
TooltipContent
side-'top' | 'right' | 'bottom' | 'left' | 'inline-start' | 'inline-end'(default'top')sideOffset- Distance from the trigger (default4)align/alignOffset- Alignment relative to the triggerclassName- Additional CSS classes
Accessibility
The Tooltip component follows WAI-ARIA guidelines for tooltips:
- Uses
role="tooltip"for proper semantic meaning - Manages focus appropriately
- Supports keyboard navigation
- Announces content to screen readers
- Provides sufficient contrast for visibility
- Maintains proper timing for display
Best Practices
- Keep tooltip content concise and helpful
- Use tooltips sparingly to avoid overwhelming users
- Ensure tooltip content adds value beyond what's visible
- Consider mobile users and touch interactions
- Test with keyboard navigation and screen readers
- Maintain appropriate delay duration for user experience
- Use consistent styling across your application