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

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

この投稿では、色適応的でアクセスしやすい <tool-tip> カスタム要素を作成する方法について、私の考えを共有したいと思います。デモをお試しください。また、ソースを確認してください。

ツールチップがさまざまな例とカラーパターンに対して表示されます

動画をご覧になる場合は、この投稿の YouTube バージョンをご覧ください。

概要

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

推奨事項: 入力値には必ずラベルを付けます。
非推奨: ラベルではなくツールチップを使用する

トグルヒントとツールチップ

多くのコンポーネントと同様に、ツールチップの説明もさまざまです(MDNWAI ARIASarah Higleyインクルーシブ コンポーネントなど)。ツールチップとトグルチップが分離されているところが気に入っています。ツールチップには非対話型の補足情報を含め、トグルヒントには対話型と重要な情報を含めます。分割の主な理由は、ユーザー補助、ユーザーがポップアップ内をどのように移動し、その情報やボタンにどうアクセスできるかです。トグルチップはすぐに複雑になります。

次の動画では、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>

スクリーン リーダーでは、ツールチップとして認識されます。次の例で、1 つ目のリンク要素にはツリー内に認識されたツールチップ要素があり、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 に下線が引かれ、上に「Hyper Text Markup Language」と表示されるツールチップがある段落のスクリーンショット。

ユーザー補助

今回は切り替えチップではなくツールチップを作成することにしたので、このセクションはずっと簡単になります。まず、期待するユーザー エクスペリエンスの概要を説明します。

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

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

ツールチップ付きのリンクを読み取っている MacOS VoiceOver のスクリーンショット

前のセクションでは、ユーザー補助ツリー、ツールチップ ロールとイナートについて説明しましたが、あとはそれをテストして、ユーザー エクスペリエンスがツールチップ メッセージを適切に表示することを確認することです。テストでは、可聴メッセージのどの部分がツールチップであるかが不明確です。これは、ユーザー補助ツリーでデバッグする際にも表示されます。「top」のリンクテキストは、ためらうことなく「Look, tooltips!」とともに一緒に実行されます。スクリーン リーダーは、テキストをツールチップのコンテンツとして破損したり、識別したりしません。

リンクテキストに「top Hey, a tooltip!」と表示されている Chrome DevTools ユーザー補助ツリーのスクリーンショット。

スクリーン リーダー専用の疑似要素を <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() が、ブロックに用意されています。

対応ブラウザ

  • 105
  • 105
  • 121
  • 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-Index のないバトルのための最上位レイヤ、ウィンドウ内に物事をより適切に配置するための anchor API を楽しみにしています。それまではツールチップを作成しましょう。

多様なアプローチを活用し、ウェブでアプリをビルドするためのあらゆる方法を学びましょう。

デモを作成してツイートのリンクをお願いします。下のコミュニティ リミックス セクションに追加します。

コミュニティのリミックス

まだ何も表示されません。

リソース