アクセシビリティを考慮したハンバーガーメニューのモダンな作り方

アクセシビリティを考慮したハンバーガーメニューのモダンな作り方

単純に見た目だけでハンバーガーメニューを作ることは簡単ですが、実際にコンピューターや視覚障害者に開閉状態を伝えるためにはアクセシビリティを考慮したコーディングが必要になります。

今回は以下の箇所に重点を置き、出来るだけシンプルな構造でハンバーガーメニューを作成しました。

  • メニューボタンはbutton要素で作成
  • 必要に応じてWAI-ARIA属性を付与
  • カスタマイズしやすい柔軟性がある設計
目次

実装での前提条件

  • アイコンをアニメーションさせる
  • キーボード操作が可能
  • 閉じたメニュー項目にフォーカスが当たらないようにする
  • 開閉状態をスクリーンリーダーで読み上げられるようにする
  • メニューを開いたとき背景がスクロールしないようする
  • 背景をクリックしたときはメニューを閉じる
  • 開閉時にボタンラベルを切り替えられる

ハンバーガーメニューボタン

See the Pen humberger by hisa (@hisaaashi) on CodePen.

button要素を採用する理由としては、デフォルトでキーボード操作が可能なことと、スクリーンリーダーやブラウザに「ボタン」であることが明示できる点です。

ボタンの構造

<button type="button" id="menu-button" aria-expanded="false" aria-controls="menu" aria-label="メニューボタン">
  <span class="bar"></span>
  <span class="menu-label" id="menu-label" aria-hidden="true">MENU</span>
</button>

ボタンの構造は単純で、上からアイコン、ラベル(MENU)です。

3本線のアイコンはspan要素と擬似要素の::before::afterで作成しました。

ラベルが不要であれば、HTMLをコメントアウトするか、CSSで非表示にするだけでOKです。

type属性について

type属性は、フォーム外では無効になるため必須ではありません。しかし、buttonを明示的に指定することで意図が明確になり、予期せぬ動作を防ぐことができます。デフォルトではsubmitが設定されています。

ボタンにWAI-ARIAの付与

button要素にはアクセシビリティを高めるため、WAI-ARIAの属性を付与しています。

aria-expanded

要素が展開されているかどうかを示す属性。展開していればtrue、展開していない場合はfalseを指定。

aria-controls

制御対象となる要素を指定する属性。対象の要素のIDを指定して関係性を紐付けます。

aria-label

要素にラベル付けする属性。ラベル内のテキストが音声ソフトなどのスクリーンリーダーで読み上げられます。

aria-labelの補足

aria-label 属性が指定されている要素内にテキストがあると、重複して読み上げられる可能性があります。テキストの読み上げを回避するにはaria-hidden="true" 属性を追加しておくのがベターです。

<!-- ❌ 装飾だけ → 読まれない -->
<button><span class="bar"></span></button>

<!-- ⚠️ テキストとaria-label両方 → 重複の可能性あり -->
<button aria-label="メニューボタン">MENU</button>

<!-- ⭕️ aria-hiddenでテキストを無視 → aria-labelのみ読み上げ -->
<button aria-label="メニューボタン"><span aria-hidden="true">MENU</span></button>

WAI-ARIAは、本来は不足している情報を補完するために使用されるべきものであり、最低限の使用が推奨されています。

無闇に属性を追加してしまうと、予期しない構造になり、悪影響を及ぼす可能性があるため、セマンティックなHTML要素を利用することが最善です。

ボタンのCSSのポイント

/* ハンバーガーメニュー */
#menu-button {
  position: fixed;
  right: 8px;
  display: grid;
  place-items: center;
  place-content: center;
  width: 60px;
  height: 60px;
  background: #ddd;
  border: none;
  cursor: pointer;
  z-index: 999;
}

ボタン要素にはflexよりも柔軟性が高いgridを採用。

ラベルの有無を考慮し、子要素が複数の場合でもボタン中央に配置されるようplace-itemsplace-contentを併用しています。

この2つを指定するだけで完全中央配置できるのは優秀です。

/* バー */
.bar,
.bar::before,
.bar::after {
  width: 25px;
  height: 3px;
  background-color: #333;
  transition: transform 0.3s;
}

.bar {
  display: grid;

  &::before,
  &::after {
    content: "";
    grid-area: 1 / 1;
  }

  &::before {
    transform: translateY(-8px);
  }

  &::after {
    transform: translateY(8px);
  }
}

/* オープン時のバー */
[data-drawer-open="true"] {

  .bar {
    background-color: transparent;

    &::before {
      transform: rotate(45deg);
    }

    &::after {
      transform: rotate(-45deg);
    }
  }
}

アイコンのバーにもgridを採用し、擬似要素にはgrid-area:1/1で全ての要素が中心に重なるように配置しています。(3本が1本の線に見える状態)

これにより、アニメーションで斜めにした際にも上下の位置調節が不要になり、しっかりと中心で綺麗なXアイコンができるようになります。

メニューの表示

<nav id="menu" inert>
  <ul>
    <!-- 略 -->
  </ul>
</nav>

メニューで重要な点は、非表示の時にメニュー項目にフォーカスが当たらないことです。

通常、この機能を実現するためにはCSSでpointer-events: nonetabindex="-1"などを指定する必要がありますが、コンテンツ全体を一括で非活性にするinert属性を採用しました。

inert属性は、子孫要素も含めて操作を無効化できるため、一時的にコンテンツを非活性にしたい場面で便利です。

アクティブな時にはJavaScriptで取り除きます。

CSSのポイント

/* メニュー */
#menu {
  position: fixed;
  height: 100%;
  width: 300px;
  background-color: #fff;
  inset: 0;
  z-index: 998;
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform 0.3s ease-out;
}

/* オープン時のメニュー */
[data-drawer-open="true"] #menu {
  transform: translateX(0);
}

CSSではtransformで100%画面外に隠し、オープン時に0に戻すことでスライドアニメーションさせています。

重要なポイントとしては、メニュー項目が多くてもメニュー内をスクロールできるようoverflow-y:autoを指定することです。

背景のオーバーレイ

<div id="overlay"></div>
/* オーバーレイ */
#overlay {
  visibility: hidden;
  opacity: 0;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  z-index: 997;
  transition: visibility .3s, opacity .3s;

  /* オープン時のオーバーレイ */
  [data-drawer-open="true"] & {
    visibility: visible;
    opacity: 1;
  }
}

画面全体を覆うように、幅と高さを100%にしposition:fixedで固定しています。

display:noneで制御すると、フェードインのようなふわっとしたアニメーションが出来なくなるので注意。

メニュー外の背景をクリックしたときにも閉じる動作は、次のJavaScriptで解説しています。

JavaScriptでの開閉の仕組み

const html = document.documentElement;
const menuButton = document.getElementById("menu-button");
const menuLabel = document.getElementById("menu-label");
const menu = document.getElementById("menu");
const overlay = document.getElementById("overlay");

// メニューの開閉を切り替える
function toggleMenu() {
  // メニューが開いているかどうかを判定
  const isOpen = html.getAttribute("data-drawer-open") === "true";

  // 開いていたら閉じる処理
  if (isOpen) {
    html.setAttribute("data-drawer-open", "false");
    menuButton.setAttribute("aria-expanded", "false");
    menuButton.setAttribute("aria-label", "メニューボタン");
    menu.inert = true;
    if (menuLabel) menuLabel.textContent = "MENU";
  } else {
  // 閉じていたら開く処理
    html.setAttribute("data-drawer-open", "true");
    menuButton.setAttribute("aria-expanded", "true");
    menuButton.setAttribute("aria-label", "メニューを閉じる");
    menu.inert = false;
    if (menuLabel) menuLabel.textContent = "CLOSE";
  }
}

// メニューをクリックしたらトグル
if (menuButton) {
  menuButton.addEventListener("click", toggleMenu);
}

// 背景をクリックしたらトグル
if (overlay) {
  overlay.addEventListener("click", toggleMenu);
}

メニューの開閉状態は、data-drawer-openというカスタムデータ属性をhtml要素に付与して管理しています。

  • data-drawer-open="false":メニューが閉じた通常状態
  • data-drawer-open="true":メニューが開いた状態

クリックイベントでこの属性値を切り替え、その状態に合わせてボタンのラベルや属性などをまとめて更新するという具合です。

WAI-ARIAの属性の変更を書き忘れると、スクリーンリーダーなどではメニューが開いているのに閉じていると誤認される可能性があるため、必ず状態に合わせて更新する必要があります。

カスタムデータ属性で状態を管理する利点

is-openなどのクラスで開閉を切り替える方法もありますが、サイトの状態は全体に影響を与える要素(htmlなど)にカスタムデータ属性を付与して、まとめて管理すると便利です。

これにより、今どんな状態かを一目で確認でき、コードの見通しや実装・保守がしやすくなります。

詳しい解説は以下の記事にまとめました。

駆け出しの頃は、各要素にオープンクラスを与えていたのですが、今考えると冗長でしかないですね…。

// ドロワーメニュー(非効率な記述方法)
jQuery('.drawer-icon').on('click',function(e) {
	e.preventDefault();

	jQuery('.drawer-icon').toggleClass('is-active');
	jQuery('.drawer-content').toggleClass('is-active');
	jQuery('.drawer-background').toggleClass('is-active');

	return false;
});

メニュー外の背景をクリックで閉じる

メニュー外の背景(つまりオーバーレイ)をクリックしても閉じられるよう、開閉処理はtoggleMenu()にまとめました。

この関数を実行すると、ボタンを押したときと同様の動作が実行できます。

要素がない場合のエラー対策

各要素がない場合のエラーを回避するため、if文を使用し要素が存在する場合にだけ処理するようにしています。

これによりボタンラベルやオーバーレイを非表示にした場合や、PCなどでハンバーガーメニューがない場合でも問題なく動作させることができます。

メニュー展開時に背景をスクロールさせない

/* body */
body {
  overflow-x: clip;

  /* オープン時のbody */
  [data-drawer-open="true"] & {
    overflow: clip;
  }
}

body要素に対するoverflow:hiddenは、position:stickyが使えなかったり、ブラウザやiPhoneなどの端末によってはhtmlにつけないといけなかったり色々問題があるので、overflow:clipを採用することにしました。

iPhoneやMacで確認したところ、Safari、Chrome共に問題なくスクロール制御できました。

課題点:PCでメニューを開くと背景がガタつく

PCでメニューボタンを開いたとき、背景が固定されてスクロールバーが急に無くなってしまうので、スクロールバーの幅だけガタつきます。

対策としては、

  • PCでは背景をスクロールさせる
  • PCではハンバーガーメニューを採用しない
  • 常にスクロールバーをつける

などですかね。

様々なサイトをのぞいてみましたが、背景をスクロールさせている例が多かったです。

確かにPCだと画面が広いし、背景をスクロールさせても特に問題ない気もしますね。

まとめ:ハンバーガーメニューはあまり好きでなはい

元も子もないようなことを言いますが、ハンバーガーメニューって分かりにくいし、なかなか押されないし、めちゃくちゃこだわる必要ってあるのかなって思います。笑

ただ、Web制作している身としては採用率高いし、実装するからには良いものを作成したい!という思いがあります。

今回のハンバーガーメニューはベースとしても使いやすいので、ぜひ参考にしてみてください。

カスタマイズに困ったらお気軽にご相談を!

  • 「ちょっとしたCSSの調整だけお願いしたい」
  • 「不具合を直してほしい」

料金は3,000円〜、お支払いは銀行振込・Amazonギフトカードなど柔軟に対応してます🤔

役に立ったら他の方にシェア

お気軽にコメントどうぞ

コメントする

目次