アクセシブルなツールチップ
ホバーは 300ms 待ってから、フォーカスとタップは即座に出る。role=tooltip を aria-describedby で結び、Esc で閉じる。帯に「保存, ボタン, 説明: …」と読まれる形を表示。
プレビュー
操作カーソルを乗せると動きます(スマホはタップ)。
仕様書
- 向く場面
- ボタンやヘルプアイコンの補足説明
- フォーカスで即座に説明を読ませたい場面
- 通り道でちらつかせたくないホバー表示
- 避ける場面
- 中にリンクやボタンを入れる
- Esc で閉じられない
- aria-describedby 無しで role=tooltip だけ付ける
- 構成要素
- トリガー(ボタン)
- ヘルプアイコン
- ツールチップ
- 読み上げ文の帯
- 持っている状態
- 通常
- ホバー
- フォーカス
- キーボード
Tab でフォーカスすると即表示、Esc で閉じる(フォーカスはそのまま)、Tab で離れると消える。タッチはタップで即表示、外側タップで閉じる
- 支援技術
role=tooltip を aria-describedby で結び、フォーカス時に「下書きを保存, ボタン, 説明: …」と続けて読まれる。マウスのホバーは 300ms 遅延、退場 120ms 猶予
- 動き
出現/退場 150ms(opacity+4px の移動)。reduced-motion で停止
デザインの要点
- ツールチップは補足であって操作ではない。中にリンクやボタンを入れない(入れるなら popover にする)
- aria-describedby で結ぶと、要素にフォーカスした時点で説明が続けて読まれる。role=tooltip だけでは結び付かない
- ホバーは 300ms 遅らせて、通り道でちらつかせない。フォーカスは意図が明確なので待たずに出す
- Esc で閉じられること。閉じられないツールチップは、拡大表示で画面の大半を覆ったまま残る
使いどころ
アイコンだけのボタンの意味、入力欄の書式、用語の補足など、短い説明を必要な人にだけ見せたいときに使う。条件は「無くても操作できる」ことで、ツールチップの中身が無いと使えない情報は、ラベルや説明文として常に見える場所へ置く。中にリンクやボタンを入れたくなったら、ツールチップではなくポップオーバーへ切り替える(ツールチップは補足であって操作面ではないので、中の要素へフォーカスを移す導線が無い)。タッチ端末にはホバーが存在しないため、タップで開いて外側タップで閉じる挙動を別に用意できないなら、そもそもツールチップに頼らない設計にする。
仕組み
トリガーと説明文を aria-describedby で結び、説明文側に role="tooltip" を付ける。表示の起点は3系統あり、見本は pointerenter(マウス)・focus(キーボード)・pointerdown(タッチ)を分けて扱っている。マウスのホバーだけ 300ms の遅延を入れているのは、通り道でカーソルが横切るたびに開閉するちらつきを防ぐため。フォーカスは意図が明確なので遅延なしで開く。退場側にも 120ms の猶予を置き、トリガーとツールチップの隙間を通るときに消えないようにしている。タッチでは pointerup の直後に pointerleave が来るので、pointerleave で閉じる処理をマウスに限定していないと読む前に消える。Esc の keydown で閉じるが、フォーカスはトリガーへ残したままにする。
実装の注意
role="tooltip" だけ付けて aria-describedby を書かない実装は、見た目は動いても読み上げには何も結び付かない——role は種類を言うだけで、関係は describedby が作る。次に多いのがホバーだけで開く実装で、キーボード利用者とタッチ利用者の両方が中身へ到達できない。title 属性で代用する手もあるが、表示の遅延も位置も制御できず、タッチでは出ず、読み上げの扱いも環境差が大きいので、説明が重要なら独自実装にする。ツールチップが枠外へはみ出して切られる事故も多く、overflow:hidden の親の中では position:fixed かアンカー配置へ逃がす。閉じる手段が無いツールチップは、拡大表示の利用者にとって画面の大半を覆ったまま残る障害物になる。
アクセシビリティ
WCAG 1.4.13「ホバーまたはフォーカスで表示されるコンテンツ」が直接関係し、(1) ポインタを乗せたまま内容の上へ移動できる (2) Esc などで閉じられる (3) 勝手に消えない、の3条件を求める。見本は退場に 120ms の猶予を置いて (1) を、Esc の keydown で (2) を満たしている。結び付けは aria-describedby で行い、フォーカス時に「下書きを保存, ボタン, 説明: …」と続けて読まれる形にする。aria-label へ説明まで詰め込むと、ボタンの名前そのものが長文になり一覧での見分けがつかなくなるので、名前(label)と説明(description)は分ける。prefers-reduced-motion では出現・退場のトランジションを止め、表示の切り替えだけを行う。
調整して使う
調整するのは遅延・位置・トリガーの3つ。ホバーの遅延 300ms は「通り道では出さない」ための値で、密に並ぶアイコン列では長め(400〜500ms)に、単独のヘルプアイコンでは短くしてよい。退場の猶予はツールチップとトリガーの隙間に比例させる。位置は上が既定だが、画面上端に近い要素では下へ反転させる必要があり、CSS のアンカー配置か JS の衝突判定で切り替える。トリガーをアイコンだけのボタンにするなら、ツールチップとは別に aria-label で名前を必ず与える——説明だけあって名前が無いボタンは、読み上げの一覧で「ボタン」としか出ない。
コード(コピーして使えます)
<div class="tp-wrap">
<div class="tp-row">
<button type="button" class="tp-btn" id="tp-save" aria-describedby="tp-tip1">下書きを保存</button>
<span class="tp-tip" role="tooltip" id="tp-tip1">変更は自動で下書きに残ります。公開はされません</span>
</div>
<div class="tp-row">
<span class="tp-field">API キー <code>sk-••••7f2a</code>
<button type="button" class="tp-info" id="tp-i" aria-label="API キーについて" aria-describedby="tp-tip2">?</button>
</span>
<span class="tp-tip" role="tooltip" id="tp-tip2">ダッシュボードの「設定 → 開発者」で再発行できます</span>
</div>
<div class="tp-band"><span class="tp-tag">読み上げ</span><span id="tp-msg">ホバー(300ms 後)か Tab でフォーカスすると表示。Esc で閉じる</span></div>
</div>