Tooltip
A component for displaying supplementary text for an element on hover or focus.
Overview
<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><span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-01">Save</button> <span class="b--tooltip_popup" role="tooltip" id="tt-01" data-side="top" data-align="center">Shortcut: Ctrl + S</span></span>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-dismissedattribute 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 defaultbutton(e.g.as="span"), addtabindex="0".
Styles
The Tooltip base styles are defined in the following 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'; import { Tooltip } from '@lism-css/ui/astro'; <link href="https://cdn.jsdelivr.net/npm/@lism-css/ui@0.29.0/dist/style.css" rel="stylesheet" /> <script type="module" src="https://cdn.jsdelivr.net/npm/@lism-css/ui@0.29.0/dist/scripts/tooltip.js"></script> <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.
side<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><div class="l--cluster -g:15"> <span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-02">top</button> <span class="b--tooltip_popup" role="tooltip" id="tt-02" data-side="top" data-align="center">side="top"</span> </span> <span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-03">bottom</button> <span class="b--tooltip_popup" role="tooltip" id="tt-03" data-side="bottom" data-align="center">side="bottom"</span> </span> <span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-04">start</button> <span class="b--tooltip_popup" role="tooltip" id="tt-04" data-side="start" data-align="center">side="start"</span> </span> <span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-05">end</button> <span class="b--tooltip_popup" role="tooltip" id="tt-05" data-side="end" data-align="center">side="end"</span> </span></div>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.
align<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><div class="l--cluster -g:15"> <span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-11">bottom / start</button> <span class="b--tooltip_popup" role="tooltip" id="tt-11" data-side="bottom" data-align="start">side="bottom" align="start"</span> </span> <span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-12">bottom / end</button> <span class="b--tooltip_popup" role="tooltip" id="tt-12" data-side="bottom" data-align="end">side="bottom" align="end"</span> </span> <span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-13">end / start</button> <span class="b--tooltip_popup" role="tooltip" id="tt-13" data-side="end" data-align="start">side="end" align="start"</span> </span></div>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.
delay and offset<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><div class="l--cluster -g:15"> <span class="b--tooltip" style="--tooltip-delay:0s"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-08">Show instantly</button> <span class="b--tooltip_popup" role="tooltip" id="tt-08" data-side="top" data-align="center">delay="0s"</span> </span> <span class="b--tooltip" style="--tooltip-delay:1s;--tooltip-offset:1rem"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-09">Show after 1s</button> <span class="b--tooltip_popup" role="tooltip" id="tt-09" data-side="top" data-align="center">delay="1s" / offset="1rem"</span> </span></div>Changing the color scheme
Add Lism properties to <Tooltip.Popup> to change its 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><span class="b--tooltip"> <button class="-bd -px:15 -py:5 -bdrs:10 b--tooltip_trigger set--plain" type="button" aria-describedby="tt-10">Tooltip with custom colors</button> <span class="b--tooltip_popup -bgc:base-2 -c:text -bd -p:15 -bdrs:10 -bxsh:20" role="tooltip" id="tt-10" data-side="top" data-align="center">Overriding the background, text color, and spacing.</span></span>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.