👀
スクロール時に非表示となるヘッダーのフォーカス可視性の考慮
はじめに
こんにちは、もりみちです。
画面領域を広く確保するために、「下にスクロールすると隠れ、上にスクロールすると現れるヘッダー」はWebサイトでよく見かけるUIパターンです。
しかし、このUIを単純に transform や top の移動だけで実装すると、キーボード操作(Tabキー移動)を行った際にアクセシビリティの問題が発生することがあります。
本記事では、この「隠れるヘッダー」におけるフォーカス管理の問題点と、WCAG 2.2「2.4.7 フォーカスの可視化」に準拠するための実装方法を紹介します。
キーボード操作でフォーカスが見えない😭
よくある実装(スクロール量だけでヘッダーの出し入れを制御する)では、以下のようなケースでユーザーが迷子になります。
- ページを下に読み進める(ヘッダーが隠れる)。
- ナビゲーションに戻ろうとして
Shift + Tabなどのキーボード操作を行う。 - ヘッダー内のリンクにフォーカスが当たるが、ヘッダー自体は画面外にあるため見えない。
実際の挙動を体験する✌️
論より証拠、まずは実際の挙動をご覧ください。以下のデモには、右上に
「WCAG 2.2 対応を有効にする」 というチェックボックスを用意しました。
-
チェックを外した状態
スクロールしてヘッダーを隠した後、Shift + Tabで戻ってみてください。フォーカスが当たっているのにヘッダーが出てこず、どこにいるか分からなくなるはずです。 -
チェックを入れた状態
同じ操作をしてみてください。今度はフォーカスが当たった瞬間にヘッダーが「スッ」と降りてきて、現在地が明確になります。
解決策☺️
この挙動を実現するロジックはシンプルです。 「スクロールによる判定」 に加えて、「ヘッダー内部にフォーカスが当たったら強制的に表示する」 という処理を追加します。
const header = document.querySelector("[data-test-header]");
let lastScrollY = window.scrollY;
let isHidden = false;
function showHeader() {
header.classList.remove("hidden");
isHidden = false;
}
function handleScroll() {
const currentScrollY = window.scrollY;
const scrollDelta = currentScrollY - lastScrollY;
if (scrollDelta > 0 && !isHidden) {
header.classList.add("hidden");
isHidden = true;
} else if (scrollDelta < 0 && isHidden) {
header.classList.remove("hidden");
isHidden = false;
}
lastScrollY = currentScrollY;
}
window.addEventListener("scroll", handleScroll, { passive: true });
header.addEventListener("focusin", showHeader);
解説😙
なぜfocusではなくfocusinなのか?
header.addEventListener("focusin", showHeader);
-
focus
バブリング(親要素への伝播)しません。もしこれを使うなら、ヘッダー内のすべての <a> タグや <button> タグに対して個別にイベントを設定する必要があります。 -
focusin
バブリングします。そのため、ヘッダー全体(親要素)にリスナーを1つ設定しておくだけで、内部のどのリンクにフォーカスが当たっても検知可能です。
これにより、ヘッダー内のメニュー項目が増減してもJS側を修正する必要がなく、メンテナンス性が保たれます。
まとめ💪
インタラクションやアニメーションはWebサイトの魅力を高める重要な要素ですが、それによって基本的なユーザビリティが損なわれてしまっては本末転倒です。
たった数行のコードを追加するだけで、アクセシビリティを確保しつつ、モダンなUIを実現できます。ぜひ、あなたのプロジェクトでも試してみてください!
参考
Discussion