Tooltip

Delayed, keyboard-accessible tooltips with shortcut hints.

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/tooltip.tsx

    components/ui/tooltip.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { Bold, Italic, Link2, Underline } from "lucide-react";
    import { useId, useRef, useState, type ReactElement, type ReactNode } from "react";
    
    type TooltipProps = {
      content: ReactNode;
      children: ReactElement;
      side?: "top" | "bottom";
      /** Milliseconds before showing */
      delay?: number;
    };
    
    export function Tooltip({ content, children, side = "top", delay = 150 }: TooltipProps) {
      const [open, setOpen] = useState(false);
      const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
      const id = useId();
      const show = () => {
        clearTimeout(timer.current);
        timer.current = setTimeout(() => setOpen(true), delay);
      };
      const hide = () => {
        clearTimeout(timer.current);
        setOpen(false);
      };
    
      return (
        <span className="relative inline-flex" onMouseEnter={show} onMouseLeave={hide} onFocus={show} onBlur={hide} aria-describedby={open ? id : undefined}>
          {children}
          <AnimatePresence>
            {open && (
              <motion.span
                id={id}
                role="tooltip"
                initial={{ opacity: 0, y: side === "top" ? 4 : -4, scale: 0.95 }}
                animate={{ opacity: 1, y: 0, scale: 1 }}
                exit={{ opacity: 0, scale: 0.95 }}
                transition={{ duration: 0.12 }}
                className={`pointer-events-none absolute left-1/2 z-50 -translate-x-1/2 whitespace-nowrap rounded-md bg-zinc-900 px-2 py-1 text-xs font-medium text-white shadow-lg ${
                  side === "top" ? "bottom-full mb-2" : "top-full mt-2"
                }`}
              >
                {content}
              </motion.span>
            )}
          </AnimatePresence>
        </span>
      );
    }
    
    export default function Demo() {
      const tools = [
        { icon: Bold, label: "Bold", keys: "⌘B" },
        { icon: Italic, label: "Italic", keys: "⌘I" },
        { icon: Underline, label: "Underline", keys: "⌘U" },
        { icon: Link2, label: "Insert link", keys: "⌘K" },
      ];
      return (
        <div className="flex gap-1 rounded-xl border border-zinc-200 bg-white p-1 shadow-sm">
          {tools.map((t) => (
            <Tooltip
              key={t.label}
              content={
                <span className="flex items-center gap-2">
                  {t.label} <kbd className="font-mono text-zinc-400">{t.keys}</kbd>
                </span>
              }
            >
              <button aria-label={t.label} className="grid size-9 place-items-center rounded-lg text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900">
                <t.icon className="size-4" />
              </button>
            </Tooltip>
          ))}
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.