Tooltip
SaveA 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 tooltipPre-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-runUsage
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
| Prop | Type | Default | Description |
|---|---|---|---|
| TooltipProvider delay / closeDelay / timeout | number | — | Shared hover delays in ms. Once one tooltip is open, neighbours open instantly within timeout (default 400). |
| Tooltip open / defaultOpen / onOpenChange | boolean / boolean / (open, details) => void | — | Root state. Leave open unset for an uncontrolled tooltip. |
| Tooltip disabled | boolean | false | Turns the tooltip off without removing it. |
| TooltipTrigger delay | number | 600 | Hover delay before opening, in ms. Keyboard focus opens it immediately. |
| TooltipTrigger | Base 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 sideOffset | number | 6 | Gap from the trigger, in pixels. |
| TooltipContent arrow | boolean | true | Shows 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.tsxShowHide
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>
);
}