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

色適応型でユーザー補助に優れたツールチップ カスタム要素を構築する方法の基本的な概要。

この記事では、色適応型でアクセシビリティの高い <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 デベロッパー ツールのユーザー補助ツリーの別のスクリーンショット。今回はツールチップ要素がありません。

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

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

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

このような場合は、クラスではなく属性を使用する傾向があります。これにより、<tool-tip> に複数の位置を同時に割り当てることができなくなります。1 つまたはなし。

最後に、ツールチップを表示する要素の中に <tool-tip> 要素を配置します。ここでは、画像と <tool-tip><picture> 要素内に配置して、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>

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

ここでは、<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 という頭字語に下線が引かれ、その上に「Hyper Text Markup Language」というツールチップが表示されている段落のスクリーンショット。

ユーザー補助

トグルチップではなくツールチップを構築することにしたため、このセクションははるかにシンプルになります。まず、望ましいユーザー エクスペリエンスの概要を説明します。

  1. スペースが限られている場合やインターフェースが煩雑な場合は、補足メッセージを非表示にします。
  2. ユーザーが要素にカーソルを合わせたり、フォーカスを置いたり、タッチ操作を行ったりしたときに、メッセージを表示します。
  3. ホバー、フォーカス、タップが終了したら、メッセージを再度非表示にします。
  4. 最後に、ユーザーがモーションの軽減を希望している場合は、モーションが軽減されていることを確認します。

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

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

前のセクションでは、アクセシビリティ ツリー、ツールチップのロール、inert について説明しました。残りの作業は、テストを行い、ユーザー エクスペリエンスが適切にツールチップ メッセージをユーザーに表示することを確認することです。テストしたところ、音声メッセージのどの部分がツールチップなのかがわかりませんでした。アクセシビリティ ツリーでデバッグしているときにも確認できます。「Look, tooltips!」というリンク テキストが「top」と躊躇なく一緒に実行されています。スクリーン リーダーはテキストを分割したり、ツールチップのコンテンツとして識別したりしません。

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 デベロッパー ツールのユーザー補助ツリーの更新されたスクリーンショット。リンクテキストの文言が「top ; Has tooltip: Hey, a tooltip!」に改善されています。

スクリーン リーダーのユーザーがリンクにフォーカスを当てると、「トップ」と読み上げられ、少し間を置いてから「ツールチップあり: ツールチップを見る」と読み上げられます。これにより、スクリーン リーダーのユーザーに UX のヒントがいくつか提供されます。この遅延により、リンクテキストとツールチップが適切に分離されます。また、「ツールチップあり」と読み上げられた場合、スクリーン リーダーのユーザーは、以前に聞いたことがある場合は簡単にキャンセルできます。補足メッセージをすでに確認しているため、すばやくホバーとホバー解除を繰り返す操作とよく似ています。これは UX のパリティとして適切に感じられました。

スタイル

<tool-tip> 要素は、補足メッセージを表示する要素の子要素になります。まず、オーバーレイ効果の基本から始めましょう。position absolute を使用してドキュメント フローから削除します。

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

親がスタッキング コンテキストでない場合、ツールチップは最も近いスタッキング コンテキストに配置されますが、これは望ましい動作ではありません。この問題を解決する新しいセレクタ :has() があります。

Browser Support

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

Source

: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-index の競合を回避できる top layer、ウィンドウ内の要素をより適切に配置できる anchor API の登場を心待ちにしています。それまでは、ツールチップを作成します。

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

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

コミュニティ リミックス

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

リソース