Skip to main content

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
disabledbooleanfalseDisables the tooltip: it never opens (hover, focus, the render-prop show() and defaultOpen / open are all ignored) and the trigger loses aria-describedby. Spar also sets native disabled on the trigger control, so a <Tooltip.Trigger as={Button} onClick> cannot be clicked or focused while the tooltip is disabled. To keep the control usable while hiding the hint, unmount the tooltip instead.
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.
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
onEscapeKeyDown(event: KeyboardEvent) => void-Called when Escape is pressed while the tooltip is open — with focus on the trigger (the usual case), inside the content, or anywhere else in the document. Runs before the internal close with the native keyboard event; call preventDefault() on it to keep the tooltip open.

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 }