Tooltip

A small floating label that names or describes the trigger it sits over.

Basic

Mark the trigger with data-stisla-tooltip and pass the content via data-stisla-tooltip-title. The tooltip element is created by JS and appended to <body>; the trigger gets aria-describedby while it's open.

<button type="button" class="btn btn--primary" data-stisla-tooltip data-stisla-tooltip-title="Saved 2 minutes ago">
  Hover or focus me
</button>

Placements

data-stisla-tooltip-placement picks the resting side. Floating UI flips automatically when the chosen side would overflow the viewport; the -start and -end variants align the tooltip to the corresponding edge of the trigger.

<div class="d-flex flex-wrap gap-2">
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="top" data-stisla-tooltip-title="Anchored above">Top</button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="right" data-stisla-tooltip-title="Anchored right">Right</button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="bottom" data-stisla-tooltip-title="Anchored below">Bottom</button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="left" data-stisla-tooltip-title="Anchored left">Left</button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="top-start" data-stisla-tooltip-title="Top, aligned to the trigger's start edge">Top start</button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="top-end" data-stisla-tooltip-title="Top, aligned to the trigger's end edge">Top end</button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="bottom-start" data-stisla-tooltip-title="Bottom, aligned to the trigger's start edge">Bottom start</button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-placement="bottom-end" data-stisla-tooltip-title="Bottom, aligned to the trigger's end edge">Bottom end</button>
</div>

Triggers

The default is hover focus, which opens on either. Pass data-stisla-tooltip-trigger to opt for one. manual wires nothing and leaves show() / hide() to a consumer script.

<div class="d-flex flex-wrap gap-2">
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-trigger="hover" data-stisla-tooltip-title="Hover only. Keyboard focus skips this.">
    Hover only
  </button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-trigger="focus" data-stisla-tooltip-title="Focus only. Try Tab to reach this.">
    Focus only
  </button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-title="Default. Opens on hover or focus.">
    Hover and focus
  </button>
</div>

Delay

The 600ms default delay prevents flash on incidental pointer crossings. Drop to 0 for instant feedback or raise it for chips that should only show on deliberate hover.

<div class="d-flex flex-wrap gap-2">
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-delay="0" data-stisla-tooltip-title="Instant. No open delay.">
    Instant
  </button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-title="Default. 600ms before opening.">
    Default
  </button>
  <button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-delay="1200" data-stisla-tooltip-title="Lazy. 1.2s before opening.">
    Lazy
  </button>
</div>

Tooltips work on any element. Inline anchors are the common case for jargon, acronyms, or external pointers.

The release pipeline runs on CI and announces the cut in the release channel.

<div class="prose">
  <p class="m-0">
    The release pipeline runs on
    <a href="#" data-stisla-tooltip data-stisla-tooltip-title="GitHub Actions">CI</a>
    and announces the cut in
    <a href="#" data-stisla-tooltip data-stisla-tooltip-title="#releases on Slack">the release channel</a>.
  </p>
</div>

Icon-only triggers

Icon buttons rely on tooltips for their label. Always pair the tooltip with an aria-label on the trigger so screen readers without hover get the same name.

<div class="d-flex flex-wrap gap-2">
  <button type="button" class="btn btn--outline btn--neutral btn--icon" data-stisla-tooltip data-stisla-tooltip-title="Edit" aria-label="Edit">
    <i data-lucide="pencil"></i>
  </button>
  <button type="button" class="btn btn--outline btn--neutral btn--icon" data-stisla-tooltip data-stisla-tooltip-title="Duplicate" aria-label="Duplicate">
    <i data-lucide="copy"></i>
  </button>
  <button type="button" class="btn btn--outline btn--neutral btn--icon" data-stisla-tooltip data-stisla-tooltip-title="Archive" aria-label="Archive">
    <i data-lucide="archive"></i>
  </button>
  <button type="button" class="btn btn--outline btn--danger btn--icon" data-stisla-tooltip data-stisla-tooltip-title="Delete" aria-label="Delete">
    <i data-lucide="trash-2"></i>
  </button>
</div>

HTML content

Pass data-stisla-tooltip-html="true" to render the title as HTML. Keep it short. Anything beyond a chip or two belongs in a popover.

<button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-html="true" data-stisla-tooltip-title="Press <kbd class='kbd'>⌘</kbd>+<kbd class='kbd'>K</kbd> to search">
  Search
</button>

Long content

Content past --tooltip-max-width (240px) wraps. Two lines is the practical ceiling. Anything longer belongs in a popover.

<button type="button" class="btn btn--outline btn--neutral" data-stisla-tooltip data-stisla-tooltip-title="Only workspace owners can change billing details and downgrade the plan">
  Hover for the rule
</button>

Disabled trigger

Disabled buttons don't fire pointer events, so the tooltip attributes go on a focusable wrapper that takes the hover and focus instead.

<span tabindex="0" data-stisla-tooltip data-stisla-tooltip-title="Upgrade to enable exports" class="d-inline-block">
  <button type="button" class="btn btn--primary" disabled style="pointer-events: none;">Export CSV</button>
</span>

Customization

Every variable that retunes .tooltip is below. Override on a single trigger, on a parent scope, or on :root. The chip uses an inverse surface (--st-foreground / --st-background) so it stays high-contrast against the page in both themes.

Geometry

VariableDefaultUse
--tooltip-max-width240pxWrap point. Long content past this folds to a second line.
--tooltip-padding-y6pxTop and bottom inset on .tooltip__inner.
--tooltip-padding-x10pxLeft and right inset on .tooltip__inner.
--tooltip-radiusvar(--st-radius-sm)Chip radius. Small tier. Matches kbd and badge.
--tooltip-z-index1050Stack level. Between drawer (1045) and dialog (1055).
--tooltip-font-size0.8125remLabel text size.
--tooltip-line-height1.4Wrapping rhythm.

Surface

VariableDefaultUse
--tooltip-bgvar(--st-foreground)Chip background. Inverse. Flips with theme via the token swap.
--tooltip-colorvar(--st-background)Label text color. Inverse pair.
--tooltip-shadow0 4px 12px -2px (foreground 12% mix)Lift off the page.

Arrow

VariableDefaultUse
--tooltip-arrow-size8pxSquare that's rotated 45° for the diamond caret.

Motion

VariableDefaultUse
--tooltip-transition-duration0.15sFade and translate duration. Zeroed under prefers-reduced-motion.