Overlays

Tooltip

Radix tooltip with a 500ms first-open delay, instant adjacent opens via skip-delay, 125ms origin-aware enter, near-instant fade exit.

Install

npx shadcn@latest add @paragon/tooltip

tooltip.tsx

"use client";

import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";

/*
 * Tooltip physics tuned by frequency: 500ms delay before the first open
 * (never interrupt a scanning cursor), then TooltipProvider's
 * skipDelayDuration lets adjacent tooltips open instantly. Enter is a
 * quick 125ms origin-aware scale; exit is a near-instant 50ms fade —
 * a leaving tooltip should not fight for attention.
 *
 * Wrap toolbars/regions in <TooltipProvider> so the skip-delay window
 * is shared across neighboring tooltips.
 */
const tooltipStyles = `
@keyframes pg-tooltip-in { from { opacity: 0; scale: 0.97; } }
@keyframes pg-tooltip-out { to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  @keyframes pg-tooltip-in { from { opacity: 0; } }
}
`;

function TooltipProvider({
  delayDuration = 500,
  skipDelayDuration = 300,
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
  return (
    <TooltipPrimitive.Provider
      delayDuration={delayDuration}
      skipDelayDuration={skipDelayDuration}
      {...props}
    />
  );
}

function Tooltip({
  delayDuration = 500,
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
  return (
    <TooltipPrimitive.Root
      data-slot="tooltip"
      delayDuration={delayDuration}
      {...props}
    />
  );
}

function TooltipTrigger(
  props: React.ComponentProps<typeof TooltipPrimitive.Trigger>,
) {
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}

function TooltipContent({
  className,
  sideOffset = 6,
  ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
  return (
    <>
      {/* Hoisted outside the Portal: React 19 keeps a hoistable <style> as a
          child node, and the Radix Portal enforces a single child. */}
      <style href="paragon-tooltip" precedence="paragon">{tooltipStyles}</style>
      <TooltipPrimitive.Portal>
        <TooltipPrimitive.Content
          data-slot="tooltip-content"
          sideOffset={sideOffset}
          className={cn(
            "z-50 origin-(--radix-tooltip-content-transform-origin) rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground",
            "data-[state=open]:animate-[pg-tooltip-in_125ms_var(--ease-out)]",
            "data-[state=closed]:animate-[pg-tooltip-out_50ms_var(--ease-exit)_forwards]",
            className,
          )}
          {...props}
        />
      </TooltipPrimitive.Portal>
    </>
  );
}

export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent };