Tooltip Patterns

Build accessible tooltips, popovers, and hover cards using Radix UI primitives.

Overview#

Tooltips provide contextual information on hover or focus. This pattern covers:

  • Basic tooltips with Radix UI
  • Popovers for interactive content
  • Hover cards for previews
  • Keyboard shortcut hints
  • Rich tooltip content

Prerequisites#

npm install @radix-ui/react-tooltip @radix-ui/react-popover @radix-ui/react-hover-card lucide-react

Basic Tooltip#

A simple tooltip component with Radix UI.

1// components/ui/Tooltip.tsx 2'use client' 3 4import * as TooltipPrimitive from '@radix-ui/react-tooltip' 5import { forwardRef } from 'react' 6import { cn } from '@/lib/utils' 7 8export const TooltipProvider = TooltipPrimitive.Provider 9export const Tooltip = TooltipPrimitive.Root 10export const TooltipTrigger = TooltipPrimitive.Trigger 11 12export const TooltipContent = forwardRef< 13 React.ElementRef<typeof TooltipPrimitive.Content>, 14 React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> 15>(({ className, sideOffset = 4, ...props }, ref) => ( 16 <TooltipPrimitive.Portal> 17 <TooltipPrimitive.Content 18 ref={ref} 19 sideOffset={sideOffset} 20 className={cn( 21 'z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-1.5 text-xs text-white shadow-md', 22 'animate-in fade-in-0 zoom-in-95', 23 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', 24 'data-[side=bottom]:slide-in-from-top-2', 25 'data-[side=left]:slide-in-from-right-2', 26 'data-[side=right]:slide-in-from-left-2', 27 'data-[side=top]:slide-in-from-bottom-2', 28 className 29 )} 30 {...props} 31 /> 32 </TooltipPrimitive.Portal> 33)) 34TooltipContent.displayName = 'TooltipContent' 35 36// Simple tooltip wrapper 37interface SimpleTooltipProps { 38 content: React.ReactNode 39 children: React.ReactNode 40 side?: 'top' | 'right' | 'bottom' | 'left' 41 delayDuration?: number 42} 43 44export function SimpleTooltip({ 45 content, 46 children, 47 side = 'top', 48 delayDuration = 200 49}: SimpleTooltipProps) { 50 return ( 51 <Tooltip delayDuration={delayDuration}> 52 <TooltipTrigger asChild>{children}</TooltipTrigger> 53 <TooltipContent side={side}>{content}</TooltipContent> 54 </Tooltip> 55 ) 56}

Usage with Provider#

Wrap your app with TooltipProvider for consistent behavior.

1// app/layout.tsx 2import { TooltipProvider } from '@/components/ui/Tooltip' 3 4export default function RootLayout({ children }: { children: React.ReactNode }) { 5 return ( 6 <html> 7 <body> 8 <TooltipProvider> 9 {children} 10 </TooltipProvider> 11 </body> 12 </html> 13 ) 14} 15 16// components/IconButton.tsx 17import { SimpleTooltip } from '@/components/ui/Tooltip' 18import { Settings } from 'lucide-react' 19 20export function IconButton() { 21 return ( 22 <SimpleTooltip content="Settings"> 23 <button className="rounded p-2 hover:bg-gray-100"> 24 <Settings className="h-4 w-4" /> 25 </button> 26 </SimpleTooltip> 27 ) 28}

Popover Component#

Popovers for interactive content that requires clicking.

1// components/ui/Popover.tsx 2'use client' 3 4import * as PopoverPrimitive from '@radix-ui/react-popover' 5import { forwardRef } from 'react' 6import { cn } from '@/lib/utils' 7 8export const Popover = PopoverPrimitive.Root 9export const PopoverTrigger = PopoverPrimitive.Trigger 10export const PopoverAnchor = PopoverPrimitive.Anchor 11export const PopoverClose = PopoverPrimitive.Close 12 13export const PopoverContent = forwardRef< 14 React.ElementRef<typeof PopoverPrimitive.Content>, 15 React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 16>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 17 <PopoverPrimitive.Portal> 18 <PopoverPrimitive.Content 19 ref={ref} 20 align={align} 21 sideOffset={sideOffset} 22 className={cn( 23 'z-50 w-72 rounded-md border bg-white p-4 shadow-md outline-none', 24 'data-[state=open]:animate-in data-[state=closed]:animate-out', 25 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 26 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', 27 'data-[side=bottom]:slide-in-from-top-2', 28 'data-[side=left]:slide-in-from-right-2', 29 'data-[side=right]:slide-in-from-left-2', 30 'data-[side=top]:slide-in-from-bottom-2', 31 className 32 )} 33 {...props} 34 /> 35 </PopoverPrimitive.Portal> 36)) 37PopoverContent.displayName = 'PopoverContent'

Info Popover#

Display helpful information with an info icon.

1// components/InfoPopover.tsx 2'use client' 3 4import { 5 Popover, 6 PopoverContent, 7 PopoverTrigger 8} from '@/components/ui/Popover' 9import { HelpCircle } from 'lucide-react' 10 11interface InfoPopoverProps { 12 title: string 13 description: string 14 children?: React.ReactNode 15} 16 17export function InfoPopover({ title, description, children }: InfoPopoverProps) { 18 return ( 19 <Popover> 20 <PopoverTrigger asChild> 21 <button className="inline-flex text-gray-400 hover:text-gray-600"> 22 <HelpCircle className="h-4 w-4" /> 23 <span className="sr-only">More info</span> 24 </button> 25 </PopoverTrigger> 26 <PopoverContent> 27 <h4 className="mb-1 font-medium">{title}</h4> 28 <p className="text-sm text-gray-600">{description}</p> 29 {children} 30 </PopoverContent> 31 </Popover> 32 ) 33} 34 35// Usage 36<div className="flex items-center gap-2"> 37 <label>API Rate Limit</label> 38 <InfoPopover 39 title="Rate Limiting" 40 description="The maximum number of API requests allowed per minute." 41 /> 42</div>

Hover Card#

Preview cards that appear on hover.

1// components/ui/HoverCard.tsx 2'use client' 3 4import * as HoverCardPrimitive from '@radix-ui/react-hover-card' 5import { forwardRef } from 'react' 6import { cn } from '@/lib/utils' 7 8export const HoverCard = HoverCardPrimitive.Root 9export const HoverCardTrigger = HoverCardPrimitive.Trigger 10 11export const HoverCardContent = forwardRef< 12 React.ElementRef<typeof HoverCardPrimitive.Content>, 13 React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> 14>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 15 <HoverCardPrimitive.Portal> 16 <HoverCardPrimitive.Content 17 ref={ref} 18 align={align} 19 sideOffset={sideOffset} 20 className={cn( 21 'z-50 w-64 rounded-md border bg-white p-4 shadow-md outline-none', 22 'data-[state=open]:animate-in data-[state=closed]:animate-out', 23 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 24 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', 25 className 26 )} 27 {...props} 28 /> 29 </HoverCardPrimitive.Portal> 30)) 31HoverCardContent.displayName = 'HoverCardContent' 32 33// User preview card 34export function UserHoverCard({ user, children }: { user: User; children: React.ReactNode }) { 35 return ( 36 <HoverCard> 37 <HoverCardTrigger asChild> 38 {children} 39 </HoverCardTrigger> 40 <HoverCardContent> 41 <div className="flex items-start gap-3"> 42 <img 43 src={user.avatar} 44 alt={user.name} 45 className="h-10 w-10 rounded-full" 46 /> 47 <div> 48 <p className="font-medium">{user.name}</p> 49 <p className="text-sm text-gray-500">@{user.username}</p> 50 <p className="mt-2 text-sm text-gray-600">{user.bio}</p> 51 </div> 52 </div> 53 </HoverCardContent> 54 </HoverCard> 55 ) 56}

Keyboard Shortcut Tooltip#

Display keyboard shortcuts alongside actions.

1// components/ShortcutTooltip.tsx 2'use client' 3 4import { SimpleTooltip } from '@/components/ui/Tooltip' 5 6interface ShortcutTooltipProps { 7 shortcut: string 8 description: string 9 children: React.ReactNode 10} 11 12export function ShortcutTooltip({ 13 shortcut, 14 description, 15 children 16}: ShortcutTooltipProps) { 17 return ( 18 <SimpleTooltip 19 content={ 20 <div className="flex items-center gap-2"> 21 <span>{description}</span> 22 <kbd className="rounded bg-white/20 px-1.5 py-0.5 text-xs font-mono"> 23 {shortcut} 24 </kbd> 25 </div> 26 } 27 > 28 {children} 29 </SimpleTooltip> 30 ) 31} 32 33// Usage 34<ShortcutTooltip shortcut="Cmd+K" description="Quick search"> 35 <button className="rounded p-2 hover:bg-gray-100"> 36 <Search className="h-4 w-4" /> 37 </button> 38</ShortcutTooltip>

Rich Tooltip Content#

Tooltips with images and complex content.

1// components/UserTooltip.tsx 2'use client' 3 4import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/Tooltip' 5import Image from 'next/image' 6 7interface User { 8 name: string 9 email: string 10 image?: string 11 role: string 12} 13 14interface UserTooltipProps { 15 user: User 16 children: React.ReactNode 17} 18 19export function UserTooltip({ user, children }: UserTooltipProps) { 20 return ( 21 <Tooltip> 22 <TooltipTrigger asChild>{children}</TooltipTrigger> 23 <TooltipContent className="w-64 p-0"> 24 <div className="flex items-start gap-3 p-3"> 25 {user.image && ( 26 <Image 27 src={user.image} 28 alt={user.name} 29 width={40} 30 height={40} 31 className="rounded-full" 32 /> 33 )} 34 <div> 35 <p className="font-medium text-white">{user.name}</p> 36 <p className="text-xs text-gray-300">{user.email}</p> 37 <span className="mt-1 inline-block rounded bg-white/20 px-1.5 py-0.5 text-xs"> 38 {user.role} 39 </span> 40 </div> 41 </div> 42 </TooltipContent> 43 </Tooltip> 44 ) 45}

Best Practices#

  1. Use tooltips for supplementary info - Not for essential information
  2. Keep content brief - Tooltips should be scannable at a glance
  3. Set appropriate delays - 200-400ms prevents accidental triggers
  4. Consider touch devices - Tooltips don't work well on mobile
  5. Use popovers for interactive content - Tooltips are hover-only