Skip to content
Seamless UI

Tooltip

Save

A short label that appears on hover and keyboard focus, built on Base UI Tooltip.

Category
Core
Version
0.1.0
Frameworks
Next.js, Vite + React
Styling
Tailwind CSS v4

Preview

Tooltip preview

Installation

npx @seamlessui/cli add tooltip

Pre-release@seamlessui/cli is not on npm yet. Install from a local build.

Review the plan first. A dry run lists every file and package without changing anything:

npx @seamlessui/cli add tooltip --dry-run

Usage

tooltip-example.tsx
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/seamless/ui/tooltip";

const iconButton =
  "inline-flex size-9 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-700 " +
  "transition-colors duration-150 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500 " +
  "motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800";

export default function TooltipExample() {
  return (
    <TooltipProvider>
      <div className="flex items-center gap-2">
        <Tooltip>
          <TooltipTrigger aria-label="Copy link" className={iconButton}>
            <svg aria-hidden="true" viewBox="0 0 16 16" fill="none" className="size-4">
              <rect x="5" y="5" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
              <path d="M3 10.5V4a1 1 0 0 1 1-1h6.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
            </svg>
          </TooltipTrigger>
          <TooltipContent>Copy link</TooltipContent>
        </Tooltip>
        <Tooltip>
          <TooltipTrigger aria-label="Download" className={iconButton}>
            <svg aria-hidden="true" viewBox="0 0 16 16" fill="none" className="size-4">
              <path d="M8 2.5v8M4.5 7 8 10.5 11.5 7M3 13.5h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </TooltipTrigger>
          <TooltipContent side="bottom">Download</TooltipContent>
        </Tooltip>
      </div>
    </TooltipProvider>
  );
}

API

PropTypeDefaultDescription
TooltipProvider delay / closeDelay / timeoutnumber—Shared hover delays in ms. Once one tooltip is open, neighbours open instantly within timeout (default 400).
Tooltip open / defaultOpen / onOpenChangeboolean / boolean / (open, details) => void—Root state. Leave open unset for an uncontrolled tooltip.
Tooltip disabledbooleanfalseTurns the tooltip off without removing it.
TooltipTrigger delaynumber600Hover delay before opening, in ms. Keyboard focus opens it immediately.
TooltipTriggerBase UI Tooltip.Trigger props—The hovered or focused element, a <button> by default. Pass render={<Button />} to reuse your own.
TooltipContent side"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end""top"Preferred side of the trigger. Flips when there is not enough room.
TooltipContent align"start" | "center" | "end""center"Alignment against the trigger.
TooltipContent sideOffsetnumber6Gap from the trigger, in pixels.
TooltipContent arrowbooleantrueShows an arrow pointing at the trigger.

Accessibility

  • The tooltip opens on hover and on keyboard focus, and Escape closes it.
  • Pointer users can move onto the tooltip without it closing, so the text stays readable.
  • Base UI does not link the tooltip text to the trigger, so give icon-only triggers an aria-label that says the same thing.
  • Do not put links or buttons inside a tooltip; use a Popover for interactive content.
  • With reduced motion the fade and scale transitions are removed.

Files

Installed into src/components/seamless, or components/seamless when the project has no src directory.

  • src/components/seamless/ui/tooltip.tsxtooltip
  • npm packages: @base-ui/react@^1.9.0React and Tailwind CSS v4 required

Source

The exact source the CLI installs, from /r/items/react/tooltip/0.1.0.json.

ui/tooltip.tsxShow
ui/tooltip.tsx
"use client";

import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";

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

function cx(...classes: (string | false | null | undefined)[]) {
  return classes.filter(Boolean).join(" ");
}

/** Prepends base classes while keeping Base UI's state-function form of className working. */
function withBase<State>(base: string, className: ClassName<State>) {
  return typeof className === "function" ? (state: State) => cx(base, className(state)) : cx(base, className);
}

/**
 * Shares hover delays across tooltips, so moving between triggers opens the next tooltip
 * instantly. Optional; wrap your app or a toolbar in it.
 */
export const TooltipProvider = BaseTooltip.Provider;

/** Groups the tooltip parts and holds its open state. */
export const Tooltip = BaseTooltip.Root;

/**
 * The element that shows the tooltip on hover and keyboard focus. Renders a `<button>`;
 * pass `render={<Button />}` to reuse your button, and give icon-only triggers an `aria-label`.
 */
export const TooltipTrigger = BaseTooltip.Trigger;

type PositionerProps = BaseTooltip.Positioner.Props;

export interface TooltipContentProps extends BaseTooltip.Popup.Props {
  /** Side of the trigger to place the tooltip on. Flips when there is not enough room. */
  side?: PositionerProps["side"];
  /** Alignment against the trigger on the chosen side. */
  align?: PositionerProps["align"];
  /** Gap between the trigger and the tooltip, in pixels. */
  sideOffset?: PositionerProps["sideOffset"];
  /** Shows an arrow pointing at the trigger. */
  arrow?: boolean;
  /** Classes for the positioner wrapper, for example to change the z-index. */
  positionerClassName?: string;
  /** Element to portal into. Defaults to `document.body`. */
  container?: BaseTooltip.Portal.Props["container"];
}

/** The tooltip bubble, rendered in a portal and positioned against the trigger. */
export function TooltipContent({
  side = "top",
  align = "center",
  sideOffset = 6,
  arrow = true,
  positionerClassName,
  container,
  className,
  children,
  ...props
}: TooltipContentProps) {
  return (
    <BaseTooltip.Portal container={container}>
      <BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset} className={cx("z-50", positionerClassName)}>
        <BaseTooltip.Popup
          className={withBase(
            cx(
              "relative max-w-xs origin-(--transform-origin) rounded-md bg-zinc-900 px-2 py-1 text-xs font-medium text-white shadow-md",
              "dark:bg-zinc-100 dark:text-zinc-900",
              "transition-[opacity,scale] duration-100 ease-out motion-reduce:transition-none",
              "data-[starting-style]:opacity-0 data-[ending-style]:opacity-0",
              "motion-safe:data-[starting-style]:scale-95 motion-safe:data-[ending-style]:scale-95",
              // Skip the fade when moving between triggers inside a TooltipProvider.
              "data-[instant]:transition-none",
            ),
            className,
          )}
          {...props}
        >
          {arrow ? (
            <BaseTooltip.Arrow
              className={cx(
                "size-2 rotate-45 bg-zinc-900 dark:bg-zinc-100",
                "data-[side=bottom]:-top-1 data-[side=top]:-bottom-1 data-[side=left]:-right-1 data-[side=right]:-left-1",
              )}
            />
          ) : null}
          {children}
        </BaseTooltip.Popup>
      </BaseTooltip.Positioner>
    </BaseTooltip.Portal>
  );
}