Docs
Search

Tooltip

This page is currently under construction
Lism UI (@lism-css/ui) is still in preparation.

A component for displaying supplementary text for an element on hover or focus.

Overview

Preview
Shortcut: Ctrl + S
<Tooltip.Root tooltipId="tt-01">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">Save</Tooltip.Trigger>
<Tooltip.Popup>Shortcut: Ctrl + S</Tooltip.Popup>
</Tooltip.Root>

Showing and hiding the tooltip is handled entirely with CSS. JavaScript only handles closing it with Esc.

  • It appears on hover over <Tooltip.Root>, or on keyboard focus (:focus-visible) of <Tooltip.Trigger>.
  • Moving the pointer onto the popup does not hide it.
  • Pressing Esc hides it. Moving the pointer or focus away and back shows it again. (This state is tracked via the data-dismissed attribute on <Tooltip.Root>.)

Placing <Tooltip.Trigger> and <Tooltip.Popup> as siblings inside <Tooltip.Root> automatically links them via aria-describedby and id.

  • Place <Tooltip.Popup> as a sibling after <Tooltip.Trigger>. The focus-visible display is controlled with a sibling selector, so reversing the order prevents the tooltip from appearing on focus.
  • Don’t place interactive elements like links or buttons inside a tooltip. If you need interactive content, use Popover instead.
  • Touch devices don’t trigger hover, so the tooltip can’t be reliably shown. Don’t put critical information only inside a tooltip.
  • <Tooltip.Trigger> must be a focusable element. If you use something other than the default button (e.g. as="span"), add tabindex="0".

Styles

The Tooltip base styles are defined in the following CSS.

_style.css
/*
 * 表示は CSS(:hover / :focus-visible)、配置は CSS Anchor Positioning。
 * anchor 配置は @supports で囲い、非対応(部分対応の Chrome 128〜130 含む)はルート基準の absolute 配置に落とす。
 */
@layer lism-block {
  /* 調整用の変数は Root で受ける(Root の props がインラインに書く --tooltip-* をここで拾う) */
  .b--tooltip {
    --duration: var(--tooltip-duration, 0.15s);
    --delay: var(--tooltip-delay, 0.4s); /* 表示までの待ち */
    --delay--close: var(--tooltip-delay--close, 0.15s); /* 退場猶予。トリガーからポップアップへポインタを移す間、表示を保つ */
    --offset: var(--tooltip-offset, var(--s5));
    --arrow-sz: var(--tooltip-arrow, 4px); /* 矢印の高さ。0 で矢印なし */
    --popup-gap: calc(var(--offset) + var(--arrow-sz)); /* popup 本体とトリガーの間隔。offset は矢印の先とトリガーの間隔 */
    display: inline-block;
    anchor-scope: --tooltip, --tooltip-popup;
  }
  .b--tooltip_trigger {
    anchor-name: --tooltip;
  }

  .b--tooltip_popup {
    --lh: 1.25;
    z-index: 10;
    inline-size: max-content;
    max-inline-size: min(20rem, 90vw);
    padding: 0.375em 0.625em;
    border-radius: var(--bdrs--10);
    background-color: var(--text);
    color: var(--base);
    font-size: var(--fz--s);
    visibility: hidden; /* pointer-events は使わない。transition しないため退場猶予中の当たり判定が消え、橋渡しが効かなくなる */
    opacity: 0;
    transition: var(--duration) var(--delay--close);
    transition-property: opacity, visibility;
  }

  /* 表示: ルートのホバー、またはトリガーのキーボードフォーカス(Popup は同じ Root 直下に置く前提) */
  .b--tooltip:hover > .b--tooltip_popup,
  .b--tooltip_trigger:focus-visible ~ .b--tooltip_popup {
    visibility: visible;
    opacity: 1;
    transition-delay: var(--delay);
  }

  /* Esc で閉じた状態(setTooltip.ts が付与)。同じ詳細度なので表示ルールより後に置いて勝たせる */
  .b--tooltip[data-dismissed] > .b--tooltip_popup {
    visibility: hidden;
    opacity: 0;
    transition: none;
  }

  /*
   * anchor 配置。position-area は側(--_side)と span(--_span)を var() で合成する。
   * align の start / end は span-* で片側に伸ばし place-self で端に揃える。span と揃える軸は side の系統ごとに違うので、
   * 系統側で --_span* / --_place* の候補を定義し、align 側はそれを選ぶだけにする。
   * top / bottom の align に span-x-* を使うのは RTL でも書字方向の端に揃えるため。物理と論理キーワードの混在は無効になるので混ぜない。
   */
  @supports (anchor-name: --a) and (anchor-scope: all) and (position-area: inline-start span-block-end) and (position-try-fallbacks: flip-block) {
    .b--tooltip_popup {
      --_span: span-all;
      position: fixed;
      anchor-name: --tooltip-popup; /* 矢印(::after)が popup の矩形を参照するため */
      position-anchor: --tooltip;
      position-area: var(--_side) var(--_span);
      /*
       * 非表示は display: none。visibility だけだと非表示中も配置され、スクロールで画面外に出た時点の flip が
       * 仕様(last successful position option)で記憶され、戻っても反転したまま残る。
       * 退場中は display を transition で残す(Firefox は display の transition 未対応で退場フェードが無くなるだけ)。
       */
      display: none;
      transition-property: opacity, visibility, display;
      transition-behavior: normal, normal, allow-discrete;
    }
    .b--tooltip:hover > .b--tooltip_popup,
    .b--tooltip_trigger:focus-visible ~ .b--tooltip_popup {
      display: block;
    }
    /* 入場の開始値。visibility を hidden から始め、入場ディレイ中は当たり判定を持たせない */
    @starting-style {
      .b--tooltip:hover > .b--tooltip_popup,
      .b--tooltip_trigger:focus-visible ~ .b--tooltip_popup {
        visibility: hidden;
        opacity: 0;
      }
    }
    .b--tooltip[data-dismissed] > .b--tooltip_popup {
      display: none;
    }
    /* トリガーとの隙間(offset)を透明な当たり判定で埋め、移動中に Root の :hover が外れないようにする。flip でどちら側に出ても効くよう全方向へ広げる */
    .b--tooltip_popup::before {
      content: '';
      position: absolute;
      inset: calc(-1 * var(--popup-gap));
      z-index: -1; /* 中身の上には被せない */
    }
    /*
     * ふきだしの矢印。ひし形の中心を「トリガー中心を popup の矩形にクランプした点」に置く。
     * flip 後の位置は CSS から知れない(@position-try でカスタムプロパティは設定できない)ので、
     * side / align / flip を見ずにトリガー側の辺へ寄るこの形にしている。
     * absolute だと popup が包含ブロックになりトリガーを anchor に取れないため fixed。
     * スクロール追従は既定アンカー(position-anchor)にしか効かないので、トリガーを既定アンカーにして省略形の anchor() で参照する。
     */
    .b--tooltip_popup::after {
      content: '';
      position: fixed;
      position-anchor: --tooltip;
      z-index: -1; /* 中身の下、popup の背景の上。内側半分は同色の背景に溶ける */
      inline-size: calc(var(--arrow-sz) * 2);
      block-size: calc(var(--arrow-sz) * 2);
      background-color: inherit;
      clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
      top: clamp(anchor(--tooltip-popup top), anchor(center), anchor(--tooltip-popup bottom));
      left: clamp(anchor(--tooltip-popup left), anchor(center), anchor(--tooltip-popup right));
      translate: -50% -50%;
    }
    /* --_place*: place-self の値(align-self justify-self)。揃える軸だけ start / end にする */
    .b--tooltip_popup:where([data-side='top'], [data-side='bottom']) {
      --_spanStart: span-x-end;
      --_spanEnd: span-x-start;
      --_placeStart: auto start;
      --_placeEnd: auto end;
      margin-block: var(--popup-gap);
      position-try-fallbacks:
        flip-block,
        flip-inline,
        flip-block flip-inline;
    }
    .b--tooltip_popup:where([data-side='start'], [data-side='end']) {
      --_spanStart: span-block-end;
      --_spanEnd: span-block-start;
      --_placeStart: start auto;
      --_placeEnd: end auto;
      margin-inline: var(--popup-gap);
      position-try-fallbacks:
        flip-inline,
        flip-block,
        flip-inline flip-block;
    }
    .b--tooltip_popup[data-side='top'] {
      --_side: top;
    }
    .b--tooltip_popup[data-side='bottom'] {
      --_side: bottom;
    }
    .b--tooltip_popup[data-side='start'] {
      --_side: inline-start;
    }
    .b--tooltip_popup[data-side='end'] {
      --_side: inline-end;
    }

    /* align: side 系統が用意した候補から選ぶ */
    .b--tooltip_popup:where([data-align='start']) {
      --_span: var(--_spanStart);
      place-self: var(--_placeStart);
    }
    .b--tooltip_popup:where([data-align='end']) {
      --_span: var(--_spanEnd);
      place-self: var(--_placeEnd);
    }
  }

  /* anchor 非対応: ルート基準の absolute 配置。side 軸と align 軸を別ルールにして打ち消しを不要にする */
  @supports not (
    (anchor-name: --a) and (anchor-scope: all) and (position-area: inline-start span-block-end) and (position-try-fallbacks: flip-block)
  ) {
    .b--tooltip {
      position: relative;
    }
    .b--tooltip_popup {
      position: absolute;
    }
    .b--tooltip_popup[data-side='top'] {
      bottom: 100%;
    }
    .b--tooltip_popup[data-side='bottom'] {
      top: 100%;
    }
    .b--tooltip_popup[data-side='start'] {
      inset-inline-end: 100%;
    }
    .b--tooltip_popup[data-side='end'] {
      inset-inline-start: 100%;
    }

    /* align: side が top / bottom なら横方向(inset-inline-* で書字方向に追従)、start / end なら縦方向の揃え */
    .b--tooltip_popup:is([data-side='top'], [data-side='bottom'])[data-align='center'] {
      left: 50%;
      translate: -50% 0;
    }
    .b--tooltip_popup:is([data-side='top'], [data-side='bottom'])[data-align='start'] {
      inset-inline-start: 0;
    }
    .b--tooltip_popup:is([data-side='top'], [data-side='bottom'])[data-align='end'] {
      inset-inline-end: 0;
    }
    .b--tooltip_popup:is([data-side='start'], [data-side='end'])[data-align='center'] {
      top: 50%;
      translate: 0 -50%;
    }
    .b--tooltip_popup:is([data-side='start'], [data-side='end'])[data-align='start'] {
      top: 0;
    }
    .b--tooltip_popup:is([data-side='start'], [data-side='end'])[data-align='end'] {
      bottom: 0;
    }
  }

  /* 減速設定時はフェードを無効化(ディレイは残す)。インラインの --duration 指定よりも優先させるため !important */
  @media (prefers-reduced-motion: reduce) {
    .b--tooltip {
      --duration: 0s !important;
    }
  }
}

The full source code is available on GitHub.

Usage

Provided as Tooltip from the @lism-css/ui package.

Import

import { Tooltip } from '@lism-css/ui/react';

<Tooltip.Root>, <Tooltip.Trigger>, and <Tooltip.Popup> are available.

Props

Prop Description
<Tooltip.Root>
tooltipId
Specifies the ID used for <Tooltip.Trigger>’s aria-describedby and <Tooltip.Popup>’s id. If omitted, an automatically generated ID is used.
<Tooltip.Root>
delay
Specifies the delay before the popup appears. Output as the --tooltip-delay variable.
<Tooltip.Root>
offset
Specifies the gap between the trigger and the popup. Output as the --tooltip-offset variable.
<Tooltip.Trigger>
tooltipId
Output as the value of aria-describedby. Only specify this when using the component standalone, outside <Tooltip.Root>.
<Tooltip.Popup>
id
Output as the id attribute. Only specify this when using the component standalone, outside <Tooltip.Root>.
<Tooltip.Popup>
side
Specifies the side the popup appears on. Output as the data-side attribute. Accepts top / bottom / start / end. start / end are the horizontal sides and follow the writing direction (start is left and end is right in dir="ltr"). Default: top.
<Tooltip.Popup>
align
Specifies the alignment along the axis perpendicular to side. Output as the data-align attribute. Accepts start / center / end. Default: center.

Don’t set individual IDs on <Tooltip.Trigger> or <Tooltip.Popup> when they’re inside <Tooltip.Root>. If you need to set an explicit ID, use only the tooltipId prop on <Tooltip.Root>.

The child-level tooltipId / id props exist for placing the parts apart from each other without <Tooltip.Root> (passing the same ID to each part yourself). Setting them on only some children inside <Tooltip.Root> breaks the linkage.

CSS variables

Use the following CSS variables to adjust the appearance. They’re all read on <Tooltip.Root> (.b--tooltip), so set them on <Tooltip.Root> or one of its ancestors. Setting them on <Tooltip.Popup> has no effect.

Variable Default Description
--tooltip-offset var(--s5) The gap between the trigger and the popup.
--tooltip-delay 0.4s The delay before the popup appears.
--tooltip-delay--close 0.15s The grace period before the popup hides. Keeps the popup visible while the pointer moves from the trigger to the popup.
--tooltip-duration 0.15s The duration of the fade transition. Set to 0s when prefers-reduced-motion: reduce is set.

Override the color, spacing, border radius, and shadow using Lism properties (bgc, c, p, bdrs, bxsh, etc.). By default, the tooltip uses an inverted color scheme with --text as the background and --base as the text color.

Examples

Changing the popup side

Use the side prop on <Tooltip.Popup> to specify which side the popup appears on.

Setting side
side=“top” side=“bottom” side=“start” side=“end”
<Cluster g="15">
<Tooltip.Root tooltipId="tt-02">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">top</Tooltip.Trigger>
<Tooltip.Popup side="top">side="top"</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root tooltipId="tt-03">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">bottom</Tooltip.Trigger>
<Tooltip.Popup side="bottom">side="bottom"</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root tooltipId="tt-04">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">start</Tooltip.Trigger>
<Tooltip.Popup side="start">side="start"</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root tooltipId="tt-05">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">end</Tooltip.Trigger>
<Tooltip.Popup side="end">side="end"</Tooltip.Popup>
</Tooltip.Root>
</Cluster>

start / end are logical directions that follow the writing direction: start is left and end is right in dir="ltr", and they swap sides when dir="rtl" is set.

Regardless of which side you choose, the popup automatically flips to the opposite side if it doesn’t fit within the viewport.

Changing the alignment

Use the align prop on <Tooltip.Popup> to set the alignment along the axis perpendicular to side. It aligns horizontally when side is top / bottom, and vertically when side is start / end.

Setting align
side=“bottom” align=“start” side=“bottom” align=“end” side=“end” align=“start”
<Cluster g="15">
<Tooltip.Root tooltipId="tt-11">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">bottom / start</Tooltip.Trigger>
<Tooltip.Popup side="bottom" align="start">side="bottom" align="start"</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root tooltipId="tt-12">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">bottom / end</Tooltip.Trigger>
<Tooltip.Popup side="bottom" align="end">side="bottom" align="end"</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root tooltipId="tt-13">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">end / start</Tooltip.Trigger>
<Tooltip.Popup side="end" align="start">side="end" align="start"</Tooltip.Popup>
</Tooltip.Root>
</Cluster>

When side is top / bottom, the start / end values of align follow the writing direction and swap sides when dir="rtl" is set. When side is start / end, start aligns to the top and end to the bottom.

Changing the delay and offset

Adjust the delay before the popup appears with the delay prop on <Tooltip.Root>, and the gap from the trigger with the offset prop.

Setting delay and offset
delay=“0s” delay=“1s” / offset=“1rem”
<Cluster g="15">
<Tooltip.Root tooltipId="tt-08" delay="0s">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">Show instantly</Tooltip.Trigger>
<Tooltip.Popup>delay="0s"</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root tooltipId="tt-09" delay="1s" offset="1rem">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">Show after 1s</Tooltip.Trigger>
<Tooltip.Popup>delay="1s" / offset="1rem"</Tooltip.Popup>
</Tooltip.Root>
</Cluster>

Changing the color scheme

Add Lism properties to <Tooltip.Popup> to change its color and spacing.

Custom colors
Overriding the background, text color, and spacing.
<Tooltip.Root tooltipId="tt-10">
<Tooltip.Trigger className="-bd -px:15 -py:5 -bdrs:10">Tooltip with custom colors</Tooltip.Trigger>
<Tooltip.Popup bgc="base-2" c="text" bd p="15" bdrs="10" bxsh="20">Overriding the background, text color, and spacing.</Tooltip.Popup>
</Tooltip.Root>

Browser support

Tooltip positioning uses CSS Anchor Positioning.

In browsers without support (e.g. Firefox ESR 140), it falls back to absolute positioning relative to <Tooltip.Root> (the element that wraps the trigger). In this case, the automatic flip when the popup doesn’t fit within the viewport doesn’t happen, so the tooltip may get clipped. The offset (--tooltip-offset) gap also isn’t applied.

Firefox doesn’t support transitioning the display property, so in Firefox 147 and later, where Anchor Positioning is active, the fade-out and the --tooltip-delay--close grace period don’t apply and the tooltip disappears as soon as the pointer leaves. The fade-in and its delay still work.

© 2026 Lism CSS.