Communication
Tooltip
Plain and rich tooltips.
Plain tooltips briefly name an element, usually an icon button. Rich tooltips add a title, more text and an optional action. Neither should hold information people can't get elsewhere.
Installation
npx @grenishrai/expressive add tooltipThis copies tooltip.tsx into your components folder, along with what it needs from Expressive (utils, motion, styles), and installs @base-ui/react.
Usage
import { RichTooltip, Tooltip, TooltipProvider } from "@/components/expressive/tooltip";
<TooltipProvider>
<Tooltip content="Search">
<IconButton aria-label="Search"><SearchIcon /></IconButton>
</Tooltip>
</TooltipProvider>Props
Tooltip
| Prop | Type | Default | Description |
|---|---|---|---|
| content | ReactNode | — | Tooltip text. |
| children | ReactElement | — | The trigger. A single element that accepts a ref. |
| side / align | "top" | "bottom" | "left" | "right" / "start" | "center" | "end" | "top" / "center" | Placement. |
| open / defaultOpen / onOpenChange | — | Control the open state. | |
| motion | "expressive" | "standard" | "fluid" | "none" | theme default | Motion scheme for this component's transitions. none turns animation off. |
RichTooltip
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | Optional title. |
| actions | ReactNode | — | Text buttons. |
Accessibility
- Opens on hover and keyboard focus, closes on Escape.
- A tooltip doesn't replace an accessible name: keep
aria-labelon icon buttons. - Wrap toolbars in TooltipProvider so moving between triggers opens the next tooltip instantly.
Source
This is the file the CLI writes into your project, shown with the default import paths. It's yours to change.
"use client";
import * as React from "react";
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
import { cn } from "@/lib/expressive/utils";
import type { MotionProp } from "@/lib/expressive/use-motion";
/*
* Plain tooltips label an element briefly; rich tooltips add a title and
* actions. Wrap the app (or a toolbar) in <TooltipProvider> so neighbouring
* tooltips open instantly once one is showing.
*/
export const TooltipProvider = BaseTooltip.Provider;
interface TooltipBaseProps {
/** The trigger. Must be a single element that accepts a ref, e.g. an IconButton. */
children: React.ReactElement;
side?: BaseTooltip.Positioner.Props["side"];
align?: BaseTooltip.Positioner.Props["align"];
motion?: MotionProp;
className?: string;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: BaseTooltip.Root.Props["onOpenChange"];
}
export interface TooltipProps extends TooltipBaseProps {
content: React.ReactNode;
}
const popupMotion =
"origin-(--transform-origin) transition-[transform,opacity] spring-effects-fast data-starting-style:scale-90 data-starting-style:opacity-0 data-ending-style:scale-90 data-ending-style:opacity-0";
export function Tooltip({ children, content, side = "top", align = "center", motion, className, ...root }: TooltipProps) {
return (
<BaseTooltip.Root {...root}>
<BaseTooltip.Trigger render={children} />
<BaseTooltip.Portal>
<BaseTooltip.Positioner side={side} align={align} sideOffset={4} className="z-50">
<BaseTooltip.Popup
data-slot="tooltip"
data-motion={motion}
className={cn(
"flex min-h-6 max-w-50 items-center rounded-shape-extra-small bg-inverse-surface px-2 py-1 text-body-small text-inverse-on-surface",
popupMotion,
className,
)}
>
{content}
</BaseTooltip.Popup>
</BaseTooltip.Positioner>
</BaseTooltip.Portal>
</BaseTooltip.Root>
);
}
export interface RichTooltipProps extends TooltipBaseProps {
title?: React.ReactNode;
content: React.ReactNode;
/** Text buttons, e.g. `<Button variant="text" size="sm">Learn more</Button>`. */
actions?: React.ReactNode;
}
export function RichTooltip({ children, title, content, actions, side = "bottom", align = "start", motion, className, ...root }: RichTooltipProps) {
return (
<BaseTooltip.Root {...root}>
<BaseTooltip.Trigger render={children} />
<BaseTooltip.Portal>
<BaseTooltip.Positioner side={side} align={align} sideOffset={4} className="z-50">
<BaseTooltip.Popup
data-slot="rich-tooltip"
data-motion={motion}
className={cn(
"flex max-w-80 flex-col gap-1 rounded-shape-medium bg-surface-container px-4 pt-3 pb-2 text-body-medium text-on-surface-variant shadow-elevation-2",
popupMotion,
!actions && "pb-3",
className,
)}
>
{title && <div className="text-title-small text-on-surface">{title}</div>}
<div>{content}</div>
{actions && <div className="-ml-3 flex gap-2 pt-1">{actions}</div>}
</BaseTooltip.Popup>
</BaseTooltip.Positioner>
</BaseTooltip.Portal>
</BaseTooltip.Root>
);
}