Webデザインのための備忘録

webデザインをするために学んだことをアウトプットするためのブログです。

ハンバーガーメニューの作り方(疑似要素で頑張ろう編)

こんにちは。webデザイン学習中、mamenoです。
今回は、ハンバーガーメニューとその挙動について、私なりに考えて作ってみたのでそのことについて書いていきたいと思います。

今回のゴール

今回作るものはこちらのハンバーガーメニュー。

動きとしては、
1. 三本線(ハンバーガーメニュー)をクリック(タップ)
2. 上からメニューが出てきて、背景が暗くなり、ハンバーガーは×ボタンになる
3. ×ボタン or 暗い背景をクリック(タップ)するとメニューが閉じる
というものになります。

See the Pen Untitled by mameno (@mameno_design) on CodePen.

なんの変哲もないハンバーガーメニューですが、一般的に調べるとよく出てくるものと違う点が1つ。
それは、背景のマスク部分を疑似要素で作っているということ。

疑似要素は、JacaScriptでは直接指定ができないので、マスク部分をクリックした時の処理を書くのにちょっとした工夫が必要でした。
ので、備忘録的に記事にしていきたいと思います。

1. ハンバーガーメニューを作る(HTML編)

まずは、HTMLを記述します。

See the Pen Untitled by mameno (@mameno_design) on CodePen.

今回、ハンバーガメニューを作るにあたって大切にしたことは、2つ。

  • そもそものHTMLに空の要素は作らない
  • スマホバージョンとPCバージョンのメニューは分けない

順に説明していきます。

空の要素を作らない

三本線アイコンについて

ハンバーガーメニューのアイコンというと、HTML上にspanタグを3つ並べて作るパターンをよく見かけます。
が、できればhtml上には不要な要素は置いておきたくない。
ということで、今回はbuttonタグの中にspanタグを一つ入れそこに一本線を作り、 :before:afterで残り2本を作るという方法にしました。

<button type="button" class="c-button p-hamburger js-hamburger" aria-controls="global-nav" aria-expanded="false">
  <span class="p-hamburger__line">
          <span class="u-screenReaderText">メニューを開閉する</span> //ここはスクリーンリーダー用で画面上には表示されない
  </span>
</button>

メニュー展開時の背景のマスクについて

ハンバーガーメニューが開いた時の背景のマスクについても、<span class="mask"></span>みたいな空要素を入れることはせずbodyの疑似要素で対応することにしています。

音声読み上げ用クラスについて

ちなみに、.u-screenReaderTextクラスがついているspanは、
音声読み上げソフトを使用した際に「ここを押すとナビゲーションメニューが開閉できますよ〜」と教えるためのもので、
画面上には必要ないので、後述するcssで見えなくしておきます。

スマホバージョンとPCバージョンのメニューは分けない

メニューを分けると、HTML上にスマホバージョンと PCバージョンのメニューが2つ存在することになるわけです。
見た目上は出し分けているので問題ないかもしれませんが、こちらも音声読みあげソフトを使用した時には、 2回メニューが読まれるわけなので鬱陶しいかなということで、HTMLは最小限の記述でcssでなんとかしていくという方法にしました。
(今回は非常にシンプルなメニューだったということもあり)

2. ハンバーガーメニューを作る(CSS編)

次はCSSです。

  1. .u-screenReaderText用に隠す記述
  2. ナビゲーションのスタイル
  3. ハンバーガーボタンのスタイル

を順番に記述します。 (767px以下でハンバーガーメニューに変わります。まだ動きません。)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

ハンバーガーメニューのcssについては、いろいろな方が書かれているのでここでは割愛します。

3.ハンバーガーメニューの挙動を作る(JS編-1)

  1. CSSに、.is-active-drawerがついた時の要素について追記(CodePen CSSの下にまとめてあります)
  2. JSにて、ハンバーガーボタンに.is-active-drawerをつける(外す)処理と、aria-expandedのtrue/falseを入れ替える処理の記述(この時点ではハンバーガーボタンのみでメニューの開閉)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

aria-expandedって?

要素、またはそれが制御する別のグループ化要素が現在展開されているか折りたたまれているかを示します。
W3Cリファレンス より

なくても見た目には影響しませんが、アクセシビリティ的な観点から記述した方がいいものです。多分。
クラスの付け替えだけだと、aria-expandedがfalseなのに見た目では展開している感じになってしまうのできちんと書き換える処理を加えています。

4.背景をクリックした時でもメニューが閉じるようにする(JS編-2)

ここがとっても難しかったです。
なぜかというと、前述した通り擬似要素はJavaScriptで直接指定できないから。

なので、擬似要素を指定するのではなく、ナビゲーション以外のbody部分を指定するという方法で背景を選択します。

     //背景部分をクリックしたときに閉じる処理
       body.addEventListener("click", (e) => {

      //.js-globalNav(ナビゲーション)部分だけを除外する処置
      const hasClass = e.path.filter((el) => {
        //クラスがついている要素かつクラスに「js-globalNav」がついている要素を変数「hasClass」に配列で取得
        return el.classList && el.classList.contains("js-globalNav");
      });

      if (hasClass.length) {
        //hasClass.lengthの値が1以上(true)だったら何もしない
        return;
      }

      //hasClass.lengthの値が0(false)だったら、js-globalNavは含まれていないので以下の処理を実行する
      // bodyのクラスとarea-expandedの処理
      body.classList.remove("is-active-drawer");
      hamburgerButton.setAttribute("aria-expanded", false);
    });

細かく解説してみる。

  1. bodyにクリックイベントを定義
  2. ナビゲーション部分を除外する処理をする

    • イベントリスナー(e)の中にある、pathプロパティにアクセスすると、その要素が何に包まれているのかを全て取得してくることができるので、それを利用します。
      試しにconsole.loge.pathの値を出してみました。

アセット 1-80.jpg

↑配列の中に「.js-globalNav」が含まれています。

アセット 2-80.jpg

↑配列の中に「.js-globalNav」が含まれていません。

  • e.pathで取得できる上記の配列に、filter()メソッドを使って当てはまる要素だけを抽出し、hasClassで受け取ります。
  • hasClassに値が入っている = .js-globalNav が含まれている場合は、メニューを閉じたくないので何もせずreturn

  • ナビゲーション部分の除外ができたら、あとはbodyをクリックしたらクラスを外す&aria-expandedをfalseにする記述をするだけ

で、以下のように動きます。 (背景部分をクリックしてみてください)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

おまけ(header要素もクリックから除外したいとき)

今回のこのデモだとheaderがそもそもないのですが、headerもクリックの対象から除外したい場合、普通にありますよね。
この場合は、2パターン解決策を考えました。

<パターン1>

HTMLを記述する際に、headerの中にナビゲーションを入れちゃう

そうすれば、el.classList.contains("js-globalNav")としていたところをel.classList.contains("js-header")みたいにしたらheader部分も除外対象に含まれます。

<パターン2>

以下のコードを除外する処理の前に記述する

    [headerを取得するための変数].addEventListener("click", (e) => {
        e.stopPropagation();
     });

stopPropagationを使えば、イベントが止まります。

まとめ

いかがでしたでしょうか。
4つ目の要素の除外については私だけでは思いつかず、入会しているコミュニティの先生にご教授いただき、ここまで作ることができました。
空のタグを使えば簡単な処理ですが、きれいなマークアップを考えるとこういう書き方もできたらいいかなと思い、チャレンジしてみました。
他にもJavaScriptで空の要素(mask用)を生成する方法も考えたので、後日ブログにまとめたいと思います。

ご覧いただき、ありがとうございました。