Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Tooltip

A hint on hover or focus, through interest invokers.

Installation

pnpm dlx zazz-ui add tooltip
npx zazz-ui add tooltip
yarn dlx zazz-ui add tooltip
bunx zazz-ui add tooltip

Also adds kbd, popover, which it depends on. Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

Default

Add to library
<ui-tooltip>
  <button type="button" data-ui="button" interestfor="tooltip-example-default-1">Hover</button>
  <div data-tooltip-slot="content" id="tooltip-example-default-1" popover="hint">
    Add to library
    <span data-tooltip-slot="arrow" aria-hidden="true"></span>
  </div>
</ui-tooltip>
/**
 * tooltip.css: Tooltip (ui-tooltip | [data-ui~="tooltip"]; slots data-tooltip-slot="trigger | content | arrow")
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), popover.css, kbd.css (--ui-kbd-*)
 * @uses       interestfor: Interest Invokers API (Chromium 142+; polyfill via invokers)
 * @uses       data-tooltip-slot="content": hint popover (light dismiss, auto-hides sibling hints)
 * @uses       anchor-name, position-anchor, position-area, position-try-fallbacks:
 *             explicit anchors for polyfill; data-popover-side maps to --ui-popover-position-*
 * @tokens     --ui-tooltip-* (@layer variables; read on the content slot)
 */

@layer variables {
  :root {
    --ui-tooltip-bg: var(--color-foreground);
    --ui-tooltip-text: var(--color-background);
    --ui-tooltip-border-color: transparent;
    --ui-tooltip-rounded: var(--radius-sm);
    --ui-tooltip-shadow: var(--shadow-sm);
    --ui-tooltip-px: var(--space-xs);
    --ui-tooltip-py: var(--spacing);
    --ui-tooltip-gap: var(--spacing);
    --ui-tooltip-font-family: var(--font-family-mono);
    --ui-tooltip-font-size: var(--font-size-xs);
    --ui-tooltip-leading: var(--leading-xs);
    --ui-tooltip-font-weight: var(--font-weight-body);
    --ui-tooltip-max-w: 16ch;

    /* Arrow */
    --ui-tooltip-arrow-size: calc(var(--spacing) * 2);
    --ui-tooltip-arrow-color: var(--ui-tooltip-bg);
    --ui-tooltip-arrow-radius: calc(var(--spacing) * 0.5);

    /* Reveal transitions: lighter than popover */
    --ui-tooltip-transition-duration: calc(var(--default-transition-duration) / 2);
    --ui-tooltip-transition-timing-function: var(--default-transition-timing-function);
    --ui-tooltip-transition-delay: 0ms;
    --ui-tooltip-transition:
      opacity var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      transform var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      overlay var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete,
      display var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete;
    --ui-tooltip-scale--start: 0.94;
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-tooltip-opacity--start: 0;
    --ui-tooltip-scale--end: 1;
    --ui-tooltip-translate--end: 0, 0, 0;
    --ui-tooltip-opacity--end: 1;

    /* Positioning: smaller offset than popover menus */
    --ui-tooltip-my: var(--spacing);
    --ui-tooltip-mx: 0;
  }
}

/* Popover overrides, on the tooltip content (declared on :root they would retheme
   every popover in the kit, since tooltip.css loads after popover.css). */
@layer ui.components {
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    --ui-popover-shadow: var(--shadow-xs);
    --ui-popover-min-w: 0; /* dual-mode: no minimum (fit-content cannot pass the typed pair) */
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
  }
}

@layer reset {
  /* ===========================================================================
  HINT POPOVER OVERRIDES: popover.css targets menus; hints need a compact
  surface without min-inline-size or backdrop blur.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
    color: var(--_text-resolved, var(--ui-tooltip-text));
    background-color: var(--_bg-resolved, var(--ui-tooltip-bg));
    min-inline-size: unset;
    --_max-w-len: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    --_max-w-num: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    max-inline-size: calc(var(--_max-w-len) + var(--_max-w-num) * var(--spacing));
    backdrop-filter: none;

    transform-origin: var(--ui-popover-origin);
    opacity: var(--ui-tooltip-opacity--start);
    transform: scale(var(--ui-tooltip-scale--start)) translate3d(var(--ui-tooltip-translate--start));
    transition: var(--ui-tooltip-transition);
  }

  @supports (anchor-name: --zazz) {
    :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
      --_my-len: var(--_my-resolved, var(--ui-tooltip-my));
      --_my-num: var(--_my-resolved, var(--ui-tooltip-my));
      margin-block: calc(var(--_my-len) + var(--_my-num) * var(--spacing));
      --_mx-len: var(--_mx-resolved, var(--ui-tooltip-mx));
      --_mx-num: var(--_mx-resolved, var(--ui-tooltip-mx));
      margin-inline: calc(var(--_mx-len) + var(--_mx-num) * var(--spacing));
    }
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"]:popover-open {
    display: flex;
    opacity: var(--ui-tooltip-opacity--end);
    transform: scale(var(--ui-tooltip-scale--end)) translate3d(var(--ui-tooltip-translate--end));

    @starting-style {
      opacity: var(--ui-tooltip-opacity--start);
      transform: scale(var(--ui-tooltip-scale--start))
        translate3d(var(--ui-tooltip-translate--start));
    }
  }
}

@layer ui.components {
  /* ===========================================================================
  TOOLTIP: interestfor sets the ARIA (aria-describedby / aria-details), so do
  not add role="tooltip". Explicit anchor-name is needed because the polyfill
  does not provide implicit anchors.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) {
    anchor-scope: --ui-tooltip-trigger;
    display: inline-block;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) > :where([interestfor], [data-tooltip-slot~="trigger"]) {
    anchor-name: --ui-tooltip-trigger;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="trigger"] {
    display: inline-flex;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    position-anchor: --ui-tooltip-trigger;

    display: flex;
    align-items: center;
    --_gap-len: var(--_gap-resolved, var(--ui-tooltip-gap));
    --_gap-num: var(--_gap-resolved, var(--ui-tooltip-gap));
    gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
    inline-size: max-content;

    --_px-len: var(--_px-resolved, var(--ui-tooltip-px));
    --_px-num: var(--_px-resolved, var(--ui-tooltip-px));
    padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
    --_py-len: var(--_py-resolved, var(--ui-tooltip-py));
    --_py-num: var(--_py-resolved, var(--ui-tooltip-py));
    padding-block: calc(var(--_py-len) + var(--_py-num) * var(--spacing));

    border: 1px solid var(--_border-color-resolved, var(--ui-tooltip-border-color));
    border-radius: var(--_rounded-resolved, var(--ui-tooltip-rounded));
    box-shadow: var(--_shadow-resolved, var(--ui-tooltip-shadow));

    font-family: var(--_font-family-resolved, var(--ui-tooltip-font-family));
    font-size: var(--_font-size-resolved, var(--ui-tooltip-font-size));
    line-height: var(--_leading-resolved, var(--ui-tooltip-leading));
    font-weight: var(--_font-weight-resolved, var(--ui-tooltip-font-weight));
    text-wrap: balance;
  }

  /* Descendant (not child) so keys wrapped in a ui-kbd-group still retint */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] :where(kbd) {
    --ui-kbd-bg: color-mix(in oklch, var(--ui-tooltip-text) 15%, transparent);
    --ui-kbd-text: var(--ui-tooltip-text);
    --ui-kbd-min-w: calc(var(--spacing) * 4);
    --ui-kbd-min-h: calc(var(--spacing) * 4);
    --ui-kbd-font-size: var(--font-size-xs);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"]:has([data-tooltip-slot~="arrow"]) {
    overflow: visible;
  }

  /* Positioning: reuse --ui-popover-position-* hooks from popover.css via data-popover-side */

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-end span-all;
    --ui-popover-origin: top center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-start span-all;
    --ui-popover-origin: center right;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-end span-all;
    --ui-popover-origin: center left;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  /* Tooltip arrow */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="arrow"] {
    position: absolute;
    /* physical top/left/right/bottom throughout the arrow rules: they pair with
       the physical [data-popover-side] placement semantics from anchor positioning */
    top: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: 50%;
    transform: translate(-50%, 0%) rotate(45deg);
    inline-size: var(--ui-tooltip-arrow-size);
    block-size: var(--ui-tooltip-arrow-size);
    background-color: var(--ui-tooltip-arrow-color);
    border-radius: var(--ui-tooltip-arrow-radius);
    pointer-events: none;
  }

  /* Tooltip arrow position */
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]
    [data-tooltip-slot~="arrow"] {
    bottom: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]
    [data-tooltip-slot~="arrow"] {
    top: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]
    [data-tooltip-slot~="arrow"] {
    left: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]
    [data-tooltip-slot~="arrow"] {
    right: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: auto;
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }
}

With kbd

Save ⌘ S
<ui-tooltip>
  <button
    type="button"
    data-ui="button"
    data-button-variant="muted"
    data-button-size="icon"
    interestfor="tooltip-example-save-1"
    aria-label="Save"
  >
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
      <rect width="256" height="256" fill="none" />
      <path
        fill="currentColor"
        d="M74.34,85.66A8,8,0,0,1,85.66,74.34L120,108.69V24a8,8,0,0,1,16,0v84.69l34.34-34.35a8,8,0,0,1,11.32,11.32l-48,48a8,8,0,0,1-11.32,0ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H84.4a4,4,0,0,1,2.83,1.17L111,145A24,24,0,0,0,145,145l23.8-23.8A4,4,0,0,1,171.6,120H224A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"
      />
    </svg>
  </button>
  <div data-tooltip-slot="content" id="tooltip-example-save-1" popover="hint">
    Save
    <ui-kbd-group>
      <kbd>⌘</kbd>
      <kbd>S</kbd>
    </ui-kbd-group>
    <span data-tooltip-slot="arrow" aria-hidden="true"></span>
  </div>
</ui-tooltip>
/**
 * tooltip.css: Tooltip (ui-tooltip | [data-ui~="tooltip"]; slots data-tooltip-slot="trigger | content | arrow")
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), popover.css, kbd.css (--ui-kbd-*)
 * @uses       interestfor: Interest Invokers API (Chromium 142+; polyfill via invokers)
 * @uses       data-tooltip-slot="content": hint popover (light dismiss, auto-hides sibling hints)
 * @uses       anchor-name, position-anchor, position-area, position-try-fallbacks:
 *             explicit anchors for polyfill; data-popover-side maps to --ui-popover-position-*
 * @tokens     --ui-tooltip-* (@layer variables; read on the content slot)
 */

@layer variables {
  :root {
    --ui-tooltip-bg: var(--color-foreground);
    --ui-tooltip-text: var(--color-background);
    --ui-tooltip-border-color: transparent;
    --ui-tooltip-rounded: var(--radius-sm);
    --ui-tooltip-shadow: var(--shadow-sm);
    --ui-tooltip-px: var(--space-xs);
    --ui-tooltip-py: var(--spacing);
    --ui-tooltip-gap: var(--spacing);
    --ui-tooltip-font-family: var(--font-family-mono);
    --ui-tooltip-font-size: var(--font-size-xs);
    --ui-tooltip-leading: var(--leading-xs);
    --ui-tooltip-font-weight: var(--font-weight-body);
    --ui-tooltip-max-w: 16ch;

    /* Arrow */
    --ui-tooltip-arrow-size: calc(var(--spacing) * 2);
    --ui-tooltip-arrow-color: var(--ui-tooltip-bg);
    --ui-tooltip-arrow-radius: calc(var(--spacing) * 0.5);

    /* Reveal transitions: lighter than popover */
    --ui-tooltip-transition-duration: calc(var(--default-transition-duration) / 2);
    --ui-tooltip-transition-timing-function: var(--default-transition-timing-function);
    --ui-tooltip-transition-delay: 0ms;
    --ui-tooltip-transition:
      opacity var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      transform var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      overlay var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete,
      display var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete;
    --ui-tooltip-scale--start: 0.94;
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-tooltip-opacity--start: 0;
    --ui-tooltip-scale--end: 1;
    --ui-tooltip-translate--end: 0, 0, 0;
    --ui-tooltip-opacity--end: 1;

    /* Positioning: smaller offset than popover menus */
    --ui-tooltip-my: var(--spacing);
    --ui-tooltip-mx: 0;
  }
}

/* Popover overrides, on the tooltip content (declared on :root they would retheme
   every popover in the kit, since tooltip.css loads after popover.css). */
@layer ui.components {
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    --ui-popover-shadow: var(--shadow-xs);
    --ui-popover-min-w: 0; /* dual-mode: no minimum (fit-content cannot pass the typed pair) */
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
  }
}

@layer reset {
  /* ===========================================================================
  HINT POPOVER OVERRIDES: popover.css targets menus; hints need a compact
  surface without min-inline-size or backdrop blur.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
    color: var(--_text-resolved, var(--ui-tooltip-text));
    background-color: var(--_bg-resolved, var(--ui-tooltip-bg));
    min-inline-size: unset;
    --_max-w-len: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    --_max-w-num: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    max-inline-size: calc(var(--_max-w-len) + var(--_max-w-num) * var(--spacing));
    backdrop-filter: none;

    transform-origin: var(--ui-popover-origin);
    opacity: var(--ui-tooltip-opacity--start);
    transform: scale(var(--ui-tooltip-scale--start)) translate3d(var(--ui-tooltip-translate--start));
    transition: var(--ui-tooltip-transition);
  }

  @supports (anchor-name: --zazz) {
    :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
      --_my-len: var(--_my-resolved, var(--ui-tooltip-my));
      --_my-num: var(--_my-resolved, var(--ui-tooltip-my));
      margin-block: calc(var(--_my-len) + var(--_my-num) * var(--spacing));
      --_mx-len: var(--_mx-resolved, var(--ui-tooltip-mx));
      --_mx-num: var(--_mx-resolved, var(--ui-tooltip-mx));
      margin-inline: calc(var(--_mx-len) + var(--_mx-num) * var(--spacing));
    }
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"]:popover-open {
    display: flex;
    opacity: var(--ui-tooltip-opacity--end);
    transform: scale(var(--ui-tooltip-scale--end)) translate3d(var(--ui-tooltip-translate--end));

    @starting-style {
      opacity: var(--ui-tooltip-opacity--start);
      transform: scale(var(--ui-tooltip-scale--start))
        translate3d(var(--ui-tooltip-translate--start));
    }
  }
}

@layer ui.components {
  /* ===========================================================================
  TOOLTIP: interestfor sets the ARIA (aria-describedby / aria-details), so do
  not add role="tooltip". Explicit anchor-name is needed because the polyfill
  does not provide implicit anchors.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) {
    anchor-scope: --ui-tooltip-trigger;
    display: inline-block;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) > :where([interestfor], [data-tooltip-slot~="trigger"]) {
    anchor-name: --ui-tooltip-trigger;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="trigger"] {
    display: inline-flex;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    position-anchor: --ui-tooltip-trigger;

    display: flex;
    align-items: center;
    --_gap-len: var(--_gap-resolved, var(--ui-tooltip-gap));
    --_gap-num: var(--_gap-resolved, var(--ui-tooltip-gap));
    gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
    inline-size: max-content;

    --_px-len: var(--_px-resolved, var(--ui-tooltip-px));
    --_px-num: var(--_px-resolved, var(--ui-tooltip-px));
    padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
    --_py-len: var(--_py-resolved, var(--ui-tooltip-py));
    --_py-num: var(--_py-resolved, var(--ui-tooltip-py));
    padding-block: calc(var(--_py-len) + var(--_py-num) * var(--spacing));

    border: 1px solid var(--_border-color-resolved, var(--ui-tooltip-border-color));
    border-radius: var(--_rounded-resolved, var(--ui-tooltip-rounded));
    box-shadow: var(--_shadow-resolved, var(--ui-tooltip-shadow));

    font-family: var(--_font-family-resolved, var(--ui-tooltip-font-family));
    font-size: var(--_font-size-resolved, var(--ui-tooltip-font-size));
    line-height: var(--_leading-resolved, var(--ui-tooltip-leading));
    font-weight: var(--_font-weight-resolved, var(--ui-tooltip-font-weight));
    text-wrap: balance;
  }

  /* Descendant (not child) so keys wrapped in a ui-kbd-group still retint */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] :where(kbd) {
    --ui-kbd-bg: color-mix(in oklch, var(--ui-tooltip-text) 15%, transparent);
    --ui-kbd-text: var(--ui-tooltip-text);
    --ui-kbd-min-w: calc(var(--spacing) * 4);
    --ui-kbd-min-h: calc(var(--spacing) * 4);
    --ui-kbd-font-size: var(--font-size-xs);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"]:has([data-tooltip-slot~="arrow"]) {
    overflow: visible;
  }

  /* Positioning: reuse --ui-popover-position-* hooks from popover.css via data-popover-side */

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-end span-all;
    --ui-popover-origin: top center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-start span-all;
    --ui-popover-origin: center right;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-end span-all;
    --ui-popover-origin: center left;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  /* Tooltip arrow */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="arrow"] {
    position: absolute;
    /* physical top/left/right/bottom throughout the arrow rules: they pair with
       the physical [data-popover-side] placement semantics from anchor positioning */
    top: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: 50%;
    transform: translate(-50%, 0%) rotate(45deg);
    inline-size: var(--ui-tooltip-arrow-size);
    block-size: var(--ui-tooltip-arrow-size);
    background-color: var(--ui-tooltip-arrow-color);
    border-radius: var(--ui-tooltip-arrow-radius);
    pointer-events: none;
  }

  /* Tooltip arrow position */
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]
    [data-tooltip-slot~="arrow"] {
    bottom: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]
    [data-tooltip-slot~="arrow"] {
    top: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]
    [data-tooltip-slot~="arrow"] {
    left: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]
    [data-tooltip-slot~="arrow"] {
    right: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: auto;
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }
}

Sides

Tooltip on left
Tooltip on top
Tooltip on bottom
Tooltip on right
<div
  style="
    --display: flex;
    --flex-wrap: wrap;
    --items: center;
    --gap: 6;
  "
>
  <ui-tooltip>
    <button type="button" data-ui="button" interestfor="tooltip-example-left-1">Left</button>
    <div
      data-tooltip-slot="content"
      id="tooltip-example-left-1"
      popover="hint"
      data-popover-side="left"
    >
      Tooltip on left
      <span data-tooltip-slot="arrow" aria-hidden="true"></span>
    </div>
  </ui-tooltip>

  <ui-tooltip>
    <button type="button" data-ui="button" interestfor="tooltip-example-top-1">Top</button>
    <div
      data-tooltip-slot="content"
      id="tooltip-example-top-1"
      popover="hint"
      data-popover-side="top"
    >
      Tooltip on top
      <span data-tooltip-slot="arrow" aria-hidden="true"></span>
    </div>
  </ui-tooltip>

  <ui-tooltip>
    <button type="button" data-ui="button" interestfor="tooltip-example-bottom-1">Bottom</button>
    <div
      data-tooltip-slot="content"
      id="tooltip-example-bottom-1"
      popover="hint"
      data-popover-side="bottom"
    >
      Tooltip on bottom
      <span data-tooltip-slot="arrow" aria-hidden="true"></span>
    </div>
  </ui-tooltip>

  <ui-tooltip>
    <button type="button" data-ui="button" interestfor="tooltip-example-right-1">Right</button>
    <div
      data-tooltip-slot="content"
      id="tooltip-example-right-1"
      popover="hint"
      data-popover-side="right"
    >
      Tooltip on right
      <span data-tooltip-slot="arrow" aria-hidden="true"></span>
    </div>
  </ui-tooltip>
</div>
/**
 * tooltip.css: Tooltip (ui-tooltip | [data-ui~="tooltip"]; slots data-tooltip-slot="trigger | content | arrow")
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), popover.css, kbd.css (--ui-kbd-*)
 * @uses       interestfor: Interest Invokers API (Chromium 142+; polyfill via invokers)
 * @uses       data-tooltip-slot="content": hint popover (light dismiss, auto-hides sibling hints)
 * @uses       anchor-name, position-anchor, position-area, position-try-fallbacks:
 *             explicit anchors for polyfill; data-popover-side maps to --ui-popover-position-*
 * @tokens     --ui-tooltip-* (@layer variables; read on the content slot)
 */

@layer variables {
  :root {
    --ui-tooltip-bg: var(--color-foreground);
    --ui-tooltip-text: var(--color-background);
    --ui-tooltip-border-color: transparent;
    --ui-tooltip-rounded: var(--radius-sm);
    --ui-tooltip-shadow: var(--shadow-sm);
    --ui-tooltip-px: var(--space-xs);
    --ui-tooltip-py: var(--spacing);
    --ui-tooltip-gap: var(--spacing);
    --ui-tooltip-font-family: var(--font-family-mono);
    --ui-tooltip-font-size: var(--font-size-xs);
    --ui-tooltip-leading: var(--leading-xs);
    --ui-tooltip-font-weight: var(--font-weight-body);
    --ui-tooltip-max-w: 16ch;

    /* Arrow */
    --ui-tooltip-arrow-size: calc(var(--spacing) * 2);
    --ui-tooltip-arrow-color: var(--ui-tooltip-bg);
    --ui-tooltip-arrow-radius: calc(var(--spacing) * 0.5);

    /* Reveal transitions: lighter than popover */
    --ui-tooltip-transition-duration: calc(var(--default-transition-duration) / 2);
    --ui-tooltip-transition-timing-function: var(--default-transition-timing-function);
    --ui-tooltip-transition-delay: 0ms;
    --ui-tooltip-transition:
      opacity var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      transform var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      overlay var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete,
      display var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete;
    --ui-tooltip-scale--start: 0.94;
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-tooltip-opacity--start: 0;
    --ui-tooltip-scale--end: 1;
    --ui-tooltip-translate--end: 0, 0, 0;
    --ui-tooltip-opacity--end: 1;

    /* Positioning: smaller offset than popover menus */
    --ui-tooltip-my: var(--spacing);
    --ui-tooltip-mx: 0;
  }
}

/* Popover overrides, on the tooltip content (declared on :root they would retheme
   every popover in the kit, since tooltip.css loads after popover.css). */
@layer ui.components {
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    --ui-popover-shadow: var(--shadow-xs);
    --ui-popover-min-w: 0; /* dual-mode: no minimum (fit-content cannot pass the typed pair) */
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
  }
}

@layer reset {
  /* ===========================================================================
  HINT POPOVER OVERRIDES: popover.css targets menus; hints need a compact
  surface without min-inline-size or backdrop blur.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
    color: var(--_text-resolved, var(--ui-tooltip-text));
    background-color: var(--_bg-resolved, var(--ui-tooltip-bg));
    min-inline-size: unset;
    --_max-w-len: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    --_max-w-num: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    max-inline-size: calc(var(--_max-w-len) + var(--_max-w-num) * var(--spacing));
    backdrop-filter: none;

    transform-origin: var(--ui-popover-origin);
    opacity: var(--ui-tooltip-opacity--start);
    transform: scale(var(--ui-tooltip-scale--start)) translate3d(var(--ui-tooltip-translate--start));
    transition: var(--ui-tooltip-transition);
  }

  @supports (anchor-name: --zazz) {
    :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
      --_my-len: var(--_my-resolved, var(--ui-tooltip-my));
      --_my-num: var(--_my-resolved, var(--ui-tooltip-my));
      margin-block: calc(var(--_my-len) + var(--_my-num) * var(--spacing));
      --_mx-len: var(--_mx-resolved, var(--ui-tooltip-mx));
      --_mx-num: var(--_mx-resolved, var(--ui-tooltip-mx));
      margin-inline: calc(var(--_mx-len) + var(--_mx-num) * var(--spacing));
    }
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"]:popover-open {
    display: flex;
    opacity: var(--ui-tooltip-opacity--end);
    transform: scale(var(--ui-tooltip-scale--end)) translate3d(var(--ui-tooltip-translate--end));

    @starting-style {
      opacity: var(--ui-tooltip-opacity--start);
      transform: scale(var(--ui-tooltip-scale--start))
        translate3d(var(--ui-tooltip-translate--start));
    }
  }
}

@layer ui.components {
  /* ===========================================================================
  TOOLTIP: interestfor sets the ARIA (aria-describedby / aria-details), so do
  not add role="tooltip". Explicit anchor-name is needed because the polyfill
  does not provide implicit anchors.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) {
    anchor-scope: --ui-tooltip-trigger;
    display: inline-block;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) > :where([interestfor], [data-tooltip-slot~="trigger"]) {
    anchor-name: --ui-tooltip-trigger;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="trigger"] {
    display: inline-flex;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    position-anchor: --ui-tooltip-trigger;

    display: flex;
    align-items: center;
    --_gap-len: var(--_gap-resolved, var(--ui-tooltip-gap));
    --_gap-num: var(--_gap-resolved, var(--ui-tooltip-gap));
    gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
    inline-size: max-content;

    --_px-len: var(--_px-resolved, var(--ui-tooltip-px));
    --_px-num: var(--_px-resolved, var(--ui-tooltip-px));
    padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
    --_py-len: var(--_py-resolved, var(--ui-tooltip-py));
    --_py-num: var(--_py-resolved, var(--ui-tooltip-py));
    padding-block: calc(var(--_py-len) + var(--_py-num) * var(--spacing));

    border: 1px solid var(--_border-color-resolved, var(--ui-tooltip-border-color));
    border-radius: var(--_rounded-resolved, var(--ui-tooltip-rounded));
    box-shadow: var(--_shadow-resolved, var(--ui-tooltip-shadow));

    font-family: var(--_font-family-resolved, var(--ui-tooltip-font-family));
    font-size: var(--_font-size-resolved, var(--ui-tooltip-font-size));
    line-height: var(--_leading-resolved, var(--ui-tooltip-leading));
    font-weight: var(--_font-weight-resolved, var(--ui-tooltip-font-weight));
    text-wrap: balance;
  }

  /* Descendant (not child) so keys wrapped in a ui-kbd-group still retint */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] :where(kbd) {
    --ui-kbd-bg: color-mix(in oklch, var(--ui-tooltip-text) 15%, transparent);
    --ui-kbd-text: var(--ui-tooltip-text);
    --ui-kbd-min-w: calc(var(--spacing) * 4);
    --ui-kbd-min-h: calc(var(--spacing) * 4);
    --ui-kbd-font-size: var(--font-size-xs);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"]:has([data-tooltip-slot~="arrow"]) {
    overflow: visible;
  }

  /* Positioning: reuse --ui-popover-position-* hooks from popover.css via data-popover-side */

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-end span-all;
    --ui-popover-origin: top center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-start span-all;
    --ui-popover-origin: center right;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-end span-all;
    --ui-popover-origin: center left;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  /* Tooltip arrow */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="arrow"] {
    position: absolute;
    /* physical top/left/right/bottom throughout the arrow rules: they pair with
       the physical [data-popover-side] placement semantics from anchor positioning */
    top: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: 50%;
    transform: translate(-50%, 0%) rotate(45deg);
    inline-size: var(--ui-tooltip-arrow-size);
    block-size: var(--ui-tooltip-arrow-size);
    background-color: var(--ui-tooltip-arrow-color);
    border-radius: var(--ui-tooltip-arrow-radius);
    pointer-events: none;
  }

  /* Tooltip arrow position */
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]
    [data-tooltip-slot~="arrow"] {
    bottom: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]
    [data-tooltip-slot~="arrow"] {
    top: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]
    [data-tooltip-slot~="arrow"] {
    left: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]
    [data-tooltip-slot~="arrow"] {
    right: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: auto;
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }
}

Disabled

This action is unavailable
<ui-tooltip>
  <button
    type="button"
    data-ui="button"
    aria-disabled="true"
    interestfor="tooltip-example-disabled-1"
  >
    Disabled
  </button>
  <div data-tooltip-slot="content" id="tooltip-example-disabled-1" popover="hint">
    This action is unavailable
    <span data-tooltip-slot="arrow" aria-hidden="true"></span>
  </div>
</ui-tooltip>
/**
 * tooltip.css: Tooltip (ui-tooltip | [data-ui~="tooltip"]; slots data-tooltip-slot="trigger | content | arrow")
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved), popover.css, kbd.css (--ui-kbd-*)
 * @uses       interestfor: Interest Invokers API (Chromium 142+; polyfill via invokers)
 * @uses       data-tooltip-slot="content": hint popover (light dismiss, auto-hides sibling hints)
 * @uses       anchor-name, position-anchor, position-area, position-try-fallbacks:
 *             explicit anchors for polyfill; data-popover-side maps to --ui-popover-position-*
 * @tokens     --ui-tooltip-* (@layer variables; read on the content slot)
 */

@layer variables {
  :root {
    --ui-tooltip-bg: var(--color-foreground);
    --ui-tooltip-text: var(--color-background);
    --ui-tooltip-border-color: transparent;
    --ui-tooltip-rounded: var(--radius-sm);
    --ui-tooltip-shadow: var(--shadow-sm);
    --ui-tooltip-px: var(--space-xs);
    --ui-tooltip-py: var(--spacing);
    --ui-tooltip-gap: var(--spacing);
    --ui-tooltip-font-family: var(--font-family-mono);
    --ui-tooltip-font-size: var(--font-size-xs);
    --ui-tooltip-leading: var(--leading-xs);
    --ui-tooltip-font-weight: var(--font-weight-body);
    --ui-tooltip-max-w: 16ch;

    /* Arrow */
    --ui-tooltip-arrow-size: calc(var(--spacing) * 2);
    --ui-tooltip-arrow-color: var(--ui-tooltip-bg);
    --ui-tooltip-arrow-radius: calc(var(--spacing) * 0.5);

    /* Reveal transitions: lighter than popover */
    --ui-tooltip-transition-duration: calc(var(--default-transition-duration) / 2);
    --ui-tooltip-transition-timing-function: var(--default-transition-timing-function);
    --ui-tooltip-transition-delay: 0ms;
    --ui-tooltip-transition:
      opacity var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      transform var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay),
      overlay var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete,
      display var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function)
        var(--ui-tooltip-transition-delay) allow-discrete;
    --ui-tooltip-scale--start: 0.94;
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-tooltip-opacity--start: 0;
    --ui-tooltip-scale--end: 1;
    --ui-tooltip-translate--end: 0, 0, 0;
    --ui-tooltip-opacity--end: 1;

    /* Positioning: smaller offset than popover menus */
    --ui-tooltip-my: var(--spacing);
    --ui-tooltip-mx: 0;
  }
}

/* Popover overrides, on the tooltip content (declared on :root they would retheme
   every popover in the kit, since tooltip.css loads after popover.css). */
@layer ui.components {
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    --ui-popover-shadow: var(--shadow-xs);
    --ui-popover-min-w: 0; /* dual-mode: no minimum (fit-content cannot pass the typed pair) */
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
  }
}

@layer reset {
  /* ===========================================================================
  HINT POPOVER OVERRIDES: popover.css targets menus; hints need a compact
  surface without min-inline-size or backdrop blur.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
    color: var(--_text-resolved, var(--ui-tooltip-text));
    background-color: var(--_bg-resolved, var(--ui-tooltip-bg));
    min-inline-size: unset;
    --_max-w-len: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    --_max-w-num: var(--_max-w-resolved, var(--ui-tooltip-max-w));
    max-inline-size: calc(var(--_max-w-len) + var(--_max-w-num) * var(--spacing));
    backdrop-filter: none;

    transform-origin: var(--ui-popover-origin);
    opacity: var(--ui-tooltip-opacity--start);
    transform: scale(var(--ui-tooltip-scale--start)) translate3d(var(--ui-tooltip-translate--start));
    transition: var(--ui-tooltip-transition);
  }

  @supports (anchor-name: --zazz) {
    :where(ui-tooltip, [data-ui~="tooltip"]) :where([data-tooltip-slot~="content"]) {
      --_my-len: var(--_my-resolved, var(--ui-tooltip-my));
      --_my-num: var(--_my-resolved, var(--ui-tooltip-my));
      margin-block: calc(var(--_my-len) + var(--_my-num) * var(--spacing));
      --_mx-len: var(--_mx-resolved, var(--ui-tooltip-mx));
      --_mx-num: var(--_mx-resolved, var(--ui-tooltip-mx));
      margin-inline: calc(var(--_mx-len) + var(--_mx-num) * var(--spacing));
    }
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"]:popover-open {
    display: flex;
    opacity: var(--ui-tooltip-opacity--end);
    transform: scale(var(--ui-tooltip-scale--end)) translate3d(var(--ui-tooltip-translate--end));

    @starting-style {
      opacity: var(--ui-tooltip-opacity--start);
      transform: scale(var(--ui-tooltip-scale--start))
        translate3d(var(--ui-tooltip-translate--start));
    }
  }
}

@layer ui.components {
  /* ===========================================================================
  TOOLTIP: interestfor sets the ARIA (aria-describedby / aria-details), so do
  not add role="tooltip". Explicit anchor-name is needed because the polyfill
  does not provide implicit anchors.
  =========================================================================== */

  :where(ui-tooltip, [data-ui~="tooltip"]) {
    anchor-scope: --ui-tooltip-trigger;
    display: inline-block;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) > :where([interestfor], [data-tooltip-slot~="trigger"]) {
    anchor-name: --ui-tooltip-trigger;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="trigger"] {
    display: inline-flex;
  }

  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] {
    position-anchor: --ui-tooltip-trigger;

    display: flex;
    align-items: center;
    --_gap-len: var(--_gap-resolved, var(--ui-tooltip-gap));
    --_gap-num: var(--_gap-resolved, var(--ui-tooltip-gap));
    gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
    inline-size: max-content;

    --_px-len: var(--_px-resolved, var(--ui-tooltip-px));
    --_px-num: var(--_px-resolved, var(--ui-tooltip-px));
    padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
    --_py-len: var(--_py-resolved, var(--ui-tooltip-py));
    --_py-num: var(--_py-resolved, var(--ui-tooltip-py));
    padding-block: calc(var(--_py-len) + var(--_py-num) * var(--spacing));

    border: 1px solid var(--_border-color-resolved, var(--ui-tooltip-border-color));
    border-radius: var(--_rounded-resolved, var(--ui-tooltip-rounded));
    box-shadow: var(--_shadow-resolved, var(--ui-tooltip-shadow));

    font-family: var(--_font-family-resolved, var(--ui-tooltip-font-family));
    font-size: var(--_font-size-resolved, var(--ui-tooltip-font-size));
    line-height: var(--_leading-resolved, var(--ui-tooltip-leading));
    font-weight: var(--_font-weight-resolved, var(--ui-tooltip-font-weight));
    text-wrap: balance;
  }

  /* Descendant (not child) so keys wrapped in a ui-kbd-group still retint */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="content"] :where(kbd) {
    --ui-kbd-bg: color-mix(in oklch, var(--ui-tooltip-text) 15%, transparent);
    --ui-kbd-text: var(--ui-tooltip-text);
    --ui-kbd-min-w: calc(var(--spacing) * 4);
    --ui-kbd-min-h: calc(var(--spacing) * 4);
    --ui-kbd-font-size: var(--font-size-xs);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"]:has([data-tooltip-slot~="arrow"]) {
    overflow: visible;
  }

  /* Positioning: reuse --ui-popover-position-* hooks from popover.css via data-popover-side */

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-start span-all;
    --ui-popover-origin: bottom center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0, 0.25em, 0;
    --ui-popover-position-area: block-end span-all;
    --ui-popover-origin: top center;
    --ui-tooltip-my: var(--spacing);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-start span-all;
    --ui-popover-origin: center right;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]:not([data-popover-align]),
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"][data-popover-align="center"] {
    --ui-tooltip-translate--start: 0.25em, 0, 0;
    --ui-popover-position-area: inline-end span-all;
    --ui-popover-origin: center left;
    --ui-tooltip-mx: var(--spacing);
    --ui-tooltip-my: 0;
  }

  /* Tooltip arrow */
  :where(ui-tooltip, [data-ui~="tooltip"]) [data-tooltip-slot~="arrow"] {
    position: absolute;
    /* physical top/left/right/bottom throughout the arrow rules: they pair with
       the physical [data-popover-side] placement semantics from anchor positioning */
    top: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: 50%;
    transform: translate(-50%, 0%) rotate(45deg);
    inline-size: var(--ui-tooltip-arrow-size);
    block-size: var(--ui-tooltip-arrow-size);
    background-color: var(--ui-tooltip-arrow-color);
    border-radius: var(--ui-tooltip-arrow-radius);
    pointer-events: none;
  }

  /* Tooltip arrow position */
  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="top"]
    [data-tooltip-slot~="arrow"] {
    bottom: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="bottom"]
    [data-tooltip-slot~="arrow"] {
    top: calc((var(--ui-tooltip-arrow-size) / 2) * -1);
    left: 50%;
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="left"]
    [data-tooltip-slot~="arrow"] {
    left: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }

  :where(ui-tooltip, [data-ui~="tooltip"])
    [data-tooltip-slot~="content"][data-popover-side="right"]
    [data-tooltip-slot~="arrow"] {
    right: calc(100% - var(--ui-tooltip-arrow-size) / 2);
    left: auto;
    top: 50%;
    transform: translate(0%, -50%) rotate(45deg);
  }
}

Attributes

tooltip attributes
AttributeKindValuesNotes
data-ui="tooltip"Identityfree valueIdentity token in data-ui.
<ui-tooltip>Tag formfree valueTag form; styled and, when scripted, registered under this name.
data-tooltip-slotSlotarrow content triggerSlot: a part inside the primitive.

Requires: popover, kbd.

Hooks

Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.

tooltip hooks
HookDefaultState
--ui-tooltip-arrow-colorvar(--ui-tooltip-bg)—
--ui-tooltip-arrow-radiuscalc(var(--spacing) * 0.5)—
--ui-tooltip-arrow-sizecalc(var(--spacing) * 2)—
--ui-tooltip-bgvar(--color-foreground)—
--ui-tooltip-border-colortransparent—
--ui-tooltip-font-familyvar(--font-family-mono)—
--ui-tooltip-font-sizevar(--font-size-xs)—
--ui-tooltip-font-weightvar(--font-weight-body)—
--ui-tooltip-gapvar(--spacing)—
--ui-tooltip-leadingvar(--leading-xs)—
--ui-tooltip-max-w16ch—
--ui-tooltip-mx0—
--ui-tooltip-myvar(--spacing)—
--ui-tooltip-opacity--end1end
--ui-tooltip-opacity--start0start
--ui-tooltip-pxvar(--space-xs)—
--ui-tooltip-pyvar(--spacing)—
--ui-tooltip-roundedvar(--radius-sm)—
--ui-tooltip-scale--end1end
--ui-tooltip-scale--start0.94start
--ui-tooltip-shadowvar(--shadow-sm)—
--ui-tooltip-textvar(--color-background)—
--ui-tooltip-transitionopacity var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function) var(--ui-tooltip-transition-delay), transform var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function) var(--ui-tooltip-transition-delay), overlay var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function) var(--ui-tooltip-transition-delay) allow-discrete, display var(--ui-tooltip-transition-duration) var(--ui-tooltip-transition-timing-function) var(--ui-tooltip-transition-delay) allow-discrete—
--ui-tooltip-transition-delay0ms—
--ui-tooltip-transition-durationcalc(var(--default-transition-duration) / 2)—
--ui-tooltip-transition-timing-functionvar(--default-transition-timing-function)—
--ui-tooltip-translate--end0, 0, 0end
--ui-tooltip-translate--start0, 0.25em, 0start