😽

Vanilla JSでアクセシビリティを考慮したハンバーガーメニューをつくる

2023/09/30に公開

経緯

「Vanilla JS ハンバーガーメニュー アクセシビリティ」で Google 検索したとき、なかなか良い記事に辿り着けなかったので備忘録としてソースコードを残しておきます。(※ライブラリ等は使用しません。)

最終的なソースコードはこちら。
https://codesandbox.io/embed/eager-elion-6ffwzs?fontsize=14&hidenavigation=1&theme=dark&view=editor

実現したいこと

  • ハンバーガーメニューをクリックするとナビゲーションが開き、ハンバーガーメニューの「≡」と「×」が切り替わる

  • ナビゲーションメニューが開いている時、裏側のコンテンツはスクロールさせない

  • esc キーでナビゲーションメニューが閉じる

※共通事項として JavaScript で制御する要素にはjs-***というクラスを指定する。

ハンバーガーメニュー(開閉ボタン)

ハンバーガーメニューは<button>タグでマークアップする。
JavaScript での制御のために、class にはjs-buttonをつける。
ハンバーガーメニューの「≡」(3 本横線)と「×」(バツ)は、<span>タグと疑似要素のbeforeafterで描画する。

指定する属性は次のとおり。

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>に付与して、「≡」と「×」を切り替える。

index.html
      <button
        class="button js-button"
        type="button"
        aria-controls="nav"
        aria-expanded=false
        aria-label="メニューを開く"
      >
        <span class="button-bar"></span>
      </button>
style.css
.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>のマークアップは次のとおり。

index.html
      <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>
style.css
.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キーでメニューが閉じるようにしている。

main.js
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; // メニューの状態を反転

  // メニューがオープンの場合
  if (isMenuOpen) {
    button.classList.add("is-active");
    menu.classList.add("is-active");
    overlay.classList.add("is-active");
    list.classList.add("is-active");
    button.setAttribute("aria-expanded", "true");
    button.setAttribute("aria-label", "メニューを閉じる");
    list.setAttribute("aria-hidden", "false");
    document.body.style.overflow = "hidden";
  }
  // メニューがクローズの場合
  else {
    button.classList.remove("is-active");
    menu.classList.remove("is-active");
    overlay.classList.remove("is-active");
    list.classList.remove("is-active");
    button.setAttribute("aria-expanded", "false");
    button.setAttribute("aria-label", "メニューを開く");
    list.setAttribute("aria-hidden", "true");
    document.body.style.overflow = "";
  }
};

button.addEventListener("click", toggleMenu);

// エスケープキーでメニューが閉じるようにする
document.addEventListener("keydown", function (e) {
  if (e.key === "Escape" && isMenuOpen) {
    toggleMenu();
  }
});

デモサイト

GitHubで編集を提案

Discussion