OperantAI Workflow Design System
ComfortableCompact

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.

PropTypeDefaultDescription
delayDurationnumber300Milliseconds of hover-intent before a tooltip opens. Overrides Radix's own default when the consumer doesn't set one.
skipDelayDurationnumberRadix defaultGrace 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.

PropTypeDefaultDescription
delayDurationnumberinherited from TooltipProviderPer-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>

PropTypeDefaultDescription
sideOffsetnumber6Pixel gap between the trigger and the tooltip.
side"top" | "right" | "bottom" | "left""top"Preferred side to render on.
classNamestring—Merged with .ods-tooltip.

Renders inside a Radix Portal; always renders TooltipPrimitive.Arrow with .ods-tooltip-arrow.

<SimpleTooltip>

PropTypeDefaultDescription
labelstring—Required. The one-line tooltip text.
childrenReactNode—Required. Trigger content — plain text or an icon, not another interactive element.
delayDurationnumber—Forwarded to Tooltip (Root).
triggerPropsOmit<TooltipTriggerProps, "children">—Forwarded to TooltipTrigger (e.g. className, aria-label).
contentPropsOmit<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-visible and mouse hover, with a configurable hover-intent delay (delayDuration, default 300ms via TooltipProvider) — 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-describedby relationship from trigger to content) — it supplements, but should not replace, an aria-label on icon-only triggers (see the Icon-only example, which sets both).
  • Esc dismisses an open tooltip.
  • Never put interactive content (a button, a link) inside TooltipContent — tooltips are not focusable/clickable surfaces; use Popover for that.