今やあらゆるサイトで使用されているハンバーガーメニュー。
私たちウェブサイト系のエンジニアは数え切れないくらい実装してきている定番パーツです。
しかしながらその実装は容易ではありません。自分の作り方に自信を持てていない方も少なくないのではないでしょうか。
ただ単にそれっぽく動くものを作るのであれば簡単ですが、アクセシビリティやメニュー展開時の他の箇所の挙動など、不具合がないように徹底的に作り込もうとするとなかなか難しいものです。
今回はそんな厄介な存在であるハンバーガーメニューについて、私なりの現在の作り方を紹介したいと思います。。
ハンバーガーメニューの作り方を見る前に、ハンバーガーメニューのあるべき姿、実装時に気を付けるべきことを洗い出してみましょう。
私が思い浮かぶものだと以下のようなものがあります。
今回はこれらの点に留意した実装を紹介しようと思います。
まずは完成形をご覧ください。
See the Pen humberger by raikikannobaigie (@raikikannobaigie) on CodePen.
順番にポイントを解説していきます。
まず基本的なことですが、フォーカスが当たりキーボードで操作可能であり、かつスクリーンリーダーにメニューを開閉するボタンであることが伝わるように、メニューボタンはbutton要素を使用して実装します。
加えて、メニューの開閉状態を伝える属性「aria-expanded」と、このボタンが制御しているメニューはどれなのかということを示す「aria-controls」属性を付与します。
メニューの展開は、クラスの有無でCSSで制御するようにします。
ボタンが押下されたらJSでメニューボタンとメニューにクラスを付与し、メニューが展開するようにします。
同時に、JSでメニューボタンの「aria-expanded」属性の属性値をtrueに書き換え、メニューが展開したことをスクリーンリーダーに伝えます。
メニューが展開した際、背景(メニューの裏に隠れている本文、メインコンテンツエリア)がスクロールできてしまう実装をよく見かけます。
これではメニューの操作性も悪くなりますし、メニューを閉じたらコンテンツ内の全然違う箇所に移動していたりと、全体的に使い勝手が悪くなります。
それらを解決するために、今回の例では「backgroundFix」と定義してある関数を用いて、メニュー展開時に背景を固定しています。
htmlタグやbodyタグにoverflow: hidden;を指定してスクロールを防ぐやり方もありますが、そのやり方だとiOSに対応できません。
今回の実装の仕組みとしては、まずコンテンツのスクロール位置を取得し、それをもとにposition: fixed; height: 100vh;で背景を固定します。メニューを閉じた際はこれを解除し、先に取得したスクロール位置を使用してもとのスクロール位置に戻してやるといった仕組みです。
これについてはCSSでvisiblity: hidden;を指定してやればフォーカスが当たりません。
メニュー展開時にJSで値をvisibleに変えます。
上記ポイント3と同じです。
メニューボタンと同様にbutton要素を使用し、area-expandedなどのWAI-ARIA属性を付与します。
キーボード操作ではエスケープキーで項目を閉じられるような仕様が多くみられ、ユーザーにとってもこれは直感的な操作として広く浸透していると考えられます。
それを踏まえると、ハンバーガーメニューにおいてもエスケープキーでメニューを閉じられる仕様にしておくことは、使い勝手向上のためにも重要です。
今回はevent.keyで押下されたキーを取得し、それがエスケープキーだったらメニューが閉じられるように実装しています。
またその際に、メニューボタンにfocus()を指定することで、メニューを閉じた際にメニューボタンにフォーカスが当たり、すぐさまメニューボタンの操作を行えるようにしています。
メニューの最後の項目までキーボード操作でフォーカスを移動させると、その次は背景である本文エリアにフォーカスが移ってしまう実装をよく見かけます。
これではユーザーは自分が今どこを操作しているのか分かりませんし、メニュー項目の操作も一苦労です。
なのでメニューの最後の項目までフォーカスが移動したら、メニューボタンかメニューの最初の項目にフォーカスを移動させてやる必要があります。
様々な実装方法がありますが、今回はフォーカストラップという手法を利用しました。
以下の要素がフォーカストラップと呼ばれるものになります。
<div id="js-focus-trap" tabindex="0"></div>
この要素にフォーカスが当たった際に、メニューボタンか先頭項目にフォーカスを当てるようにJSで制御しています。
今回ご紹介した実装方法はあくまで私個人の考えに基づくものですので、このやり方の方が良いのではないか、などある方はぜひともご意見いただきたいです。