# Tooltip

> A short hint on hover or keyboard focus that opens after a brief rest, switches instantly between neighbours and shows shortcuts.

Docs: https://hextaui.com/docs/tooltip
Markdown: https://hextaui.com/docs/tooltip.md

```tsx title="components/examples/tooltip/demo.tsx"
import {
  IconArrowBackUp,
  IconArrowForwardUp,
  IconShare2,
  IconTrash,
} from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Kbd } from "@/components/ui/kbd"
import { TooltipGroup, TooltipTrigger } from "@/components/ui/tooltip"

export function TooltipDemo() {
  return (
    <TooltipGroup>
      <div className="flex items-center gap-1">
        <TooltipTrigger
          content={
            <>
              Undo
              <Kbd keys="mod+z" />
            </>
          }
          render={<Button variant="ghost" size="icon" aria-label="Undo" />}
        >
          <IconArrowBackUp />
        </TooltipTrigger>
        <TooltipTrigger
          content={
            <>
              Redo
              <Kbd keys="mod+shift+z" />
            </>
          }
          render={<Button variant="ghost" size="icon" aria-label="Redo" />}
        >
          <IconArrowForwardUp />
        </TooltipTrigger>
        <TooltipTrigger
          content="Share"
          render={<Button variant="ghost" size="icon" aria-label="Share" />}
        >
          <IconShare2 />
        </TooltipTrigger>
        <TooltipTrigger
          content="Move to trash"
          render={
            <Button variant="ghost" size="icon" aria-label="Move to trash" />
          }
        >
          <IconTrash />
        </TooltipTrigger>
      </div>
    </TooltipGroup>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/tooltip.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/tooltip.tsx"
"use client"

import * as React from "react"
import {
  DirectionProvider,
  useDirection,
  type TextDirection,
} from "@base-ui/react/direction-provider"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

function readDirection(element: Element | null | undefined) {
  if (!element || !element.isConnected) {
    return undefined
  }
  return getComputedStyle(element).direction === "rtl" ? "rtl" : "ltr"
}

const TooltipProviderContext = React.createContext(false)

const TooltipGroupContext =
  React.createContext<TooltipPrimitive.Handle<React.ReactNode> | null>(null)

const TooltipTriggerContext =
  React.createContext<React.RefObject<Element | null> | null>(null)

function TooltipProvider({
  delay = 300,
  closeDelay = 0,
  timeout = 400,
  ...props
}: TooltipPrimitive.Provider.Props) {
  return (
    <TooltipProviderContext.Provider value={true}>
      <TooltipPrimitive.Provider
        delay={delay}
        closeDelay={closeDelay}
        timeout={timeout}
        {...props}
      />
    </TooltipProviderContext.Provider>
  )
}

function Tooltip<Payload>({
  onOpenChange,
  children,
  ...props
}: TooltipPrimitive.Root.Props<Payload>) {
  const hasProvider = React.useContext(TooltipProviderContext)
  const inherited = useDirection()
  const triggerRef = React.useRef<Element | null>(null)
  const [triggerDirection, setTriggerDirection] =
    React.useState<TextDirection>()
  const direction = triggerDirection === "rtl" ? "rtl" : inherited

  const root = (
    <TooltipTriggerContext.Provider value={triggerRef}>
      <DirectionProvider direction={direction}>
        <TooltipPrimitive.Root
          onOpenChange={(open, eventDetails) => {
            onOpenChange?.(open, eventDetails)
            if (open && !eventDetails.isCanceled && eventDetails.trigger) {
              triggerRef.current = eventDetails.trigger
              setTriggerDirection(readDirection(eventDetails.trigger))
            }
          }}
          {...props}
        >
          {children}
        </TooltipPrimitive.Root>
      </DirectionProvider>
    </TooltipTriggerContext.Provider>
  )

  return hasProvider ? root : <TooltipProvider>{root}</TooltipProvider>
}

type TooltipTriggerProps<Payload> = Omit<
  TooltipPrimitive.Trigger.Props<Payload>,
  "content"
> & {
  content?: React.ReactNode
}

function TooltipTrigger<Payload>({
  ref,
  render,
  type,
  delay,
  handle,
  payload,
  content,
  ...props
}: TooltipTriggerProps<Payload>) {
  const hasProvider = React.useContext(TooltipProviderContext)
  const triggerRef = React.useContext(TooltipTriggerContext)
  const groupHandle = React.useContext(TooltipGroupContext)
  const grouped = handle === undefined && groupHandle !== null
  const setRef = React.useCallback(
    (node: HTMLButtonElement | null) => {
      if (triggerRef && node) {
        triggerRef.current ??= node
      }
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    },
    [ref, triggerRef]
  )

  return (
    <TooltipPrimitive.Trigger
      ref={setRef}
      data-slot="tooltip-trigger"
      render={render}
      delay={delay ?? (hasProvider ? undefined : 300)}
      type={type ?? (render === undefined ? "button" : undefined)}
      handle={
        grouped
          ? (groupHandle as unknown as TooltipPrimitive.Handle<Payload>)
          : handle
      }
      payload={grouped ? (content as Payload) : payload}
      {...props}
    />
  )
}

function TooltipPortal(props: TooltipPrimitive.Portal.Props) {
  return <TooltipPrimitive.Portal data-slot="tooltip-portal" {...props} />
}

type TooltipContentProps = TooltipPrimitive.Popup.Props &
  Pick<
    TooltipPrimitive.Positioner.Props,
    | "side"
    | "align"
    | "sideOffset"
    | "alignOffset"
    | "anchor"
    | "arrowPadding"
    | "collisionPadding"
    | "collisionAvoidance"
    | "collisionBoundary"
    | "sticky"
    | "positionMethod"
    | "disableAnchorTracking"
  > & {
    portalProps?: Omit<TooltipPrimitive.Portal.Props, "children">
    arrow?: boolean
  }

function TooltipContent({
  className,
  children,
  side = "top",
  align = "center",
  arrow = false,
  sideOffset = arrow ? 8 : 6,
  alignOffset = 0,
  anchor,
  arrowPadding = 8,
  collisionPadding = 8,
  collisionAvoidance,
  collisionBoundary,
  sticky,
  positionMethod,
  disableAnchorTracking,
  portalProps,
  dir,
  ...props
}: TooltipContentProps) {
  const direction = useDirection()
  const triggerRef = React.useContext(TooltipTriggerContext)
  const setPositioner = React.useCallback(
    (positioner: HTMLDivElement | null) => {
      if (!positioner || dir !== undefined || direction === "rtl") {
        return
      }
      if (readDirection(triggerRef?.current) === "rtl") {
        positioner.setAttribute("dir", "rtl")
      }
    },
    [dir, direction, triggerRef]
  )

  return (
    <TooltipPrimitive.Portal {...portalProps}>
      <TooltipPrimitive.Positioner
        ref={setPositioner}
        data-slot="tooltip-positioner"
        dir={dir ?? (direction === "rtl" ? "rtl" : undefined)}
        side={side}
        align={align}
        sideOffset={sideOffset}
        alignOffset={alignOffset}
        anchor={anchor}
        arrowPadding={arrowPadding}
        collisionPadding={collisionPadding}
        collisionAvoidance={collisionAvoidance}
        collisionBoundary={collisionBoundary}
        sticky={sticky}
        positionMethod={positionMethod}
        disableAnchorTracking={disableAnchorTracking}
        className="isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) outline-none focus-visible:outline-hidden has-data-transitioning:duration-250 has-data-transitioning:ease-out-quint data-instant:transition-none motion-safe:has-data-transitioning:transition-[top,left,right,bottom] motion-reduce:transition-none"
      >
        <TooltipPrimitive.Popup
          data-slot="tooltip-content"
          className={mergeClassName(
            "relative h-(--popup-height,auto) w-(--popup-width,auto) max-w-[min(var(--container-2xs),var(--available-width))] origin-(--transform-origin) rounded-md bg-foreground text-xs/4 text-pretty wrap-anywhere text-background shadow-md/10 transition-[opacity,scale,width,height] duration-[150ms,150ms,250ms,250ms] ease-out-quint outline-none focus-visible:outline-hidden data-ending-style:opacity-0 data-ending-style:duration-100 data-instant:transition-none data-starting-style:opacity-0 motion-safe:data-starting-style:scale-96 motion-reduce:transition-opacity [&_[data-slot=kbd-separator]]:text-background/70 [&_kbd[data-slot=kbd][data-variant]]:rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--radius-md)-0.25rem))] [&_kbd[data-slot=kbd][data-variant]]:bg-background/15 [&_kbd[data-slot=kbd][data-variant]]:text-background [&_kbd[data-slot=kbd][data-variant]]:shadow-none [&_kbd[data-slot=kbd][data-variant]]:inset-ring-background/20 [&_kbd[data-slot=kbd][data-variant]]:[direction:ltr]",
            className
          )}
          {...props}
        >
          <TooltipPrimitive.Viewport
            ref={freezeOutgoingWidth}
            data-slot="tooltip-viewport"
            className={viewport}
          >
            {children}
          </TooltipPrimitive.Viewport>
          {arrow && <TooltipArrow />}
        </TooltipPrimitive.Popup>
      </TooltipPrimitive.Positioner>
    </TooltipPrimitive.Portal>
  )
}

const viewport =
  "relative size-full overflow-clip rounded-[inherit] [&>*]:flex [&>*]:items-center [&>*]:justify-between [&>*]:gap-2 [&>*]:px-2 [&>*]:py-1 [&>*:has(>[data-slot=kbd-group])]:pe-1 [&>*:has(>[data-slot=kbd])]:pe-1 [&>[data-current]]:w-(--popup-width) [&>[data-previous]]:top-0 [&>[data-previous]]:start-0 [&>[data-current]]:transition-[translate,opacity,filter] [&>[data-previous]]:transition-[translate,opacity,filter] [&>[data-current]]:duration-250 [&>[data-previous]]:duration-150 [&>[data-current]]:ease-out-quint [&>[data-previous]]:ease-out-quint [&>[data-current][data-starting-style]]:opacity-0 [&>[data-previous][data-ending-style]]:opacity-0 motion-safe:[&>[data-current][data-starting-style]]:blur-[1px] motion-safe:[&>[data-previous][data-ending-style]]:blur-[1px] motion-safe:data-[activation-direction~=right]:[&>[data-current][data-starting-style]]:translate-x-4 motion-safe:data-[activation-direction~=right]:[&>[data-previous][data-ending-style]]:-translate-x-4 motion-safe:data-[activation-direction~=left]:[&>[data-current][data-starting-style]]:-translate-x-4 motion-safe:data-[activation-direction~=left]:[&>[data-previous][data-ending-style]]:translate-x-4 motion-safe:data-[activation-direction~=down]:[&>[data-current][data-starting-style]]:translate-y-2 motion-safe:data-[activation-direction~=down]:[&>[data-previous][data-ending-style]]:-translate-y-2 motion-safe:data-[activation-direction~=up]:[&>[data-current][data-starting-style]]:-translate-y-2 motion-safe:data-[activation-direction~=up]:[&>[data-previous][data-ending-style]]:translate-y-2 motion-reduce:[&>*]:transition-opacity data-instant:[&>*]:transition-none"

function freezeOutgoingWidth(viewport: HTMLDivElement | null) {
  if (!viewport || typeof MutationObserver === "undefined") {
    return
  }
  let currentWidth = 0
  const sizes =
    typeof ResizeObserver === "undefined"
      ? null
      : new ResizeObserver((entries) => {
          for (const entry of entries) {
            if ((entry.target as HTMLElement).hasAttribute("data-current")) {
              currentWidth =
                entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
            }
          }
        })
  const sync = () => {
    for (const child of Array.from(viewport.children)) {
      const element = child as HTMLElement
      sizes?.observe(element)
      if (!element.hasAttribute("data-previous")) {
        element.style.removeProperty("width")
        delete element.dataset.frozen
        continue
      }
      const key = element.textContent ?? ""
      if (element.dataset.frozen !== key) {
        element.dataset.frozen = key
        const width = currentWidth || viewport.getBoundingClientRect().width
        element.style.width = `${width}px`
      }
    }
  }
  sync()
  const observer = new MutationObserver(sync)
  observer.observe(viewport, {
    childList: true,
    subtree: true,
    characterData: true,
    attributes: true,
    attributeFilter: ["data-current", "data-previous"],
  })
  return () => {
    observer.disconnect()
    sizes?.disconnect()
  }
}

function TooltipArrow({ className, ...props }: TooltipPrimitive.Arrow.Props) {
  return (
    <TooltipPrimitive.Arrow
      data-slot="tooltip-arrow"
      className={mergeClassName(
        "h-1.5 w-3 overflow-clip transition-[left,top] duration-250 ease-out-quint before:absolute before:bottom-0 before:left-1/2 before:size-[calc(var(--spacing)*1.5*sqrt(2))] before:-translate-x-1/2 before:translate-y-1/2 before:rotate-45 before:rounded-[1px] before:bg-foreground data-instant:transition-none data-[side=bottom]:-top-1.5 data-[side=inline-end]:-left-[9px] data-[side=inline-end]:-rotate-90 data-[side=inline-start]:-right-[9px] data-[side=inline-start]:rotate-90 data-[side=left]:-right-[9px] data-[side=left]:rotate-90 data-[side=right]:-left-[9px] data-[side=right]:-rotate-90 data-[side=top]:-bottom-1.5 data-[side=top]:rotate-180 motion-reduce:transition-none rtl:data-[side=inline-end]:right-[-9px] rtl:data-[side=inline-end]:left-auto rtl:data-[side=inline-end]:rotate-90 rtl:data-[side=inline-start]:right-auto rtl:data-[side=inline-start]:left-[-9px] rtl:data-[side=inline-start]:-rotate-90",
        className
      )}
      {...props}
    />
  )
}

type TooltipGroupProps = TooltipPrimitive.Provider.Props &
  Omit<TooltipContentProps, "children"> & {
    children?: React.ReactNode
  }

function TooltipGroup({
  delay,
  closeDelay = 100,
  timeout,
  children,
  ...contentProps
}: TooltipGroupProps) {
  const [handle] = React.useState(() =>
    TooltipPrimitive.createHandle<React.ReactNode>()
  )

  return (
    <TooltipProvider delay={delay} closeDelay={closeDelay} timeout={timeout}>
      <TooltipGroupContext.Provider value={handle}>
        {children}
      </TooltipGroupContext.Provider>
      <Tooltip handle={handle}>
        {({ payload }) => (
          <TooltipContent {...contentProps}>
            {payload as React.ReactNode}
          </TooltipContent>
        )}
      </Tooltip>
    </TooltipProvider>
  )
}

const createTooltipHandle = TooltipPrimitive.createHandle

export {
  TooltipProvider,
  Tooltip,
  TooltipGroup,
  TooltipTrigger,
  TooltipPortal,
  TooltipContent,
  TooltipArrow,
  createTooltipHandle,
}
export type { TooltipContentProps, TooltipGroupProps, TooltipTriggerProps }
```

Update the import paths to match your project setup.

## Usage

```tsx
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
```

```tsx
<Tooltip>
  <TooltipTrigger render={<Button variant="ghost" size="icon" aria-label="Undo" />}>
    <IconArrowBackUp />
  </TooltipTrigger>
  <TooltipContent>Undo</TooltipContent>
</Tooltip>
```

A tooltip opens after a short rest on the trigger, or right away when the trigger gets keyboard focus. Once one is showing, its neighbours open instantly and without animation, so scanning a toolbar feels like reading labels rather than waiting for each one.

That instant switching works between tooltips that share a `TooltipProvider`. Wrap your app (or a toolbar) in one to share the delay. A tooltip without a provider still works on its own with the same defaults.

```tsx
<TooltipProvider>
  <App />
</TooltipProvider>
```

## Composition

```text
TooltipProvider
└── Tooltip
    ├── TooltipTrigger
    └── TooltipContent

TooltipGroup
└── TooltipTrigger
```

## Examples

### Sides

Set `side` on `TooltipContent`. Logical sides follow the reading direction, and the tooltip flips when there isn’t room.

```tsx title="components/examples/tooltip/sides.tsx"
import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

const sides = [
  { side: "top", cell: "col-start-2 row-start-1" },
  { side: "inline-start", cell: "col-start-1 row-start-2" },
  { side: "inline-end", cell: "col-start-3 row-start-2" },
  { side: "bottom", cell: "col-start-2 row-start-3" },
] as const

export function TooltipSides() {
  return (
    <TooltipProvider>
      <div className="grid grid-cols-3 grid-rows-3 place-items-center gap-2">
        {sides.map(({ side, cell }) => (
          <div key={side} className={cell}>
            <Tooltip>
              <TooltipTrigger render={<Button variant="outline" />}>
                {side}
              </TooltipTrigger>
              <TooltipContent side={side}>{side}</TooltipContent>
            </Tooltip>
          </div>
        ))}
      </div>
    </TooltipProvider>
  )
}
```

### With an arrow

`arrow` adds a pointer that stays on the trigger even when the tooltip shifts to fit the screen.

```tsx title="components/examples/tooltip/arrow.tsx"
import { IconBell } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipWithArrow() {
  return (
    <Tooltip>
      <TooltipTrigger
        render={
          <Button variant="outline" size="icon" aria-label="Notifications" />
        }
      >
        <IconBell />
      </TooltipTrigger>
      <TooltipContent arrow>Notifications</TooltipContent>
    </Tooltip>
  )
}
```

### Keyboard shortcut

Put a `Kbd` or `KbdGroup` after the label. It picks up the tooltip’s colors and sits at the end, showing ⌘ on Apple devices and Ctrl elsewhere.

```tsx title="components/examples/tooltip/shortcut.tsx"
import { IconDeviceFloppy } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { KbdGroup } from "@/components/ui/kbd"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipShortcut() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          <IconDeviceFloppy data-icon="inline-start" />
          Save
        </TooltipTrigger>
        <TooltipContent>
          Save changes
          <KbdGroup keys="mod+s" size="sm" />
        </TooltipContent>
      </Tooltip>
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          Go to inbox
        </TooltipTrigger>
        <TooltipContent>
          Go to inbox
          <KbdGroup keys="g i" size="sm" />
        </TooltipContent>
      </Tooltip>
    </div>
  )
}
```

### Toolbar

Rest on one button, then slide along the group. Tooltips under one `TooltipProvider` swap instantly while you move between them.

```tsx title="components/examples/tooltip/toolbar.tsx"
import {
  IconBold,
  IconItalic,
  IconLink,
  IconStrikethrough,
  IconUnderline,
} from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Kbd } from "@/components/ui/kbd"
import { TooltipGroup, TooltipTrigger } from "@/components/ui/tooltip"

const tools = [
  { label: "Bold", keys: "mod+b", icon: IconBold },
  { label: "Italic", keys: "mod+i", icon: IconItalic },
  { label: "Underline", keys: "mod+u", icon: IconUnderline },
  { label: "Strikethrough", keys: "mod+shift+x", icon: IconStrikethrough },
  { label: "Insert link", keys: "mod+k", icon: IconLink },
]

export function TooltipToolbar() {
  return (
    <TooltipGroup side="bottom">
      <ButtonGroup aria-label="Formatting">
        {tools.map(({ label, keys, icon: Icon }) => (
          <TooltipTrigger
            key={label}
            content={
              <>
                {label}
                <Kbd keys={keys} size="sm" />
              </>
            }
            render={<Button variant="outline" size="icon" aria-label={label} />}
          >
            <Icon />
          </TooltipTrigger>
        ))}
      </ButtonGroup>
    </TooltipGroup>
  )
}
```

### Disabled

A disabled button can still explain why with `focusableWhenDisabled`, which keeps it hoverable and in the tab order. `disabled` on `Tooltip` turns the tooltip off.

```tsx title="components/examples/tooltip/disabled.tsx"
import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipDisabled() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Tooltip>
        <TooltipTrigger render={<Button disabled focusableWhenDisabled />}>
          Publish
        </TooltipTrigger>
        <TooltipContent>Add a title before publishing</TooltipContent>
      </Tooltip>
      <Tooltip disabled>
        <TooltipTrigger render={<Button variant="outline" />}>
          No tooltip
        </TooltipTrigger>
        <TooltipContent>You won’t see this</TooltipContent>
      </Tooltip>
    </div>
  )
}
```

### Long content

Text wraps at a comfortable width and never runs past the screen edge, even long URLs.

```tsx title="components/examples/tooltip/long-content.tsx"
import { IconInfoCircle } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipLongContent() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          <IconInfoCircle data-icon="inline-start" />
          Retention
        </TooltipTrigger>
        <TooltipContent>
          Deleted projects stay in the trash for 30 days. After that they’re
          removed for good, along with their deployments and logs.
        </TooltipContent>
      </Tooltip>
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          Webhook URL
        </TooltipTrigger>
        <TooltipContent>
          https://api.example.com/v1/hooks/8f3a2c91d7e64b0f9a1c5e2d7b8a4f60/deliveries
        </TooltipContent>
      </Tooltip>
    </div>
  )
}
```

### Controlled

Pass `open` and `onOpenChange` to drive the tooltip yourself. The reason tells you what opened or closed it.

```tsx title="components/examples/tooltip/controlled.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipControlled() {
  const [open, setOpen] = React.useState(false)
  const [reason, setReason] = React.useState("none")

  return (
    <div className="flex flex-col items-center gap-3">
      <div className="flex items-center gap-2">
        <Tooltip
          open={open}
          onOpenChange={(next, details) => {
            setOpen(next)
            setReason(details.reason)
          }}
        >
          <TooltipTrigger render={<Button variant="outline" />}>
            Hover or focus me
          </TooltipTrigger>
          <TooltipContent>Controlled tooltip</TooltipContent>
        </Tooltip>
        <Button
          variant="ghost"
          onClick={() => {
            setOpen((value) => !value)
            setReason("button")
          }}
        >
          {open ? "Hide" : "Show"}
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">
        Open: {String(open)} · last reason: {reason}
      </p>
    </div>
  )
}
```

### One tooltip, many triggers

Create a handle with `createTooltipHandle` and pass a `payload` from each trigger to share a single tooltip.

```tsx title="components/examples/tooltip/detached.tsx"
"use client"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  createTooltipHandle,
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

type Person = { name: string; initials: string; status: string }

const people: Person[] = [
  { name: "Ada Lovelace", initials: "AL", status: "Online" },
  { name: "Alan Turing", initials: "AT", status: "In a meeting" },
  { name: "Grace Hopper", initials: "GH", status: "Away" },
]

const presence = createTooltipHandle<Person>()

export function TooltipDetached() {
  return (
    <TooltipProvider>
      <div className="flex items-center gap-2">
        {people.map((person) => (
          <TooltipTrigger
            key={person.name}
            handle={presence}
            payload={person}
            aria-label={person.name}
            render={
              <button className="rounded-full outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden" />
            }
          >
            <Avatar>
              <AvatarFallback>{person.initials}</AvatarFallback>
            </Avatar>
          </TooltipTrigger>
        ))}
        <Tooltip handle={presence}>
          {({ payload }) => (
            <TooltipContent side="bottom">
              {payload ? `${payload.name} · ${payload.status}` : null}
            </TooltipContent>
          )}
        </Tooltip>
      </div>
    </TooltipProvider>
  )
}
```

### Right to left

Inside `dir="rtl"`, `inline-end` opens on the left and the shortcut moves to the other end.

```tsx title="components/examples/tooltip/rtl.tsx"
import { IconBookmark, IconHeart, IconShare2 } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Kbd } from "@/components/ui/kbd"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

const actions = [
  { label: "أعجبني", icon: IconHeart },
  { label: "حفظ", icon: IconBookmark, keys: "mod+d" },
  { label: "مشاركة", icon: IconShare2 },
]

export function TooltipRtl() {
  return (
    <div dir="rtl">
      <TooltipProvider>
        <div className="flex items-center gap-1">
          {actions.map(({ label, icon: Icon, keys }) => (
            <Tooltip key={label}>
              <TooltipTrigger
                render={
                  <Button variant="ghost" size="icon" aria-label={label} />
                }
              >
                <Icon />
              </TooltipTrigger>
              <TooltipContent side="inline-end" arrow>
                {label}
                {keys && <Kbd keys={keys} size="sm" />}
              </TooltipContent>
            </Tooltip>
          ))}
        </div>
      </TooltipProvider>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Focusing the trigger from the keyboard opens its tooltip right away. |
| `Esc` | Closes the tooltip and keeps focus on the trigger. |
| `Enter` `Space` | Activates the trigger and closes the tooltip, so it doesn’t cover what happens next. |

## Accessibility

- A tooltip is a visual hint, not a label. Give icon-only triggers an `aria-label` that matches the tooltip text.
- Nothing opens on touch, and a tap only does what the trigger does. Don’t put anything in a tooltip that people need; use a popover for that.
- Pointer users can move onto the tooltip to read or select it without it closing.
- With reduced motion on, the tooltip fades without scaling.

## API reference

Built on the Base UI tooltip. Every part accepts the props of the primitive it wraps.

### TooltipProvider

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `delay` | `number` | `300` | Milliseconds to rest on a trigger before opening. |
| `closeDelay` | `number` | `0` | Milliseconds before closing after leaving. |
| `timeout` | `number` | `400` | How long after one closes that the next opens instantly. |

### Tooltip

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open, details) => void` | – |  |
| `onOpenChangeComplete` | `(open) => void` | – | Called after the open or close animation. |
| `disabled` | `boolean` | `false` | Turns the tooltip off. |
| `disableHoverablePopup` | `boolean` | `false` | Close as soon as the pointer leaves the trigger. |
| `trackCursorAxis` | `"none" \| "x" \| "y" \| "both"` | `"none"` |  |
| `handle` | `TooltipHandle<Payload>` | – | From createTooltipHandle. |
| `children` | `ReactNode \| ({ payload }) => ReactNode` | – |  |

### TooltipTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `delay` | `number` | – | Overrides the provider delay for this trigger. |
| `closeDelay` | `number` | – |  |
| `closeOnClick` | `boolean` | `true` |  |
| `disabled` | `boolean` | `false` | Stops this trigger opening the tooltip. The element stays enabled. |
| `handle` | `TooltipHandle<Payload>` | – |  |
| `payload` | `Payload` | – |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="tooltip-trigger"` | The trigger. |
| `data-popup-open` | Present while its tooltip is open. |
| `data-trigger-disabled` | Present when the trigger can’t open the tooltip. |

### TooltipContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"top"` |  |
| `align` | `"start" \| "center" \| "end"` | `"center"` |  |
| `sideOffset` | `number \| (data) => number` | `6, or 8 with arrow` |  |
| `alignOffset` | `number` | `0` |  |
| `arrow` | `boolean` | `false` |  |
| `collisionPadding` | `number` | `8` |  |
| `arrowPadding` | `number` | `8` |  |
| `portalProps` | `TooltipPortal props` | – | Such as container or keepMounted. |

| Attribute | Description |
| --- | --- |
| `data-slot="tooltip-content"` | The popup. |
| `data-slot="tooltip-positioner"` | The positioning wrapper around the popup. |
| `data-side` | The side it opened on after flipping. |
| `data-align` | The alignment. |
| `data-instant` | "delay", "focus" or "dismiss" when it opens or closes without animation. |
| `data-starting-style` | Present while it animates in. |
| `data-ending-style` | Present while it animates out. |
| `--available-width` | Room between the trigger and the screen edge. |
| `--transform-origin` | The point the tooltip scales from. |

### TooltipArrow

Rendered for you by `arrow`. Use it directly only when composing your own content part.

| Attribute | Description |
| --- | --- |
| `data-slot="tooltip-arrow"` | The arrow. |
| `data-uncentered` | Present when it can’t point at the trigger’s center. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
