# Tooltip

> Lightweight tooltip with four sides, show delay and keyboard focus support.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/tooltip
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/tooltip.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/tooltip-demo.tsx"
import { Bold, Italic, Link2, Strikethrough, Underline } from "lucide-react"

import { Tooltip } from "@/components/tooltip"

const tools = [
  { icon: Bold, label: "Bold", keys: "⌘B" },
  { icon: Italic, label: "Italic", keys: "⌘I" },
  { icon: Underline, label: "Underline", keys: "⌘U" },
  { icon: Strikethrough, label: "Strikethrough", keys: "⇧⌘X" },
  { icon: Link2, label: "Insert link", keys: "⌘K" },
]

export default function TooltipDemo() {
  return (
    <div className="flex flex-col items-center gap-16">
      <div className="flex gap-1 rounded-xl border bg-card p-1 shadow-sm">
        {tools.map(({ icon: Icon, label, keys }, index) => (
          <Tooltip
            key={label}
            open={index === 0 ? true : undefined}
            content={
              <span className="flex items-center gap-2">
                {label}
                <kbd className="font-sans text-background/60">{keys}</kbd>
              </span>
            }
          >
            <button
              type="button"
              aria-label={label}
              className="flex size-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
            >
              <Icon className="size-4" />
            </button>
          </Tooltip>
        ))}
      </div>
      <div className="flex gap-3">
        {(["left", "bottom", "right"] as const).map((side) => (
          <Tooltip key={side} side={side} content={`Tooltip on the ${side}`}>
            <button type="button" className="h-9 rounded-lg border bg-background px-4 text-sm font-medium capitalize hover:bg-accent">
              {side}
            </button>
          </Tooltip>
        ))}
      </div>
    </div>
  )
}
```

## Source

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

import * as React from "react"

import { cn } from "@/lib/utils"

type Side = "top" | "right" | "bottom" | "left"

export interface TooltipProps extends Omit<React.ComponentProps<"span">, "content"> {
  content: React.ReactNode
  side?: Side
  /** Delay before showing, in ms */
  delay?: number
  showArrow?: boolean
  /** Keep the tooltip visible (useful for docs and previews) */
  open?: boolean
}

const position: Record<Side, string> = {
  top: "bottom-full left-1/2 mb-2 -translate-x-1/2 slide-in-from-bottom-1",
  bottom: "top-full left-1/2 mt-2 -translate-x-1/2 slide-in-from-top-1",
  left: "right-full top-1/2 mr-2 -translate-y-1/2 slide-in-from-right-1",
  right: "left-full top-1/2 ml-2 -translate-y-1/2 slide-in-from-left-1",
}

const arrow: Record<Side, string> = {
  top: "top-full left-1/2 -translate-x-1/2 -translate-y-1/2",
  bottom: "bottom-full left-1/2 -translate-x-1/2 translate-y-1/2",
  left: "left-full top-1/2 -translate-x-1/2 -translate-y-1/2",
  right: "right-full top-1/2 translate-x-1/2 -translate-y-1/2",
}

export function Tooltip({ content, side = "top", delay = 300, showArrow = true, open, className, children, ...props }: TooltipProps) {
  const [visible, setVisible] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)
  const id = React.useId()
  const shown = open ?? visible

  const show = () => {
    clearTimeout(timer.current)
    timer.current = setTimeout(() => setVisible(true), delay)
  }
  const hide = () => {
    clearTimeout(timer.current)
    setVisible(false)
  }

  React.useEffect(() => () => clearTimeout(timer.current), [])

  return (
    <span
      aria-describedby={shown ? id : undefined}
      onPointerEnter={show}
      onPointerLeave={hide}
      onFocus={show}
      onBlur={hide}
      onKeyDown={(event) => event.key === "Escape" && hide()}
      className={cn("relative inline-flex", className)}
      {...props}
    >
      {children}
      {shown && (
        <span
          id={id}
          role="tooltip"
          className={cn(
            "pointer-events-none absolute z-40 w-max max-w-64 rounded-md bg-foreground px-2.5 py-1.5 text-xs text-background shadow-md animate-in fade-in zoom-in-95 duration-150",
            position[side]
          )}
        >
          {content}
          {showArrow && <span className={cn("absolute size-2 rotate-45 rounded-[1px] bg-foreground", arrow[side])} />}
        </span>
      )}
    </span>
  )
}
```
