Skip to main content
Markdown

Tooltip

Tooltip displays a floating label on hover or focus to provide brief contextual information. It wraps the Spar headless Tooltip primitive and adds Takeoff visual vocabulary — variant, header, description, and arrow slots.

Usage

import { Tooltip } from '@takeoff-ui/react-spar';
<Tooltip>
<Tooltip.Trigger />
<Tooltip.Content>
<Tooltip.Header />
<Tooltip.Description />
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip>

To share delay configuration across a group of tooltips, wrap them in Tooltip.Provider:

<Tooltip.Provider delayDuration={300} skipDelayDuration={150}>
{/* multiple <Tooltip /> instances */}
</Tooltip.Provider>

Playground


function PlaygroundDemo() {
  return (
    <Tooltip>
      <Tooltip.Trigger as={Button}>Hover me</Tooltip.Trigger>
      <Tooltip.Content>
        <Tooltip.Description>Simple tooltip</Tooltip.Description>
      </Tooltip.Content>
    </Tooltip>
  );
}

render(<PlaygroundDemo />);

Variants


function VariantsDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Tooltip>
        <Tooltip.Trigger as={Button}>White</Tooltip.Trigger>
        <Tooltip.Content variant="white">
          <Tooltip.Description>White variant</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Dark</Tooltip.Trigger>
        <Tooltip.Content variant="dark">
          <Tooltip.Description>Dark variant</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Info</Tooltip.Trigger>
        <Tooltip.Content variant="info">
          <Tooltip.Description>Info variant</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Success</Tooltip.Trigger>
        <Tooltip.Content variant="success">
          <Tooltip.Description>Success variant</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Warning</Tooltip.Trigger>
        <Tooltip.Content variant="warning">
          <Tooltip.Description>Warning variant</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Danger</Tooltip.Trigger>
        <Tooltip.Content variant="danger">
          <Tooltip.Description>Danger variant</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Neutral</Tooltip.Trigger>
        <Tooltip.Content variant="neutral">
          <Tooltip.Description>Neutral variant</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
    </div>
  );
}

render(<VariantsDemo />);

Placement


function PlacementDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Tooltip>
        <Tooltip.Trigger as={Button}>Top</Tooltip.Trigger>
        <Tooltip.Content side="top">
          <Tooltip.Description>Positioned top</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Bottom</Tooltip.Trigger>
        <Tooltip.Content side="bottom">
          <Tooltip.Description>Positioned bottom</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Left</Tooltip.Trigger>
        <Tooltip.Content side="left">
          <Tooltip.Description>Positioned left</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Tooltip>
        <Tooltip.Trigger as={Button}>Right</Tooltip.Trigger>
        <Tooltip.Content side="right">
          <Tooltip.Description>Positioned right</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
    </div>
  );
}

render(<PlacementDemo />);

Header & Description


function HeaderDemo() {
  return (
    <Tooltip>
      <Tooltip.Trigger as={Button}>With header</Tooltip.Trigger>
      <Tooltip.Content variant="info">
        <Tooltip.Header>Important</Tooltip.Header>
        <Tooltip.Description>This tooltip has a header and description.</Tooltip.Description>
      </Tooltip.Content>
    </Tooltip>
  );
}

render(<HeaderDemo />);

Arrow

Add Tooltip.Arrow as the last child of Tooltip.Content to point at the trigger. The arrow is bordered by default — its two outer edges continue the content's outline in the variant's border color, and the neck stays open where it joins the bubble. This is matched automatically for every variant and placement.


function ArrowDemo() {
  return (
    <Tooltip>
      <Tooltip.Trigger as={Button}>With arrow</Tooltip.Trigger>
      <Tooltip.Content>
        <Tooltip.Description>Tooltip with arrow</Tooltip.Description>
        <Tooltip.Arrow />
      </Tooltip.Content>
    </Tooltip>
  );
}

render(<ArrowDemo />);

Pass your own children to Tooltip.Arrow to replace the default shape (and its border). The default draws its border with the .tk-arrow-border layer and its fill with .tk-arrow-fill — target those classes to restyle it.

Provider

Wrap a group of tooltips in Tooltip.Provider to share delayDuration and a skipDelayDuration window — once one tooltip in the group has opened, moving to a sibling within the skip window opens it instantly. Recommended for toolbar-style clusters (WCAG 1.4.13).


function ProviderDemo() {
  return (
    <Tooltip.Provider delayDuration={300} skipDelayDuration={150}>
      <div className="flex flex-wrap justify-center gap-3">
        <Tooltip>
          <Tooltip.Trigger as={Button}>First</Tooltip.Trigger>
          <Tooltip.Content>
            <Tooltip.Description>Hover, then move to the next one</Tooltip.Description>
          </Tooltip.Content>
        </Tooltip>
        <Tooltip>
          <Tooltip.Trigger as={Button}>Second</Tooltip.Trigger>
          <Tooltip.Content>
            <Tooltip.Description>Opens instantly within skip window</Tooltip.Description>
          </Tooltip.Content>
        </Tooltip>
        <Tooltip>
          <Tooltip.Trigger as={Button}>Third</Tooltip.Trigger>
          <Tooltip.Content>
            <Tooltip.Description>Same skip-delay behavior</Tooltip.Description>
          </Tooltip.Content>
        </Tooltip>
      </div>
    </Tooltip.Provider>
  );
}

render(<ProviderDemo />);

Controlled


function ControlledDemo() {
  const [open, setOpen] = React.useState(false);

  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Tooltip open={open} onOpenChange={setOpen}>
        <Tooltip.Trigger as={Button}>Controlled</Tooltip.Trigger>
        <Tooltip.Content>
          <Tooltip.Description>Controlled tooltip</Tooltip.Description>
        </Tooltip.Content>
      </Tooltip>
      <Button onClick={() => setOpen(!open)}>{open ? 'Hide' : 'Show'}</Button>
    </div>
  );
}

render(<ControlledDemo />);

Accessibility & Keyboard

  • Trigger uses aria-describedby pointing to the content element.
  • Content has role="tooltip".
  • Tooltip respects WCAG 1.4.13 — hoverable content remains visible while the pointer is over it.
  • Escape key dismisses the tooltip and returns focus to the trigger.
KeyBehavior
EscapeDismiss the tooltip.
TabFocus trigger shows tooltip; blur hides it.

API Reference

Tooltip

See Spar Tooltip docs for primitive behavior.

Props

NameTypeDefaultDescription
childrenReact.ReactNode-Tooltip trigger and content components
idstring-Custom base ID for ARIA relationships. If not provided, one will be generated automatically. Sub-element IDs are derived as ${id}-trigger and ${id}-content.
disabledbooleanfalseWhether tooltip is disabled
openboolean-Controlled state for tooltip visibility
defaultOpenbooleanfalseDefault open state for uncontrolled tooltip
delaynumber-Override provider delay for this tooltip
hideDelaynumber0Override provider hide delay for this tooltip

Events

NameTypeDefaultDescription
onOpenChange(open: boolean) => void-Callback when tooltip open state changes

Tooltip.Content

Props

NameTypeDefaultDescription
childrenReact.ReactNode-
variantTooltipVariant'white'Color variant.
classNamesPartial<Record<"root", string>>-Per-slot extra classes.
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-Per-slot HTML-attribute overrides.
containerHTMLElement | nulldocument.bodyPortal container element. Content is portaled to document.body by default.
sideSide'top'Preferred placement relative to trigger
alignAlign'center'Alignment relative to trigger
classNamestring-

Events

NameTypeDefaultDescription
onOpenAutoFocus(event: Event) => void-Called when auto-focusing on open
onCloseAutoFocus(event: Event) => void-Called when auto-focusing on close
onEscapeKeyDown(event: KeyboardEvent) => void-Escape key handler
onPointerDownOutside(event: PointerEvent) => void-Outside pointer down handler

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-variantAlwaysReflects the resolved variant for theme recipe scoping.

Tooltip.Header

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for the header.

Tooltip.Description

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for the description.

Tooltip.Trigger

Tooltip.Arrow

Type Definitions

NameDefinition
TooltipVariant'white' | 'dark' | 'info' | 'success' | 'warning' | 'danger' | 'neutral'
Side'top' | 'right' | 'bottom' | 'left'
Align'start' | 'center' | 'end'
TooltipTriggerRenderProps{ isOpen: boolean; disabled: boolean; show: () => void; hide: () => void }