アクセシブルなツールチップとポップオーバーの実装:キーボード操作とARIA活用
はじめに
ウェブサイトやアプリケーションでよく利用されるUIコンポーネントの一つに、ツールチップやポップオーバーがあります。要素にマウスカーソルを重ねたり、クリックしたりした際に、追加情報が表示される仕組みです。しかし、これらがマウス操作にのみ依存している場合、キーボード操作やスクリーンリーダーを利用するユーザーにとっては情報にアクセスできなかったり、使いづらさを感じたりする大きな要因となります。
本記事では、ツールチップとポップオーバーをアクセシブルに実装するための具体的な方法を、コード例を交えながら解説します。
なぜツールチップ・ポップオーバーのアクセシビリティ対応が必要か
ツールチップやポップオーバーは、通常、要素の :hover
や :focus
といった状態、あるいはクリックイベントによって表示・非表示が切り替わります。
- キーボードユーザー: マウスを使わず、キーボード(Tabキー、Enterキー、Spaceキーなど)で操作するユーザーは、
:hover
状態を発生させることができません。要素にフォーカスを当てた際に情報が表示され、かつそのツールチップ/ポップオーバー自体にもキーボードでアクセスできる必要があります。また、Escキーで閉じられるといった操作性も求められます。 - スクリーンリーダーユーザー: スクリーンリーダーは、画面上の要素を読み上げますが、単にCSSで表示・非表示を切り替えているだけの要素は、適切な情報が付与されていないと内容が読み上げられない、あるいは関連性が理解できないといった問題が発生します。表示された情報がトリガー要素と関連付けられ、必要なタイミングでユーザーに伝えられるようにする必要があります。
- 認知障がいのあるユーザー: 表示された追加情報がすぐに消えてしまったり、表示位置が固定されず追従したりする場合、情報を読み取るのに時間がかかるユーザーにとっては内容を把握しきれない可能性があります。
これらの課題を解決し、すべてのユーザーがツールチップやポップオーバーによって提供される情報にアクセスできるようにすることが、アクセシブルな実装の目的です。
アクセシブルな実装方法の基本原則
アクセシブルなツールチップやポップオーバーを実装するためには、以下の点を考慮する必要があります。
-
キーボード操作への対応:
- トリガー要素(ツールチップを表示させる要素)にフォーカスが当たった際にツールチップ/ポップオーバーが表示されるようにする。
- 表示されたツールチップ/ポップオーバーは、Escキーで閉じられるようにする。
- ポップオーバーの場合、内部にインタラクティブな要素(リンクやボタンなど)が含まれる可能性があるため、表示されたポップオーバー内部にフォーカスが移動し、タブ移動できる必要がある場合があります。(ツールチップは通常、補足情報のみのため、フォーカス移動は不要なケースが多いです)
- ポップオーバーが閉じられた後、フォーカスが適切な位置(通常はトリガー要素)に戻るように制御する。
-
ARIA属性の活用:
- トリガー要素とツールチップ/ポップオーバーの内容との関連付けを明確にするために、ARIA属性を使用します。
- ツールチップ:
aria-describedby
属性が適しています。トリガー要素にaria-describedby="[ツールチップ要素のID]"
を指定することで、スクリーンリーダーはトリガー要素にフォーカスが当たった際にそのラベルや役割に加えて、関連付けられたツールチップの内容を読み上げます。 - ポップオーバー:
aria-haspopup
属性を使用して、トリガー要素がポップアップ要素(メニュー、ダイアログなど)を制御することを示します。また、表示状態をaria-expanded
属性で示し、トリガー要素にaria-controls="[ポップオーバー要素のID]"
を使用して制御する要素との関連を示すことも検討できます。ポップオーバーがダイアログのような役割を持つ場合はrole="dialog"
やaria-modal="true"
なども使用します。
-
表示・非表示の制御:
- CSSの
:hover
や:focus
だけでは、キーボード操作による表示や、JavaScriptでの動的な制御、ARIA属性の切り替えが困難です。JavaScriptを使って、トリガー要素へのフォーカスイベント、クリックイベント、Escキーの押下などを検知し、ツールチップ/ポップオーバー要素の表示・非表示を切り替えるのが一般的です。 - 表示・非表示の切り替えには、要素の
display
プロパティをnone
とblock
/inline-block
などで切り替えるか、visibility
プロパティをhidden
とvisible
で切り替える方法があります。display: none;
やvisibility: hidden;
は、要素がアクセシビリティツリーから削除されるため、非表示状態ではスクリーンリーダーに読み上げられません。コンテンツを存在させたまま非表示にする場合は、opacity: 0;
かつpointer-events: none;
といったCSSで視覚的に隠す方法もありますが、ツールチップ/ポップオーバーの場合は、必要な時だけ存在を知らせたいケースが多いため、display
やvisibility
による制御が適していることが多いです。また、hidden
属性([hidden]
)の利用も有効です。
- CSSの
具体的な実装例(ツールチップ)
シンプルなツールチップの実装例を示します。トリガー要素はボタンとし、フォーカス時とホバー時にツールチップが表示され、Escキーで閉じられるようにします。
HTML
ツールチップの内容を持つ要素に一意のIDを付与し、トリガー要素に aria-describedby
でそのIDを指定します。ツールチップ要素は、初期状態で hidden
属性またはCSSで非表示にしておきます。
<button id="tooltip-trigger" aria-describedby="tooltip-content">
設定
</button>
<span id="tooltip-content" role="tooltip" class="tooltip">
設定画面を開くためのボタンです。
</span>
ポイント:
* ツールチップ要素には role="tooltip"
を付与します。
* 初期状態ではCSSで非表示にしますが、hidden
属性を使うのも良い方法です。
CSS
ツールチップ要素を初期状態で非表示にし、トリガー要素のホバーまたはフォーカス時に表示するようにします。位置調整のCSSは適宜調整してください。
/* ツールチップを初期状態で非表示 */
.tooltip {
visibility: hidden; /* または display: none; */
opacity: 0;
transition: opacity 0.2s ease-in-out; /* フェードイン/アウト */
position: absolute;
/* 位置調整のためのスタイルは省略 */
}
/* トリガー要素のホバーまたはフォーカス時に表示 */
#tooltip-trigger:hover + .tooltip,
#tooltip-trigger:focus + .tooltip,
#tooltip-trigger[aria-describedby]:not([hidden]) + .tooltip /* JavaScriptでhidden属性を制御する場合 */
{
visibility: visible;
opacity: 1;
}
/* JavaScriptでaria-expandedやhidden属性を切り替える場合は、
その属性の状態に応じて表示・非表示を制御するCSSも記述 */
#tooltip-trigger[aria-expanded="true"] + .tooltip,
.tooltip:not([hidden]) { /* hidden属性が付いていない場合に表示 */
visibility: visible;
opacity: 1;
}
/* JavaScriptで display: none; を切り替える場合は、
.tooltip.is-active { display: block; } のようなクラスを使う */
ポイント:
* :hover
と :focus
の両方で表示されるようにします。
* CSSセレクターで隣接セレクター(+
)を使用していますが、HTML構造によっては親要素に対するホバー/フォーカスを利用したり、JavaScriptでクラスを付与したりする方法がより柔軟です。後述のJavaScript制御を推奨します。
JavaScript
キーボード操作(特にEscキー)での非表示や、より複雑な表示制御を行うためにJavaScriptを使用します。
const trigger = document.getElementById('tooltip-trigger');
const tooltip = document.getElementById('tooltip-content');
if (trigger && tooltip) {
// 初期状態ではツールチップを非表示 (HTML/CSSでも設定)
tooltip.setAttribute('hidden', ''); // hidden属性を使用
// フォーカスまたはマウスオーバーで表示
trigger.addEventListener('focus', showTooltip);
trigger.addEventListener('mouseenter', showTooltip);
// フォーカスアウトまたはマウスリーブで非表示
trigger.addEventListener('blur', hideTooltip);
trigger.addEventListener('mouseleave', hideTooltip);
// Escキーで非表示
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
hideTooltip();
}
});
function showTooltip() {
tooltip.removeAttribute('hidden'); // hidden属性を削除して表示
// aria-expanded などの状態を示す属性が必要ならここで設定
// trigger.setAttribute('aria-expanded', 'true'); // ポップオーバーの場合は検討
}
function hideTooltip() {
// ツールチップがフォーカスを受け取ることがない場合はこれで十分
tooltip.setAttribute('hidden', ''); // hidden属性を設定して非表示
// trigger.setAttribute('aria-expanded', 'false'); // ポップオーバーの場合は検討
}
}
ポイント:
* focus
/blur
イベントでキーボード操作に対応します。
* mouseenter
/mouseleave
イベントでマウス操作に対応します。
* hidden
属性を使うと、CSSとJavaScriptで表示状態を一元管理しやすくなります。
* document
全体でEscキーを監視し、表示中のツールチップ/ポップオーバーを閉じるようにします。
具体的な実装例(ポップオーバー)
ポップオーバーはツールチップよりも複雑になることがあります。インタラクティブな要素が含まれる場合や、モーダルのような振る舞いが求められる場合があるためです。ここでは、クリックで表示・非表示が切り替わり、Escキーで閉じられるシンプルな例を示します。
HTML
ポップオーバー要素に一意のIDを付与し、トリガー要素に aria-haspopup
と aria-controls
を指定します。
<button id="popover-trigger" aria-haspopup="dialog" aria-expanded="false" aria-controls="popover-content">
詳細を表示
</button>
<div id="popover-content" role="dialog" aria-labelledby="popover-title" hidden>
<h3 id="popover-title">追加情報のタイトル</h3>
<p>ここに詳細情報が入ります。</p>
<button id="popover-close">閉じる</button>
</div>
ポイント:
* トリガー要素に aria-haspopup="dialog"
を指定し、クリックでダイアログのようなポップアップが表示されることを示します。(表示される内容に応じて menu
, listbox
など適切な値を設定します)
* aria-expanded="false"
で初期状態が閉じていることを示し、開閉時にJavaScriptで true
/false
を切り替えます。
* aria-controls="popover-content"
で、このボタンが popover-content
というIDの要素を制御することを示します。
* ポップオーバー要素に role="dialog"
を指定し、ダイアログであることを明示します。
* ポップオーバーのタイトル要素にIDを付与し、ポップオーバー要素に aria-labelledby
でそのIDを指定することで、スクリーンリーダーがポップオーバーを開いた際にタイトルを読み上げるようにします。
* 初期状態は hidden
属性で非表示にします。
CSS
ツールチップと同様に、hidden
属性がない場合に表示されるCSSを記述します。
.popover {
/* 位置調整など */
position: absolute;
border: 1px solid #ccc;
padding: 1em;
background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.popover[hidden] {
display: none; /* hidden属性があれば非表示 */
}
/* JavaScriptでクラスを切り替える場合は .popover.is-open { ... } のように */
JavaScript
クリックイベントでの表示・非表示切り替え、Escキーでの非表示、そしてフォーカス管理が重要になります。
const trigger = document.getElementById('popover-trigger');
const popover = document.getElementById('popover-content');
const closeButton = document.getElementById('popover-close');
if (trigger && popover && closeButton) {
// クリックで開閉
trigger.addEventListener('click', togglePopover);
closeButton.addEventListener('click', closePopover);
// Escキーで閉じる
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closePopover();
}
});
function togglePopover() {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
openPopover();
}
}
function openPopover() {
// aria属性の更新
trigger.setAttribute('aria-expanded', 'true');
// hidden属性を削除して表示
popover.removeAttribute('hidden');
// フォーカス管理: ポップオーバー内の最初のインタラクティブ要素、
// またはポップオーバー自体にフォーカスを移動
// ここでは閉じるボタンにフォーカスを移動する例
closeButton.focus();
}
function closePopover() {
// aria属性の更新
trigger.setAttribute('aria-expanded', 'false');
// hidden属性を設定して非表示
popover.setAttribute('hidden', '');
// フォーカス管理: トリガー要素にフォーカスを戻す
trigger.focus();
}
// ポップオーバーの外側をクリックで閉じる(オプション)
document.addEventListener('click', (event) => {
const isClickInside = trigger.contains(event.target) || popover.contains(event.target);
if (!isClickInside && popover.hasAttribute('hidden') === false) {
closePopover();
}
});
}
ポイント:
* クリックイベントで aria-expanded
と hidden
属性を切り替えます。
* openPopover
関数内で、表示後にポップオーバー内の要素(ここでは閉じるボタン)にフォーカスを移動させます。これにより、キーボードユーザーが表示された内容にすぐにアクセスできます。
* closePopover
関数内で、非表示後にトリガー要素にフォーカスを戻します。これにより、ユーザーは操作を継続しやすくなります。
* Escキーでの非表示に対応します。
* ポップオーバーの外側をクリックで閉じる機能を追加する場合は、上記のようにイベントリスナーを設定します。ただし、この処理はフォーカス管理やモーダルではないポップアップ(非モーダル)の挙動に影響する場合があるため、慎重に実装してください。
実装時の注意点
- 動的なコンテンツ: ツールチップ/ポップオーバーの内容が非同期で読み込まれる場合、コンテンツが完全に準備できてから表示を切り替える、あるいはローディング状態を示すなどの配慮が必要です。
- 表示位置: ツールチップ/ポップオーバーが表示される位置が、トリガー要素や他の重要なコンテンツを隠してしまわないように注意が必要です。画面端での表示位置調整なども考慮します。
- 連続する要素: 複数の要素にツールチップ/ポップオーバーを適用する場合、IDが重複しないように管理する必要があります。JavaScriptで要素を特定する際に、イベントの発生元(
event.target
)や親要素からの関連付けを利用すると、再利用しやすいコードになります。 - WCAGの達成基準:
- 達成基準 1.4.13 コンテンツの押下時の不透明化(SC 1.4.13 Content on Hover or Focus):ホバーまたはフォーカスによって追加コンテンツが表示される場合、そのコンテンツを非表示にできる、ホバーやフォーカスを外してもコンテンツが維持される、コンテンツが表示されてもポインタが移動できる、といった要件があります。特にツールチップでコンテンツがホバーを外すとすぐに消える場合、この基準を満たせない可能性があります。この基準を満たすためには、例えばマウスカーソルをツールチップ領域に移動させられるようにしたり、ユーザー設定で非表示にならないオプションを提供したりといった工夫が必要になります。
- 達成基準 2.1.1 キーボード(SC 2.1.1 Keyboard):すべての機能がキーボードから操作できること。
- 達成基準 2.1.2 キーボードトラップなし(SC 2.1.2 No Keyboard Trap):キーボードフォーカスが特定のサブセクション内に閉じ込められないこと。ポップオーバーを開いた際に内部にフォーカスを移す場合は、Escキーや閉じるボタンで確実に閉じ、フォーカスを戻せるようにする必要があります。
- 達成基準 4.1.2 名前 (name)、役割 (role)、値 (value) (SC 4.1.2 Name, Role, Value):UIコンポーネントの名前、役割、状態がプログラムによって解釈可能であること。ARIA属性の適切な使用がこれに該当します。
テスト方法
実装したアクセシビリティ対応が正しく機能しているか、以下の方法でテストします。
- キーボード操作:
- Tabキーでトリガー要素にフォーカスを移動させ、ツールチップ/ポップオーバーが表示されるか確認します。
- 表示されたポップオーバー内にTabキーでフォーカスが移動するか、内部のインタラクティブ要素にアクセスできるか確認します。(ツールチップの場合は不要な場合が多い)
- Escキーを押して、ツールチップ/ポップオーバーが閉じるか確認します。
- ポップオーバーが閉じた後、フォーカスがトリガー要素に戻るか確認します。
- スクリーンリーダー:
- NVDA (Windows), VoiceOver (macOS/iOS), TalkBack (Android) などのスクリーンリーダーを起動します。
- Tabキーでトリガー要素に移動し、スクリーンリーダーがトリガー要素の名前や役割に加え、ツールチップ/ポップオーバーの内容を適切に読み上げるか確認します。特に
aria-describedby
やaria-labelledby
が正しく機能しているか確認します。 - ポップオーバーの場合、開いた際に内容が読み上げられ、内部の要素にアクセスできるか確認します。
- 自動評価ツール:
- axe DevTools, Lighthouse, Accessibility Insights for Web などの自動評価ツールを使用して、ARIA属性の誤りやその他の基本的なアクセシビリティの問題がないかスキャンします。ただし、これらのツールだけではすべての問題を検出できるわけではないため、手動でのテストと組み合わせることが重要です。
まとめ
ツールチップやポップオーバーのアクセシブルな実装は、キーボードユーザーやスクリーンリーダーユーザーが情報にアクセスするために不可欠です。本記事で紹介したように、HTMLでの適切なマークアップ、ARIA属性の活用、そしてJavaScriptによるキーボード操作と表示状態の制御を組み合わせることで、多くのユーザーにとって使いやすいコンポーネントを提供できます。
単にマウスホバーやクリックで表示するだけでなく、フォーカス時の表示、Escキーでの非表示、そして適切なARIA属性の付与を忘れずに行い、提供する情報が誰にでも利用可能であることを目指しましょう。テストを繰り返し行い、より多くのユーザーが快適に情報にアクセスできるウェブサイトを構築していくことが重要です。