Skip to main content

tooltip

A customizable and accessible tooltip component with Floating UI positioning, state-aware styling hooks, and flexible triggers.

Philosophy

Tooltips are the lightest touch of contextual help - they appear on hover, require no interaction, and disappear when attention moves on. We build on Floating UI because positioning against viewport edges, scroll containers, and dynamic layouts is harder than it looks. The data-state and data-side attributes give you animation hooks without JavaScript state management.

  • Floating UI powered positioning - Smart placement with flip, shift, and offset middleware.
  • State-aware styling hooks - data-state and data-side attributes for state and placement styling.
  • Transform-origin variable - Uses --gentleduck-tooltip-content-transform-origin for direction-aware animations.
  • Customizable delays - Configure open delays globally with TooltipProvider or per tooltip with delayDuration.
  • Flexible triggers - Wrap any element using asChild.
  • Accessible by default - Implements proper ARIA attributes and keyboard navigation.
  • Portal rendering - Renders tooltips in a portal to avoid layout clipping.

How It's Built

Loading diagram...

Installation


npx @gentleduck/cli add tooltip

npx @gentleduck/cli add tooltip

Usage

import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
 
// app/layout.tsx (once)
<TooltipProvider>{children}</TooltipProvider>
 
<Tooltip delayDuration={500}>
  <TooltipTrigger>Hover</TooltipTrigger>
  <TooltipContent>Tooltip text</TooltipContent>
</Tooltip>
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
 
// app/layout.tsx (once)
<TooltipProvider>{children}</TooltipProvider>
 
<Tooltip delayDuration={500}>
  <TooltipTrigger>Hover</TooltipTrigger>
  <TooltipContent>Tooltip text</TooltipContent>
</Tooltip>

Examples

Basic

Custom Trigger with asChild

<Tooltip>
  <TooltipTrigger asChild>
    <span className="cursor-pointer underline">Hover me</span>
  </TooltipTrigger>
  <TooltipContent>Custom element trigger</TooltipContent>
</Tooltip>
<Tooltip>
  <TooltipTrigger asChild>
    <span className="cursor-pointer underline">Hover me</span>
  </TooltipTrigger>
  <TooltipContent>Custom element trigger</TooltipContent>
</Tooltip>

Animated Tooltip

<Tooltip>
  <TooltipTrigger>Hover</TooltipTrigger>
  <TooltipContent className="TooltipContent">Animated tooltip</TooltipContent>
</Tooltip>
<Tooltip>
  <TooltipTrigger>Hover</TooltipTrigger>
  <TooltipContent className="TooltipContent">Animated tooltip</TooltipContent>
</Tooltip>
.TooltipContent {
  transform-origin: var(--gentleduck-tooltip-content-transform-origin);
  transition: transform 150ms ease, opacity 150ms ease;
}
 
.TooltipContent[data-state='closed'] {
  opacity: 0;
  transform: scale(0.95);
}
 
.TooltipContent[data-state='delayed-open'],
.TooltipContent[data-state='instant-open'] {
  opacity: 1;
  transform: scale(1);
}
.TooltipContent {
  transform-origin: var(--gentleduck-tooltip-content-transform-origin);
  transition: transform 150ms ease, opacity 150ms ease;
}
 
.TooltipContent[data-state='closed'] {
  opacity: 0;
  transform: scale(0.95);
}
 
.TooltipContent[data-state='delayed-open'],
.TooltipContent[data-state='instant-open'] {
  opacity: 1;
  transform: scale(1);
}

Tooltip with Toggle

When wrapping a Toggle (or any interactive element that manages its own pressed/active state), use disableCloseOnClick to prevent the tooltip from intercepting clicks and overriding data-state:

import { Toggle } from '@/components/ui/toggle'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Bold } from 'lucide-react'
 
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild disableCloseOnClick>
      <Toggle aria-label="Toggle bold">
        <Bold className="h-4 w-4" />
      </Toggle>
    </TooltipTrigger>
    <TooltipContent>Toggle bold</TooltipContent>
  </Tooltip>
</TooltipProvider>
import { Toggle } from '@/components/ui/toggle'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Bold } from 'lucide-react'
 
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild disableCloseOnClick>
      <Toggle aria-label="Toggle bold">
        <Bold className="h-4 w-4" />
      </Toggle>
    </TooltipTrigger>
    <TooltipContent>Toggle bold</TooltipContent>
  </Tooltip>
</TooltipProvider>

Without disableCloseOnClick, the tooltip's onClick handler prevents the Toggle from toggling, and its data-state="closed" overrides the Toggle's data-state="on"/"off", breaking the visual feedback.

Styling Hooks

  • data-state - Set on trigger and content (closed, delayed-open, instant-open) for state-based styling.
  • data-side - Set on content (top, right, bottom, left) for placement-aware styles.
  • --gentleduck-tooltip-content-transform-origin - CSS variable for animation transform origin.

RTL Support

Set dir="rtl" on Tooltip for a local override, or set DirectionProvider once at app/root level for global direction. This mirrors tooltip positioning in right-to-left layouts.

<TooltipProvider>
  <Tooltip dir="rtl">
    <TooltipTrigger>مرر الماوس</TooltipTrigger>
    <TooltipContent>نص التلميح</TooltipContent>
  </Tooltip>
</TooltipProvider>
<TooltipProvider>
  <Tooltip dir="rtl">
    <TooltipTrigger>مرر الماوس</TooltipTrigger>
    <TooltipContent>نص التلميح</TooltipContent>
  </Tooltip>
</TooltipProvider>

Motion

Use MotionTooltip and MotionTooltipContent for smooth enter/exit animations powered by motion. The tooltip fades in with a subtle scale and directional shift toward the trigger.

API Reference

TooltipProvider

PropTypeDefaultDescription
childrenReact.ReactNode-Tooltip tree to provide behavior for
delayDurationnumber700Delay before opening tooltips
skipDelayDurationnumber300Window where moving between triggers skips delay
disableHoverableContentbooleanfalseClose tooltip as soon as pointer leaves trigger

Tooltip

PropTypeDefaultDescription
childrenReact.ReactNode-Tooltip sub-components (TooltipTrigger, TooltipContent)
openboolean-Controlled open state
defaultOpenbooleanfalseUncontrolled initial open state
onOpenChange(open: boolean) => void-Callback when open state changes
delayDurationnumber700Per-tooltip delay override
disableHoverableContentbooleanfalsePer-tooltip hover-content behavior override
dir'ltr' | 'rtl'-Text direction. Resolved by primitives useDirection (dir prop -> DirectionProvider -> 'ltr').
...propsReact.ComponentPropsWithRef<typeof TooltipPrimitive.Root>-Additional root props

TooltipTrigger

PropTypeDefaultDescription
asChildbooleanfalseRenders the child element as the trigger instead of a button
disableCloseOnClickbooleanfalsePrevents tooltip from closing on click and from overriding the child's data-state/data-slot. Use when wrapping interactive elements like Toggle.
childrenReact.ReactNode-Content rendered inside the trigger
classNamestring-Additional CSS class names to apply
...propsOmit<React.ComponentPropsWithRef<typeof TooltipPrimitive.Trigger>, 'size'>-Additional props to spread to the button element

TooltipContent

PropTypeDefaultDescription
classNamestring-Additional CSS class names to apply
childrenReact.ReactNode-Content rendered inside the tooltip
refReact.Ref<HTMLDivElement>-Ref forwarded to the content container
forceMountboolean-Keep content mounted for external animation control
side'top' | 'right' | 'bottom' | 'left''top'Preferred side relative to the trigger
align'start' | 'center' | 'end''center'Alignment on the chosen side
sideOffsetnumber4Main-axis offset from trigger
alignOffsetnumber0Cross-axis offset from trigger
...propsReact.ComponentPropsWithRef<typeof TooltipPrimitive.Content>-Additional props to spread to the content div

MotionTooltip

Wraps with useMotionRoot for exit animation support. Requires the motion package.

PropTypeDefaultDescription
...propsTooltipProps-All props from Tooltip are supported

MotionTooltipContent

Adds directional scale, blur, and opacity enter/exit animation with ultra-fast tweenMicro (100ms) transition. Requires the motion package.

PropTypeDefaultDescription
...propsTooltipContentProps-All props from TooltipContent are supported