Skip to content

Tooltip Primitive

Primitive Tooltip combines Popup with pointer-enter and hover-away behavior.

Usage

tsx
import {
  Tooltip,
  TooltipTrigger,
  TooltipContent,
  useTooltip,
} from '@suis-ui/primitives';

The recommended primitive structure combines a trigger anchor with tooltip content rendered in a portal.

text
Tooltip
├── TooltipTrigger
│   └── HTMLElement child
└── TooltipContent
    └── Portal
        └── tooltip content
tsx
<Tooltip openDelay={300} closeDelay={100} placement="top">
  <Tooltip.Trigger>
    <button type="button">Hover me</button>
  </Tooltip.Trigger>
  <Tooltip.Content>
    Tooltip content
  </Tooltip.Content>
</Tooltip>

Props

Delay

NameTypeDefaultDescription
openDelaynumber0Delay before opening after pointer enter.
closeDelaynumber0Delay before closing after pointer leave.
childrenJSX.ElementRequiredTooltip composition.

Tooltip is built on Popup, so it also accepts Popup state and positioning props.

NameTypeDefaultDescription
openbooleanfalsePopup open request value.
placementPlacementFloating UI defaultPreferred placement.
strategyStrategyFloating UI defaultCSS positioning strategy.
offsetOffsetOptions-Enables offset middleware.
shiftShiftOptions | boolean-Enables shift middleware.
flipFlipOptions | boolean-Enables flip middleware.
autoUpdateAutoUpdateOptions | booleantrueRecomputes position when layout changes.
middlewareMiddleware[]-Additional Floating UI middleware.

Component

Tooltip.Trigger

Registers a DOM child as the popup anchor, opens after openDelay on pointer enter, and closes after closeDelay on hover away.

When the trigger registers an anchor, the anchor receives aria-describedby pointing to the tooltip content id.

NameTypeDefaultDescription
childrenJSX.Element-DOM element registered as the trigger anchor.

Tooltip.Content

Renders popup content in a portal, applies role="tooltip", and receives the generated id.

NameTypeDefaultDescription
asTdivElement or component rendered as the content.
childrenJSX.Element-Content rendered inside the tooltip.
Selected element propsOmit<ComponentProps<T>, 'children' | 'style'>-Props forwarded to the content element. They are merged with computed popup style.

Hooks

useTooltip

Use useTooltip under a Tooltip provider to read tooltip metadata, popup state, and the popup open action.

Signature

ts
const [context, actions] = useTooltip();
ts
const [context, actions]: readonly [
  {
    id: string;
    openDelay?: number;
    closeDelay?: number;
    anchor: Element | null;
    element: HTMLElement | null;
    position: ComputePositionReturn | null;
    open: boolean;
    mount: boolean;
  },
  {
    requestOpen: (open: boolean) => void;
  },
] = useTooltip();

Calling it outside a Tooltip provider fails because there is no context to read.

Context

context merges Tooltip context and Popup context.

NameTypeDescription
context.idstringUnique id applied to tooltip content and referenced by the trigger's aria-describedby.
context.openDelaynumberDelay before opening after pointer enter.
context.closeDelaynumberDelay before closing after pointer leave.
context.anchorElement | nullPopup anchor registered by Tooltip.Trigger.
context.elementHTMLElement | nullTooltip element rendered by Tooltip.Content in the portal.
context.positionComputePositionReturn | nullTooltip position computed by Floating UI.
context.openbooleanMost recently requested tooltip open state.
context.mountbooleanWhether tooltip content is actually rendered in the portal.

Tooltip extensions should use only the id, delay, and popup state fields listed above.

Actions

NameTypeDescription
actions.requestOpen(open: boolean) => voidRequests the tooltip popup open state.

Behavior

The default Tooltip.Trigger already handles pointer enter and hover-away behavior. Use useTooltip when a custom trigger or content component needs to read state or directly request open state while staying inside the same tooltip context.

openDelay and closeDelay are used by the default Tooltip.Trigger pointer behavior. Calling requestOpen(true) or requestOpen(false) directly sends an open request immediately, without applying those delays.

id is applied to Tooltip.Content and connected from the trigger through aria-describedby. Custom content should keep that id to preserve the screen reader relationship.

Example

tsx
const TooltipStateLabel = () => {
  const [context] = useTooltip();

  return (
    <span data-open={context.open}>
      {context.open ? 'Open' : 'Closed'}
    </span>
  );
};

Examples

Basic Tooltip

tsx
<Tooltip placement="top" offset={6}>
  <Tooltip.Trigger>
    <button type="button">Hover me</button>
  </Tooltip.Trigger>
  <Tooltip.Content>
    Tooltip content
  </Tooltip.Content>
</Tooltip>

Delayed Tooltip

tsx
<Tooltip openDelay={300} closeDelay={100} placement="right">
  <Tooltip.Trigger>
    <button type="button">Help</button>
  </Tooltip.Trigger>
  <Tooltip.Content>
    Opens after a short delay.
  </Tooltip.Content>
</Tooltip>

Custom Content Element

tsx
<Tooltip placement="bottom" shift flip>
  <Tooltip.Trigger>
    <button type="button">Status</button>
  </Tooltip.Trigger>
  <Tooltip.Content as="section" aria-label="Status details">
    The job is running.
  </Tooltip.Content>
</Tooltip>