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-reactBasic 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#
- Use tooltips for supplementary info - Not for essential information
- Keep content brief - Tooltips should be scannable at a glance
- Set appropriate delays - 200-400ms prevents accidental triggers
- Consider touch devices - Tooltips don't work well on mobile
- Use popovers for interactive content - Tooltips are hover-only
Related Patterns#
- Dropdowns - Dropdown menu patterns
- Modals - Modal dialog patterns
- Navigation - Navigation component patterns