A short label for a control, shown on hover and keyboard focus, with an optional shortcut hint.
pnpm dlx shadcn@latest add @latheui/tooltipFirst time? Run npx latheui init once to add the @latheui registry. The CLI then writes the source into your project and installs what it needs. Or add it by hand:
Install the npm dependencies.
pnpm add radix-ui motionAdd the Lathe items this component imports.
lib/motion.tshooks/use-controllable-state.tsCopy the source into your project.
"use client"
import * as React from "react"
import { AnimatePresence, motion } from "motion/react"
import { Tooltip as TooltipPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { useControllableState } from "@/hooks/use-controllable-state"
import { fade, spring, useMotionSafe } from "@/lib/motion"
import { Kbd } from "@/components/ui/kbd"
// Spread rather than written as an attribute: in Base UI projects the shadcn CLI
// rewrites a literal `asChild` into a `render` prop, and these are Radix parts.
const slot = { asChild: true } as const
const ProviderContext = React.createContext(false)
/**
* Optional. Wrap a group of tooltips (or the whole app) so that moving
* between neighbouring triggers skips the open delay.
*/
function TooltipProvider({
delayDuration = 350,
skipDelayDuration = 300,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<ProviderContext.Provider value={true}>
<TooltipPrimitive.Provider
delayDuration={delayDuration}
skipDelayDuration={skipDelayDuration}
{...props}
/>
</ProviderContext.Provider>
)
}
export type TooltipProps = {
/** The tooltip text. Keep it short; it is not reachable on touch devices. */
content: React.ReactNode
/** The trigger. Must be a single focusable element that accepts a ref. */
children: React.ReactElement
/**
* Keyboard shortcut shown after the text: `"mod+k"` (⌘K on Apple, Ctrl K elsewhere), `"g d"` for a
* sequence, or `["⌘", "K"]`.
*/
shortcut?: string | string[]
side?: "top" | "right" | "bottom" | "left"
align?: "start" | "center" | "end"
sideOffset?: number
delayDuration?: number
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
className?: string
}
function Tooltip({
content,
children,
shortcut,
side = "top",
align = "center",
sideOffset = 6,
delayDuration,
open: openProp,
defaultOpen = false,
onOpenChange,
className,
}: TooltipProps) {
const hasProvider = React.useContext(ProviderContext)
const [open, setOpen] = useControllableState({
value: openProp,
defaultValue: defaultOpen,
onChange: onOpenChange,
})
const { safe } = useMotionSafe()
const tooltip = (
<TooltipPrimitive.Root open={open} onOpenChange={setOpen} delayDuration={delayDuration}>
<TooltipPrimitive.Trigger {...slot}>{children}</TooltipPrimitive.Trigger>
<AnimatePresence>
{open && (
<TooltipPrimitive.Portal forceMount>
<TooltipPrimitive.Content
{...slot}
forceMount
side={side}
align={align}
sideOffset={sideOffset}
collisionPadding={8}
>
<motion.div
data-slot="tooltip"
initial={{ opacity: 0, scale: 0.94 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.94, transition: fade }}
transition={safe(spring.snappy)}
className={cn(
"z-50 flex max-w-64 origin-(--radix-tooltip-content-transform-origin) items-center gap-2 rounded-md bg-primary px-2.5 py-1.5 text-xs font-medium text-pretty text-primary-foreground shadow-md",
className
)}
>
{content}
{shortcut && <Kbd keys={shortcut} variant="soft" size="sm" keyClassName="bg-current/15" />}
</motion.div>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)}
</AnimatePresence>
</TooltipPrimitive.Root>
)
return hasProvider ? tooltip : <TooltipProvider>{tooltip}</TooltipProvider>
}
export { Tooltip, TooltipProvider }Depends on
| Prop | Type | Default | Description |
|---|---|---|---|
| content* (required) | ReactNode | — | The tooltip text. |
| children* (required) | ReactElement | — | The trigger. One focusable element that accepts a ref. |
| shortcut | string[] | — | Keys shown after the text. Display only. |
| side | "top" | "right" | "bottom" | "left" | "top" | Preferred side. |
| align | "start" | "center" | "end" | "center" | Alignment along the side. |
| sideOffset | number | 6 | Gap between trigger and tooltip in pixels. |
| delayDuration | number | 350 | Milliseconds before opening on hover. |
| open | boolean | — | Controlled open state. |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Called when the tooltip opens or closes. |
| className | string | — | Classes for the tooltip surface. |
Optional. Shares delay timing across the tooltips inside it.
| Prop | Type | Default | Description |
|---|---|---|---|
| delayDuration | number | 350 | Default open delay for tooltips inside. |
| skipDelayDuration | number | 300 | Window in which moving to another trigger opens it instantly. |
import { TooltipProvider } from "@/components/ui/tooltip"
export function Providers({ children }: { children: React.ReactNode }) {
return <TooltipProvider delayDuration={200}>{children}</TooltipProvider>
}