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

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

この記事では、色に適応し、ユーザー補助に対応した <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 ; Has tooltip: Hey, a tooltip!」というテキストのリンクが表示され、フォーカスできます。内部には「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>

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

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

最後に、ツールチップを表示する要素内に <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 がツールチップ付きのリンクを読み上げるスクリーンショット

前のセクションでは、ユーザー補助ツリー、ツールチップ ロール、inert について説明しました。残す作業は、テストを行い、ユーザー エクスペリエンスでツールチップ メッセージが適切にユーザーに表示されることを検証することです。テストしたところ、音声メッセージのどの部分がツールチップなのか不明です。これは、ユーザー補助ツリーでデバッグ中に確認できます。リンクテキスト「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 でウィンドウ内のオブジェクトを適切に配置できるようになることを楽しみにしています。それまでは、ツールチップを作成します。

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

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

コミュニティ リミックス

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

リソース