👀

スクロール時に非表示となるヘッダーのフォーカス可視性の考慮

に公開

はじめに

こんにちは、もりみちです。
画面領域を広く確保するために、「下にスクロールすると隠れ、上にスクロールすると現れるヘッダー」はWebサイトでよく見かけるUIパターンです。
しかし、このUIを単純に transform や top の移動だけで実装すると、キーボード操作(Tabキー移動)を行った際にアクセシビリティの問題が発生することがあります。
本記事では、この「隠れるヘッダー」におけるフォーカス管理の問題点と、WCAG 2.2「2.4.7 フォーカスの可視化」に準拠するための実装方法を紹介します。

キーボード操作でフォーカスが見えない😭

よくある実装(スクロール量だけでヘッダーの出し入れを制御する)では、以下のようなケースでユーザーが迷子になります。

  1. ページを下に読み進める(ヘッダーが隠れる)。
  2. ナビゲーションに戻ろうとしてShift + Tab などのキーボード操作を行う。
  3. ヘッダー内のリンクにフォーカスが当たるが、ヘッダー自体は画面外にあるため見えない。

実際の挙動を体験する✌️

論より証拠、まずは実際の挙動をご覧ください。以下のデモには、右上に
「WCAG 2.2 対応を有効にする」 というチェックボックスを用意しました。

  1. チェックを外した状態
    スクロールしてヘッダーを隠した後、Shift + Tab で戻ってみてください。フォーカスが当たっているのにヘッダーが出てこず、どこにいるか分からなくなるはずです。
  2. チェックを入れた状態
    同じ操作をしてみてください。今度はフォーカスが当たった瞬間にヘッダーが「スッ」と降りてきて、現在地が明確になります。

解決策☺️

この挙動を実現するロジックはシンプルです。 「スクロールによる判定」 に加えて、「ヘッダー内部にフォーカスが当たったら強制的に表示する」 という処理を追加します。

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を実現できます。ぜひ、あなたのプロジェクトでも試してみてください!

参考

https://waic.jp/translations/WCAG22/Understanding/focus-visible

Discussion