ツールチップ コンポーネントの作成

色に適応し、ユーザー補助に対応したツールチップ カスタム要素を作成する方法の基本的な概要。

この記事では、色に適応し、ユーザー補助に対応した <tool-tip> カスタム要素を作成する方法について説明します。デモを試すソースを表示する

さまざまな例と配色で動作するツールチップが表示されます

動画で視聴したい場合は、この投稿の YouTube バージョンをご利用ください。

概要

ツールチップは、ユーザー インターフェースの補足情報を含んだ、モーダルではない、ブロックしない、インタラクティブではないオーバーレイです。デフォルトでは非表示で、関連する要素にカーソルを合わせたりフォーカスしたりすると表示されます。ツールチップは直接選択または操作できません。ツールチップは、ラベルなどの価値の高い情報の代用ではありません。ユーザーはツールチップなしでタスクを完全に完了できる必要があります。

すべきこと: 入力には常にラベルを付けます。
しないでください: ラベルではなくツールチップに依存する

Toggletip と Tooltip

多くのコンポーネントと同様に、ツールチップについてはさまざまな説明があります(MDNWAI ARIASarah HigleyInclusive Components など)。ツールチップと切り替えチップが分離されている点が気に入っています。ツールチップには非対話型の補足情報を含める必要がありますが、切り替えチップには対話型と重要な情報を含めることができます。分割が行われる主な理由は、アクセシビリティ、ポップアップへの移動、内部の情報やボタンへのアクセスについてユーザーが期待する方法です。切り替えツールチップはすぐに複雑になります。

Designcember サイトの切り替えチップに関する動画をご覧ください。これは、ユーザーがピンで開いて探索し、ライトを閉じるか Esc キーで閉じることができる、インタラクティブ性のあるオーバーレイです。

この GUI チャレンジでは、ほとんどの処理を CSS で行うために、ツールチップを選択しました。以下に、その作成方法を示します。

マークアップ

カスタム要素 <tool-tip> を使用することに決めました。必要に応じて、カスタム要素をウェブ コンポーネントにする必要はありません。ブラウザは <foo-bar><div> と同様に扱います。カスタム要素は、より具体的でないクラス名のようなものです。JavaScript は使用されません。

<tool-tip>A tooltip</tool-tip>

これは、内部にテキストがある div のようなものです。対応するスクリーン リーダーのユーザー補助ツリーに関連付けるには、[role="tooltip"] を追加します。

<tool-tip role="tooltip">A tooltip</tool-tip>

現在は、スクリーン リーダーではツールチップとして認識されます。次の例では、最初のリンク要素のツリーに認識されたツールチップ要素があり、2 番目のリンク要素には認識されたツールチップ要素がないことをご確認ください。2 番目にはロールがありません。スタイル セクションでは、このツリービューを改善します。

HTML を表す Chrome DevTools のユーザー補助ツリーのスクリーンショット。「top、ツールチップがあります: ツールチップがあります」というテキスト付きのリンクが表示されます。これはフォーカス可能です。内部には「top」という静的テキストとツールチップ要素があります。

次に、ツールチップをフォーカス不可にする必要があります。スクリーン リーダーがツールチップ ロールを認識していない場合、ユーザーは <tool-tip> にフォーカスしてコンテンツを読み取ることができますが、ユーザー エクスペリエンスではこの操作は必要ありません。スクリーン リーダーはコンテンツを親要素に追加するため、アクセス可能にするためにフォーカスする必要はありません。ここでは、inert を使用して、ユーザーがタブフロー内でこのツールチップ コンテンツを誤って見つけないようにします。

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Chrome DevTools のユーザー補助ツリーの別のスクリーンショット。今回はツールチップ要素がありません。

次に、ツールチップの位置を指定するインターフェースとして、属性を使用することを選択しました。デフォルトでは、すべての <tool-tip> は「上」の位置になりますが、tip-position を追加することで要素の位置をカスタマイズできます。

<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

右側に「ツールチップ」というツールチップが表示されたリンクのスクリーンショット。

このような場合は、クラスではなく属性を使用する傾向があります。これにより、<tool-tip> に複数の位置を同時に割り当てることができなくなります。どちらか一方のみを指定することも、何も指定しないことも可能です。

最後に、ツールチップを表示する要素内に <tool-tip> 要素を配置します。ここでは、<picture> 要素内に画像と <tool-tip> を配置して、alt テキストを目の見えないユーザーと共有しています。

<picture>
  <img alt="The GUI Challenges skull logo" width="100" src="...">
  <tool-tip role="tooltip" tip-position="bottom">
    The <b>GUI Challenges</b> skull logo
  </tool-tip>
</picture>

「The GUI Challenges skull logo」というツールチップが表示された画像のスクリーンショット。

ここでは、<abbr> 要素内に <tool-tip> を配置します。

<p>
  The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

頭字語 HTML が下線付きで表示されている段落のスクリーンショット。その上には「ハイパーテキスト マークアップ言語」というツールチップが表示されています。

ユーザー補助

ここでは、切り替えチップではなくツールチップを作成することにしたため、このセクションはかなり簡単になっています。まず、Google が目指すユーザー エクスペリエンスについて概要を説明します。

  1. 制約のあるスペースやインターフェースが雑然としている場合は、補足メッセージを非表示にします。
  2. ユーザーが要素にカーソルを合わせたり、フォーカスを合わせたり、タップして操作したりすると、メッセージを表示します。
  3. ホバー、フォーカス、タップが終了したら、メッセージを再度非表示にします。
  4. 最後に、ユーザーがモーションの低減を指定している場合は、モーションを低減します。

目標は、オンデマンドの補足メッセージです。視覚に障がいのないマウスまたはキーボードのユーザーは、カーソルを合わせるとメッセージが表示され、目で読むことができます。スクリーン リーダーを使用している視覚障がいのあるユーザーは、フォーカスを当ててメッセージを表示し、ツールで音声で受け取ることができます。

ツールチップ付きのリンクを macOS VoiceOver が読み上げるスクリーンショット

前のセクションでは、ユーザー補助ツリー、ツールチップ ロール、不許可について説明しました。残っているのは、それをテストし、ユーザー エクスペリエンスによってツールチップ メッセージがユーザーに適切に表示されることを確認することです。テストしたところ、音声メッセージのどの部分がツールチップなのか不明です。これは、ユーザー補助ツリーでデバッグ中に確認できます。リンクテキスト「top」は「Look, tooltips!」と連続して実行され、中断はありません。スクリーン リーダーは、テキストを区切ったり、ツールチップ コンテンツとして識別したりしません。

Chrome DevTools のユーザー補助ツリーのスクリーンショット。リンクテキストは「top Hey, a tooltip!」です。

スクリーン リーダー専用の疑似要素を <tool-tip> に追加すると、視覚障がいのあるユーザー向けに独自のプロンプト テキストを追加できます。

&::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

以下は、更新されたユーザー補助ツリーです。リンクテキストの後にセミコロンがあり、ツールチップのプロンプト「Has tooltip:」が追加されています。

Chrome DevTools のユーザー補助ツリーの更新されたスクリーンショット。リンクテキストの表現が改善され、「top ; Has tooltip: Hey, a tooltip!」になっています。

スクリーン リーダーのユーザーがリンクにフォーカスを当てると、「上」と読み上げられて少しの間停止し、「ツールチップあり: ツールチップ」と読み上げられるようになりました。これにより、スクリーン リーダー ユーザーに UX に関するヒントをいくつか提供できます。遅延により、リンクテキストとツールチップが適切に分離されます。また、「ツールチップあり」が読み上げられたときに、画面読み上げのユーザーは、すでに聞いたことがある場合は簡単にキャンセルできます。補足メッセージですでに見たように、すばやくホバーしてからホバーを解除する動作に似ています。これは UX の同等性を確保するうえで適切だと感じました。

スタイル

<tool-tip> 要素は、補足メッセージの要素の子要素になります。まずは、オーバーレイ エフェクトの基本事項から確認しましょう。position absolute を使用してドキュメント フローの対象から除外します。

tool-tip {
  position: absolute;
  z-index: 1;
}

親がスタック コンテキストでない場合、ツールチップは最も近いコンテキスト(ここでの目的ではない)に配置されます。ブロックに新しいセレクタ :has() が追加されています。

対応ブラウザ

  • Chrome: 105。
  • Edge: 105。
  • Firefox: 121。
  • Safari: 15.4。

ソース

:has(> tool-tip) {
  position: relative;
}

ブラウザのサポートについては、あまり心配する必要はありません。まず、これらのツールチップは補足的なものであることを覚えておいてください。それでも問題が解決しない場合は、次に、JavaScript セクションで、:has() をサポートしていないブラウザに必要な機能をポリフィルするスクリプトをデプロイします。

次に、親要素からポインタ イベントを奪わないように、ツールチップを非インタラクティブにします。

tool-tip {
  
  pointer-events: none;
  user-select: none;
}

次に、不透明度でツールチップを非表示にして、クロスフェードでツールチップを遷移できるようにします。

tool-tip {
  opacity: 0;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
}

ここでは複雑な処理を :is():has() が行い、子ツールチップの表示 / 非表示を切り替える際に、親要素を含む tool-tip がユーザーの操作を認識できるようにします。マウスではカーソルを合わせ、キーボードとスクリーン リーダーではフォーカスを、タップではタップできます。

視覚障がいのあるユーザーに対してオーバーレイの表示と非表示が機能するようになりました。次は、テーマ設定、配置、バブルへの三角形の追加のためのスタイルを追加します。次のスタイルでは、カスタム プロパティを使用して、これまでのスタイルを拡張し、シャドウ、タイポグラフィ、色を追加して、フローティング ツールチップのようにします。

ダークモードのツールチップがリンク「block-start」の上に浮かんでいるスクリーンショット。

tool-tip {
  --_p-inline: 1.5ch;
  --_p-block: .75ch;
  --_triangle-size: 7px;
  --_bg: hsl(0 0% 20%);
  --_shadow-alpha: 50%;

  --_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
  --_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
  --_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
  --_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;

  pointer-events: none;
  user-select: none;

  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;

  position: absolute;
  z-index: 1;
  inline-size: max-content;
  max-inline-size: 25ch;
  text-align: start;
  font-size: 1rem;
  font-weight: normal;
  line-height: normal;
  line-height: initial;
  padding: var(--_p-block) var(--_p-inline);
  margin: 0;
  border-radius: 5px;
  background: var(--_bg);
  color: CanvasText;
  will-change: filter;
  filter:
    drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
    drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}

/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
  position: relative;
}

/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

/* prepend some prose for screen readers only */
tool-tip::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
  content: "";
  background: var(--_bg);
  position: absolute;
  z-index: -1;
  inset: 0;
  mask: var(--_tip);
}

/* top tooltip styles */
tool-tip:is(
  [tip-position="top"],
  [tip-position="block-start"],
  :not([tip-position]),
  [tip-position="bottom"],
  [tip-position="block-end"]
) {
  text-align: center;
}

テーマの調整

テキストの色はシステム キーワード CanvasText を介してページから継承されるため、ツールチップで管理する色はごくわずかです。また、値を保存するカスタム プロパティを作成したので、それらのカスタム プロパティのみを更新し、残りはテーマに処理させることができます。

@media (prefers-color-scheme: light) {
  tool-tip {
    --_bg: white;
    --_shadow-alpha: 15%;
  }
}

ツールチップのライトモードとダークモードのスクリーンショットを並べて表示しています。

ライトモードでは、背景を白に適応させ、不透明度を調整してシャドウを弱くします。

右から左

右から左の読み取りモードをサポートするために、カスタム プロパティはドキュメントの向きの値をそれぞれ -1 または 1 の値に格納します。

tool-tip {
  --isRTL: -1;
}

tool-tip:dir(rtl) {
  --isRTL: 1;
}

これは、ツールチップの配置に役立ちます。

tool-tip[tip-position="top"]) {
  --_x: calc(50% * var(--isRTL));
}

また、三角形の位置も特定できます。

tool-tip[tip-position="right"]::after {
  --_tip: var(--_left-tip);
}

tool-tip[tip-position="right"]:dir(rtl)::after {
  --_tip: var(--_right-tip);
}

最後に、translateX() の論理変換にも使用できます。

--_x: calc(var(--isRTL) * -3px * -1);

ツールチップの配置

inset-block プロパティまたは inset-inline プロパティを使用してツールチップを論理的に配置し、ツールチップの物理的な位置と論理的な位置の両方を処理します。次のコードは、左から右と右から左の両方の方向で、4 つの位置のそれぞれにスタイルを設定する方法を示しています。

上揃えとブロック開始揃え

左から右のトップ位置と右から左のトップ位置のプレースメントの違いを示すスクリーンショット。

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
  inset-inline-start: 50%;
  inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
  --_tip: var(--_bottom-tip);
  inset-block-end: calc(var(--_triangle-size) * -1);
  border-block-end: var(--_triangle-size) solid transparent;
}

右揃えとインラインの末尾揃え

左から右の右側の位置と右から左のインライン エンドの位置のプレースメントの違いを示すスクリーンショット。

tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
  inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
  --_tip: var(--_left-tip);
  inset-inline-start: calc(var(--_triangle-size) * -1);
  border-inline-start: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
  --_tip: var(--_right-tip);
}

下端とブロック端の配置

左から右の下部と右から左のブロックの端の位置の違いを示すスクリーンショット。

tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
  inset-inline-start: 50%;
  inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
  --_tip: var(--_top-tip);
  inset-block-start: calc(var(--_triangle-size) * -1);
  border-block-start: var(--_triangle-size) solid transparent;
}

左揃えとインライン開始揃え

左から右の左位置と右から左のインライン開始位置の配置の違いを示すスクリーンショット。

tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
  inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
  --_tip: var(--_right-tip);
  inset-inline-end: calc(var(--_triangle-size) * -1);
  border-inline-end: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
  --_tip: var(--_left-tip);
}

アニメーション

ここまでは、ツールチップの表示 / 非表示を切り替えました。このセクションでは、まず、すべてのユーザーを対象に、不透明度をアニメーション化します。これは、一般に安全な縮小されたモーションの遷移であるためです。次に、親要素からスライドアウトするようにツールチップの位置をアニメーション化します。

安全で有意義なデフォルトの遷移

次のように、ツールチップ要素のスタイルを設定して、不透明度と変換を遷移させます。

tool-tip {
  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

切り替え効果にモーションを追加する

ツールチップが表示される各側について、ユーザーが動きを許可している場合は、移動する距離を少し指定して translateX プロパティを少しずつ配置します。

@media (prefers-reduced-motion: no-preference) {
  :has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: 3px;
  }

  :has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: -3px;
  }

  :has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: -3px;
  }

  :has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: 3px;
  }
}

「in」状態が translateX(0) にあるため、「out」状態が設定されていることに注意してください。

JavaScript

私の意見では、JavaScript は任意です。これは、UI でタスクを完了するために、これらのツールチップを読む必要がないためです。そのため、ツールチップが完全に機能しなくても問題ありません。また、ツールチップを段階的に拡張することもできます。最終的にはすべてのブラウザが :has() をサポートし、このスクリプトは完全に不要になります。

ポリフィル スクリプトは、ブラウザが :has() をサポートしていない場合にのみ、次の 2 つの処理を行います。まず、:has() のサポートを確認します。

if (!CSS.supports('selector(:has(*))')) {
  // do work
}

次に、<tool-tip> の親要素を見つけて、操作するクラス名を指定します。

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))
}

次に、そのクラス名を使用する一連のスタイルを挿入して、まったく同じ動作の :has() セレクタをシミュレートします。

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))

  let styles = document.createElement('style')
  styles.textContent = `
    .has_tool-tip {
      position: relative;
    }
    .has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
      opacity: 1;
      transition-delay: 200ms;
    }
  `
  document.head.appendChild(styles)
}

これで、:has() がサポートされていない場合でもすべてのブラウザでツールチップが表示されるようになりました。

まとめ

私がどのようにしたかがわかったところで、皆さんはどうしますか?popup API でトグルチップを簡単にし、トップレイヤで z インデックスの競合を回避し、anchor API でウィンドウ内のオブジェクトを適切に配置する方法が楽しみです。それまではツールチップを作成します

アプローチを多様化し、ウェブで構築するすべての方法を学びましょう。

デモを作成し、ツイートしてください。リンクを送っていただければ、以下のコミュニティ リミックスのセクションに追加いたします。

コミュニティ リミックス

表示する項目はありません。

リソース