Tooltip shows a brief, one-line hint on hover or keyboard focus — unlike Popover, it never contains interactive content and opens automatically on hover-intent rather than a click. Most consumers should reach for SimpleTooltip, which composes Tooltip + TooltipTrigger + TooltipContent from a single label string.
Installation
HTML — already included if you loaded the 3 stylesheets — see the Installation guide.
React
npm install @operant/ui
@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.
Usage
HTML
Plain HTML has no built-in hover-intent delay or focus-visible-only trigger logic — the native title attribute is the closest zero-JS approximation, though it lacks styling and a configurable delay. For the styled floating panel shown below you need the React version, or your own small hover/focus script applying .ods-tooltip to the resulting panel.
<button class="ods-btn -secondary" aria-describedby="tip-1">Hover me</button>
<div class="ods-tooltip" role="tooltip" id="tip-1">A brief, one-line hint.</div>
React
Wrap your app (or a subtree) once in TooltipProvider so every Tooltip beneath it shares hover-intent timing.
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@operant/ui";
export default function Example() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="ods-btn -secondary">Hover me</TooltipTrigger>
<TooltipContent>A brief, one-line hint.</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
Examples
Basic
HTML
<button class="ods-btn -secondary" aria-describedby="tip-basic">Hover me</button>
<div class="ods-tooltip" role="tooltip" id="tip-basic">A brief, one-line hint.</div>
React
<Tooltip>
<TooltipTrigger className="ods-btn -secondary">Hover me</TooltipTrigger>
<TooltipContent>A brief, one-line hint.</TooltipContent>
</Tooltip>
Aligned to top
side/sideOffset control placement; the default sideOffset on TooltipContent is 6.
HTML
<div class="ods-tooltip" role="tooltip" data-side="top">Positioned above the trigger.</div>
React
<Tooltip>
<TooltipTrigger className="ods-btn -secondary">Hover me</TooltipTrigger>
<TooltipContent side="top" sideOffset={10}>
Positioned above the trigger.
</TooltipContent>
</Tooltip>
Simple (convenience wrapper)
SimpleTooltip collapses the three-part composition into a single component with a label string. Pass trigger styling via triggerProps, not by nesting a separate <Button> (that would create an invalid <button><button> tree, same reasoning as PopoverTrigger).
HTML
<button class="ods-btn -danger-ghost" aria-describedby="tip-delete">Delete</button>
<div class="ods-tooltip" role="tooltip" id="tip-delete">Delete this run</div>
React
<SimpleTooltip label="Delete this run" triggerProps={{ className: "ods-btn -danger-ghost" }}>
Delete
</SimpleTooltip>
Icon-only trigger
The most common real use of SimpleTooltip: an icon-only button, where the required aria-label (per AGENTS.md #8: icon-only controls need one) and the tooltip's visible label carry the same text, serving sighted and screen-reader users identically.
HTML
<button class="ods-iconbtn" aria-label="Delete this run" aria-describedby="tip-icon">
<svg width="16" height="16" aria-hidden="true"><!-- trash icon --></svg>
</button>
<div class="ods-tooltip" role="tooltip" id="tip-icon">Delete this run</div>
React
import { Trash } from "react-bootstrap-icons";
<SimpleTooltip
label="Delete this run"
triggerProps={{ className: "ods-iconbtn", "aria-label": "Delete this run" }}
>
<Trash />
</SimpleTooltip>
API Reference
<TooltipProvider>
Wraps Radix TooltipPrimitive.Provider, mounted once near the app root so every Tooltip beneath it shares hover-intent timing.
| Prop | Type | Default | Description |
|---|---|---|---|
delayDuration | number | 300 | Milliseconds of hover-intent before a tooltip opens. Overrides Radix's own default when the consumer doesn't set one. |
skipDelayDuration | number | Radix default | Grace period for moving between multiple tooltip triggers without re-incurring the full delay. |
<Tooltip>
Direct re-export of Radix TooltipPrimitive.Root — owns open state for a single tooltip. Must be rendered beneath a TooltipProvider.
| Prop | Type | Default | Description |
|---|---|---|---|
delayDuration | number | inherited from TooltipProvider | Per-instance override of hover-intent timing. |
open / defaultOpen / onOpenChange | — | — | Controlled/uncontrolled open state, same shape as other Radix primitives. |
<TooltipTrigger>
Direct re-export of Radix TooltipPrimitive.Trigger, with no .ods-* class of its own and no asChild — same reasoning as PopoverTrigger: it renders a real <button>, so apply .ods-btn/.ods-iconbtn/-<variant> via className yourself rather than nesting another interactive component inside it.
<TooltipContent>
| Prop | Type | Default | Description |
|---|---|---|---|
sideOffset | number | 6 | Pixel gap between the trigger and the tooltip. |
side | "top" | "right" | "bottom" | "left" | "top" | Preferred side to render on. |
className | string | — | Merged with .ods-tooltip. |
Renders inside a Radix Portal; always renders TooltipPrimitive.Arrow with .ods-tooltip-arrow.
<SimpleTooltip>
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Required. The one-line tooltip text. |
children | ReactNode | — | Required. Trigger content — plain text or an icon, not another interactive element. |
delayDuration | number | — | Forwarded to Tooltip (Root). |
triggerProps | Omit<TooltipTriggerProps, "children"> | — | Forwarded to TooltipTrigger (e.g. className, aria-label). |
contentProps | Omit<TooltipContentProps, "children"> | — | Forwarded to TooltipContent (e.g. side, align, sideOffset). |
HTML-side classes: .ods-tooltip (max-width 240px, --color-tooltip-bg/--color-tooltip-text, radius-sm, shadow-sm), .ods-tooltip-arrow (fills with --color-tooltip-bg).
Accessibility
- Radix Tooltip only shows on
:focus-visibleand mouse hover, with a configurable hover-intent delay (delayDuration, default 300ms viaTooltipProvider) — it does not show on a plain mouse-down/click, and it does not show on touch by default. - Because it opens on keyboard focus as well as hover, a tooltip attached to any focusable trigger is available to keyboard users automatically — no extra wiring needed.
- The tooltip content is exposed to assistive tech via Radix's ARIA wiring (an
aria-describedbyrelationship from trigger to content) — it supplements, but should not replace, anaria-labelon icon-only triggers (see the Icon-only example, which sets both). Escdismisses an open tooltip.- Never put interactive content (a button, a link) inside
TooltipContent— tooltips are not focusable/clickable surfaces; usePopoverfor that.