shadimage

Tooltip

A Base UI tooltip component

registry/8starlabs-ui/ui/tooltip.tsx

"use client";

import * as React from "react";
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";

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

type TooltipMode = "tooltip" | "popover";

const DESKTOP_TOOLTIP_QUERY =
  "(hover: hover) and (pointer: fine) and (min-width: 1024px)";

const TOOLTIP_CONTENT_CLASSNAME =
  "bg-foreground text-background z-50 w-fit origin-(--transform-origin) rounded-md px-3 py-1.5 text-xs text-balance transition-[transform,scale,opacity] data-[starting-style]:scale-95 data-[starting-style]:opacity-0 data-[ending-style]:scale-95 data-[ending-style]:opacity-0";

const TOOLTIP_ARROW_CLASSNAME =
  "bg-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]";

const TooltipModeContext = React.createContext<TooltipMode>("tooltip");

function getTooltipMode(query: MediaQueryList): TooltipMode {
  return query.matches ? "tooltip" : "popover";
}

function useTooltipMode(): TooltipMode {
  const [mode, setMode] = React.useState<TooltipMode>("tooltip");

  React.useEffect(() => {
    const query = window.matchMedia(DESKTOP_TOOLTIP_QUERY);
    const updateMode = () => setMode(getTooltipMode(query));

    updateMode();
    query.addEventListener("change", updateMode);

    return () => query.removeEventListener("change", updateMode);
  }, []);

  return mode;
}

function TooltipProvider({
  delayDuration = 0,
  ...props
}: Omit<TooltipPrimitive.Provider.Props, "delay"> & {
  delayDuration?: number;
}) {
  return <TooltipPrimitive.Provider delay={delayDuration} {...props} />;
}

function Tooltip({
  children,
  open,
  defaultOpen,
  onOpenChange,
  delayDuration,
  disableHoverableContent,
  ...props
}: Omit<TooltipPrimitive.Root.Props, "onOpenChange"> & {
  onOpenChange?: (open: boolean) => void;
  delayDuration?: number;
  disableHoverableContent?: boolean;
}) {
  const mode = useTooltipMode();

  return (
    <TooltipModeContext.Provider value={mode}>
      {mode === "tooltip" ? (
        <TooltipProvider delayDuration={delayDuration}>
          <TooltipPrimitive.Root
            open={open}
            defaultOpen={defaultOpen}
            onOpenChange={onOpenChange}
            disableHoverablePopup={disableHoverableContent}
            {...props}
          >
            {children}
          </TooltipPrimitive.Root>
        </TooltipProvider>
      ) : (
        <PopoverPrimitive.Root
          open={open}
          defaultOpen={defaultOpen}
          onOpenChange={onOpenChange}
        >
          {children}
        </PopoverPrimitive.Root>
      )}
    </TooltipModeContext.Provider>
  );
}

function TooltipTrigger({
  nativeButton,
  render,
  ...props
}: React.ComponentProps<"button"> & {
  render?: React.ReactElement;
  nativeButton?: boolean;
}) {
  const mode = React.useContext(TooltipModeContext);

  if (mode === "popover") {
    return (
      <PopoverPrimitive.Trigger
        data-slot="tooltip-trigger"
        nativeButton={nativeButton}
        render={render}
        {...props}
      />
    );
  }

  return (
    <TooltipPrimitive.Trigger
      data-slot="tooltip-trigger"
      render={render}
      {...props}
    />
  );
}

type TooltipContentProps = React.ComponentProps<"div"> &
  Pick<
    TooltipPrimitive.Positioner.Props,
    "align" | "side" | "sideOffset" | "alignOffset"
  > & {
    hideArrow?: boolean;
  };

function TooltipContent({
  className,
  align = "center",
  side,
  sideOffset = 0,
  alignOffset,
  hideArrow = false,
  children,
  ...props
}: TooltipContentProps) {
  const mode = React.useContext(TooltipModeContext);

  if (mode === "popover") {
    return (
      <PopoverPrimitive.Portal>
        <PopoverPrimitive.Positioner
          align={align}
          side={side}
          sideOffset={sideOffset}
          alignOffset={alignOffset}
          className="isolate z-50"
        >
          <PopoverPrimitive.Popup
            data-slot="tooltip-content"
            className={cn(TOOLTIP_CONTENT_CLASSNAME, className)}
            {...props}
          >
            {children}
            {!hideArrow && (
              <PopoverPrimitive.Arrow className={TOOLTIP_ARROW_CLASSNAME} />
            )}
          </PopoverPrimitive.Popup>
        </PopoverPrimitive.Positioner>
      </PopoverPrimitive.Portal>
    );
  }

  return (
    <TooltipPrimitive.Portal>
      <TooltipPrimitive.Positioner
        align={align}
        side={side}
        sideOffset={sideOffset}
        alignOffset={alignOffset}
        className="isolate z-50"
      >
        <TooltipPrimitive.Popup
          data-slot="tooltip-content"
          className={cn(TOOLTIP_CONTENT_CLASSNAME, className)}
          {...props}
        >
          {children}
          {!hideArrow && (
            <TooltipPrimitive.Arrow className={TOOLTIP_ARROW_CLASSNAME} />
          )}
        </TooltipPrimitive.Popup>
      </TooltipPrimitive.Positioner>
    </TooltipPrimitive.Portal>
  );
}

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
Interaction
tooltip

DependenciesBundle weight · light

ui-primitives
@base-ui/react