Vanilla JSでアクセシビリティを考慮したハンバーガーメニューをつくる
はじめに
「Vanilla JS ハンバーガーメニュー アクセシビリティ」で Google 検索したとき、なかなか良い記事に辿り着けなかったので備忘録としてソースコードを残しておく。(※ライブラリ等は使用しません。)
最終的なソースコードはこちら。
実現したいこと
-
ハンバーガーメニューをクリックするとナビゲーションが開き、ハンバーガーメニューの「≡」と「×」が切り替わる
-
ナビゲーションメニューが開いている時、裏側のコンテンツはスクロールさせない
-
esc キーでナビゲーションメニューが閉じる
※共通事項として JavaScript で制御する要素にはjs-***
というクラスを指定する。
ハンバーガーメニュー(開閉ボタン)
ハンバーガーメニューは<button>
タグでマークアップする。
JavaScript での制御のために、class にはjs-button
をつける。
ハンバーガーメニューの「≡」(3 本横線)と「×」(バツ)は、<span>
タグと疑似要素のbefore
、after
で描画する。
指定する属性は次のとおり。
type
属性
type
属性は、button
を指定。button
の他に、フォームのデータをサーバーへ送信するsubmit
、コントロールを初期値にするreset
属性が存在する。
button
属性は、規定の動作がなく、押されても何も行わない。イベントが発生すると起動されるクライアント側スクリプトを設定することができる。
aria-controls
属性
ある要素が別の要素の内容や表示を制御する場合に使用し、制御する要素の id を指定。
<button>
は<nav>
の開閉状態を制御するので、<button>
のaria-controls
属性には<nav>
の id であるnav
を指定する。
aria-expanded
属性
その要素が展開可能であるか、または展開されている状態をスクリーンリーダーに伝える。aria-expanded
の値がfalse
の場合は「折りたたみ」、true
の場合は「展開」とスクリーンリーダーが読み上げ、この属性を使用する際は、展開の有無によって JavaScript で値を書き換える必要がある。
初期状態はメニューが閉じているのでfalse
を指定しておいて、JavaScript でメニューの開閉状態にあわせてtrue
と切り替える。
aria-label
属性
現在の要素にラベル付けする文字列を定義するために使用する。この属性はテキストラベルが画面に表示されない場合に使用する。
この属性で指定したラベルは画面上には表示されませんが、スクリーンリーダーなどに対してのみラベルを設定しておきたい場合に利用できる。
aria-label
属性にはメニューを開く
を指定してあげて、こちらも JavaScript でメニューの開閉状態にあわせて、メニューを閉じる
と値を切り替える。
ソースコード
最終的な<button>
のマークアップは次のようになります。
CSS については詳しく解説しませんが、<button>
をクリックしたときにis-active
クラスを<button>
に付与して、「≡」と「×」を切り替える。
<button
class="button js-button"
type="button"
aria-controls="nav"
aria-expanded=false
aria-label="メニューを開く"
>
<span class="button-bar"></span>
</button>
.button {
position: relative;
z-index: 100;
right: 16px;
height: 24px;
width: 36px;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
}
.button-bar {
display: block;
content: "";
width: 100%;
height: 1px;
background: #333333;
}
.button-bar::before,
.button-bar::after {
display: block;
position: absolute;
content: "";
width: 100%;
height: 1px;
background: #333333;
transition: 0.3s ease;
}
.button-bar::before {
top: 0;
}
.button-bar::after {
bottom: 0;
}
.button.is-active .button-bar {
height: 0;
}
/* ボタンをクリックしたときにバツに変える */
.button.is-active .button-bar::before {
opacity: 1;
top: 50%;
transform: rotate(45deg) translateY(-50%);
transition: 0.3s ease;
}
.button.is-active .button-bar::after {
opacity: 1;
top: 50%;
transform: rotate(-45deg) translateY(-50%);
transition: 0.3s ease;
}
ナビゲーションメニュー
nav
の中に<ul>
と<li>
をいれてマークアップする。
id
id
には<nav>
開閉のトリガーとなる<button>
タグに指定した、aria-controls
属性とあわせるようにする。
今回はnav
を指定。
aria-hidden
属性
その要素と子孫要素が、ユーザーに表示されない、または知覚されないということを示すために使用される。たとえば、何かのアクションをしたときにはじめて表示されるような要素は、aria-hidden
を true に設定して、その要素が表示されたときに aria-hidden
を false にする。
<nav>
の初期状態は非表示なので、<ul>
要素にaria-hidden="true"
を指定し、JavaScript でfalse
と切り替えます。
ソースコード
最終的な<nav>
のマークアップは次のとおり。
<nav id="nav" class="menu js-menu">
<div class="overlay js-overlay"></div>
<ul class="list js-list" aria-hidden="true">
<li class="item"><a href="/">menu</a></li>
<li class="item"><a href="/">menu</a></li>
<li class="item"><a href="/">menu</a></li>
<li class="item"><a href="/">menu</a></li>
</ul>
</nav>
.menu {
opacity: 0;
visibility: hidden;
position: fixed;
inset: 0;
transition: 0.3s ease;
}
.menu.is-active {
opacity: 1;
visibility: visible;
width: 100%;
height: 100vh;
transition: 0.5s ease;
}
.overlay {
position: fixed;
inset: 0;
z-index: 1;
width: 100vw;
height: 100vh;
background: rgba(124, 124, 124, 0.233);
}
.list {
display: flex;
z-index: 100;
align-items: center;
justify-content: center;
height: 100vh;
height: 100dvh;
gap: 24px;
font-size: 20px;
flex-direction: column;
list-style-type: none;
}
JavaScript
JavaScript は次のとおり。
簡単に説明すると、メニューのオープン/クローズの状態を定義し、その状態に基づいて属性やクラスを切り替えている。
また、<body>
にoverflow: hidden
を付与してメニューが開いているときはスクロールができないように、esc
キーでメニューが閉じるようにしている。
document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector(".js-button");
const menu = document.querySelector(".js-menu");
const overlay = document.querySelector(".js-overlay");
const list = document.querySelector(".js-list");
let isMenuOpen = false; // メニューの状態を表す変数
const toggleMenu = () => {
isMenuOpen = !isMenuOpen; // メニューの状態を反転
// クラスのトグル操作
button.classList.toggle("is-active", isMenuOpen);
menu.classList.toggle("is-active", isMenuOpen);
overlay.classList.toggle("is-active", isMenuOpen);
list.classList.toggle("is-active", isMenuOpen);
// 属性操作
button.setAttribute("aria-expanded", isMenuOpen.toString());
button.setAttribute("aria-label", isMenuOpen ? "メニューを閉じる" : "メニューを開く");
list.setAttribute("aria-hidden", (!isMenuOpen).toString());
document.body.style.overflow = isMenuOpen ? "hidden" : "";
};
const handleKeydown = (e) => {
if (e.key === "Escape" && isMenuOpen) {
toggleMenu();
}
};
if (button) {
button.addEventListener("click", toggleMenu);
}
document.addEventListener("keydown", handleKeydown);
});
Discussion