今更感がありますが、「ハンバーガーメニュー」と呼ばれるメニューがあります。三本線の見た目がハンバーガーのように見えることから名付けられたメニューで、ここをクリック(タップ)すると隠れていたメニューアイテムが表示されて選択できるようになる、というものです。スマートフォンなどの、メニューを常時表示するには小さい画面でメニューを実現するためのメジャーな方法です。

このハンバーガーメニューを(ウェブアプリの)CSS だけで実現する方法がいくつか紹介されています。まあコピペすれば動くし、自分も以前はそうしていたのですが、ちゃんと理屈を理解してはいませんでした。そこで今回改めて自分で理解しながら作って公開してみました。まずは一度動かしてみて、ハンバーガーメニューの挙動を確認してみてください:
https://dotnsf.github.io/css_hamburgermenu/

(ハンバーガーメニュー)
2021022001

(タップすると左からメニュー本体が表示され、自分は×印に。×をタップで元に戻る)
2021022002


ハンバーガーメニューに限りませんが、このような動きのある処理を実現するには、一般的には JavaScript を使います。またメニューの見た目である三本線や、メニューが開いた時の×印の表現は一般的には画像を使って表現することが考えられます。上図のページではどちらも使わず、純粋な HTML と CSS だけで実現しています。

同じようなコード(HTML & CSS)のサンプルは数多く存在していて、コピー&ペーストすれば動くようにはなっています。ただコードの中身の解説がされていない場合が多く、「なんでこのコードでハンバーガーメニューになるのか」の理解が難しいのでした。

というわけで、どうやって画像も JavaScript を使わずにハンバーガーメニューを実現しているかを、上図の例を使って解説します。


まず、このページの HTML と CSS は以下のようになっています(よかったらコピペするかダウンロードして、手元の HTML ファイルに保存して動かしてみてください):
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="CSS Hamburger Menu"/>

<style>
/* ハンバーガーメニューのサイズ(32x32) */
.hamburger-menu{
  vertical-align: center;
  width: 32px;
  height: 32px;
}
/* ハンバーガーメニューの位置と色 */
.menu-btn{
  top: 20px;
  left: 10px;
  display: flex;
  height: 32px;
  width: 32px;
  justify-content: center;
  align-items: center;
  z-index: 90;
  background-color: #005B00;
}
/* メニュー線(本体と before と after で3本表示する) */
.menu-btn span,
.menu-btn span:before,
.menu-btn span:after{
  /* 20x3 の白線 */
  content: '';
  display: block;
  height: 3px;
  width: 20px;
  border-radius: 3px;
  background-color: #ffffff;
  position: absolute;
}
/* before を少し上にずらして描画 */
.menu-btn span:before{
  bottom: 8px;
}
/* after を少し下にずらして描画 */
.menu-btn span:after{
  top: 8px;
}

/* メニューをオープンしたら三本線を×にする */
#menu-btn-check:checked ~ .menu-btn span{
  /* メニューオープン時は三本線の真ん中の線を透明にする */
  background-color: rgba( 255, 255, 255, 0 ); 
}
#menu-btn-check:checked ~ .menu-btn span::before{
  /* メニューオープン時は三本線の上の線を 45 度傾ける */
  bottom: 0;
  transform: rotate( 45deg ); 
}
#menu-btn-check:checked ~ .menu-btn span::after{
  /* メニューオープン時は三本線の下の線を -45 度傾ける */
  top: 0;
  transform: rotate( -45deg ); 
}
/* チェックを非表示にする */
#menu-btn-check{
  display: none;
}

/* メニュー装飾 */
.menu-content{
  width: 60%;
  height: 100%;
  position: fixed;
  top: 80;
  left: -100%; /* メニューを画面外へ */
  z-index: 80; /* 下のコンテンツの上にかぶせて表示する */
  background-color: #005B00;
  transition: all 0.5s; /* 0.5秒かけてアニメーションで出し入れする */
}
.menu-content ul{
  padding: 70px 10px 0;
}
.menu-content ul li{
  border-bottom: solid 1px #ffffff; /* メニューアイテムの区切り線 */
  list-style: none;
}
.menu-content ul li a{
  display: block;
  width: 100%;
  font-size: 15px;
  box-sizing: border-box;
  text-decoration: none;
  color: #ffffff;
  padding: 9px 15px 10px 0;
  position: relative;
}

/* メニューの出し入れ */
#menu-btn-check:checked ~ .menu-content{
  left: 0;  /* チェックされたら画面内へ */
}
</style>

<body>

<span class="header">
  <div class="hamburger-menu">
    <!-- ここからメニューボタン -->
    <input type="checkbox" id="menu-btn-check"/>
    <label for="menu-btn-check" class="menu-btn"><span></span></label>
    <!-- ここまでメニューボタン -->

    <!-- ここからメニュー本体 -->
    <div class="menu-content">
      <ul>
        <li><a href="#">メニューアイテム1</a></li>
        <li><a href="#">メニューアイテム2</a></li>
        <li><a href="#">メニューアイテム3</a></li>
      </ul>
    </div>
    <!-- ここまでメニュー本体 -->
  </div>
</span>

</body>
</html>

画像も JavaScript も使っていません。CSS もすべてこの中に含まれていて外部ファイルを参照していません。本当に「これだけ」です。この内容を index.html として保存し、ローカルのブラウザで開けば同じ挙動を確認できるはずです。

HTML の <body> 部分だけを抜き出すとこのようになっています:
<body>

<span class="header">
  <div class="hamburger-menu">
    <!-- ここからメニューボタン -->
    <input type="checkbox" id="menu-btn-check"/>
    <label for="menu-btn-check" class="menu-btn"><span></span></label>
    <!-- ここまでメニューボタン -->

    <!-- ここからメニュー本体 -->
    <div class="menu-content">
      <ul>
        <li><a href="#">メニューアイテム1</a></li>
        <li><a href="#">メニューアイテム2</a></li>
        <li><a href="#">メニューアイテム3</a></li>
      </ul>
    </div>
    <!-- ここまでメニュー本体 -->
  </div>
</span>

</body>

<body> はハンバーガーメニューのボタン部分と、ボタンをタップした時に表示されるメニュー本体部分に分かれています。メニュー本体はそれほど難しくないので、このブログエントリではボタン部分の CSS を中心に、以下の4つの観点から詳しく紹介します:

(1) JavaScript なしでどうやってタップされる前と後を識別しているのか?
(2) 「三本線」の見た目を画像を使わずにどうやって実現しているのか?
(3) (タップ後の)×印の見た目を画像を使わずにどうやって実現しているのか?
(4) JavaScript なしでどうやってメニュー本体の出し入れを実現しているのか?



(1) JavaScript なしでどうやってタップされる前と後を識別しているのか?

<body> のハンバーガーメニューボタン部分に以下のコードが含まれています:
    <input type="checkbox" id="menu-btn-check"/>

この部分はチェックボックスですが CSS で非表示に設定されているので、画面上には現れていません:
/* チェックを非表示にする */
#menu-btn-check{
  display: none;
}

メニューをタップするとチェックが入り、もう一度タップするとチェックが外れます(ただし画面には表示されません)。このチェックの有無でタップされる前と後を識別しているのでした。

また HTML ではこの部分の直後に <label for="menu-btn-check" class="menu-btn"><span></span></label> があります。後述しますがこの <span></span> 部分で三本線や×印を描画します。つまり三本線部分をタップすることで、このチェックボックスのチェックが入ったり消えたりするようになっていて、このチェックの有無で三本線か×印か、どちらを描画するかを切り替えられるような仕組みを実現しています。

以下の説明をわかりやすくするために、ここで一時的にチェックボックスの非表示スタイルを解除してみましょう。以下は該当 CSS 部分にこのようなコメントが入っている前提で説明を続けます:
/* チェックを非表示にする */
#menu-btn-check{
/*  display: none; */
}

この時点でハンバーガーメニューや×印の上部に(今まで非表示だった)チェックボックスが表示されているはずです:

2021022003

2021022004


(2) 「三本線」の見た目を画像を使わずにどうやって実現しているのか?

今回のブログを書くために色々調べる前は、ここが最大の謎でした。この三本線が画像でないとしたら何? SVG で描画しているのかと思ったけどそういう記述はないし・・・ と興味津々だった謎でした。

これを実現している HTML 及び CSS 部分は以下です:
(HTML)
<label  class="menu-btn" for="menu-btn-check"><span></span></label>

(CSS)
/* メニュー線(本体と before と after で3本表示する) */
.menu-btn span,
.menu-btn span:before,
.menu-btn span:after{
  /* 20x3 の白線 */
  content: '';
  display: block;
  height: 3px;
  width: 20px;
  border-radius: 3px;
  background-color: #ffffff;
  position: absolute;
}
/* before を少し上にずらして描画 */
.menu-btn span:before{
  bottom: 8px;
}
/* after を少し下にずらして描画 */
.menu-btn span:after{
  top: 8px;
}

HTML の <span> 部分に線を描画します。そのため CSS で背景色(#ffffff = 白)、高さ(3px)、幅(20px)の塗りつぶし矩形を描画しています。実際は白い矩形ですが、20x3 と横に細長いので白線に見えるというわけです。親要素である .menu-btn の指定で縦横とのセンタリングされて(つまり 32x32 のメニューボタンの真ん中に)表示されます。

これだけだと線が1本表示されるだけですが、CSS では span, span:before, span:after すべてで同じ指定がされています。つまり before と after も合わせると3本の線が描画されることになります(まさかこんな方法で3本描いていたとは・・。この方法だと4本に増やすことはできないかも。。)。別途 before は bottom:8px; で本体よりも少し上に、after は top:8px; で本体よりも少し下に描画するよう指定しており、これらの結果3本の白線が縦に並んでハンバーガーメニューとして表示されているのでした。これ考えた人天才だな。。

2021022005



(3) (タップ後の)×印の見た目を画像を使わずにどうやって実現しているのか?

今回の調査をする前に一番の謎だったのが (2) とすると、調査し終わって一番感動したのがこの (3) の実現方法でした。前述の方法で三本線が表示できるとして、これをタップした時にどうやって表示内容を×印に(画像も JavaScript も使わずに)切り替えるのか、です。

上述したように、まずタップしたかどうかはチェックボックスのチェックの有無で判断できるようになっています。つまり #menu-btn-check:checked の時の .menu-btn の見た目を三本線から×印に変えればよい、ということになります。ここまではなんとなくわかります。

では今度はどのようにして×印の見た目を作るのか、その答えがこちらです。チェックが付いている時の span, span:before, span:after に適用しているスタイルです:
/* メニューをオープンしたら三本線を×にする */
#menu-btn-check:checked ~ .menu-btn span{
  /* メニューオープン時は三本線の真ん中の線を透明にする */
  background-color: rgba( 255, 255, 255, 0 ); 
}
#menu-btn-check:checked ~ .menu-btn span::before{
  /* メニューオープン時は三本線の上の線を 45 度傾ける */
  bottom: 0;
  transform: rotate( 45deg ); 
}
#menu-btn-check:checked ~ .menu-btn span::after{
  /* メニューオープン時は三本線の下の線を -45 度傾ける */
  top: 0;
  transform: rotate( -45deg ); 
}

まず三本線の真ん中の白線(span)については background-color: rgba( 255, 255, 255, 0 ); のスタイルを適用します。これはいわば「透明にする」ためのスタイルで、要するに真ん中の白線を透明にして見えなくします。

次に三本線の上の白線(span:before)は bottom:0; で真ん中の白線と同じ高さに移動した上で transform: rotate( 45deg ); を適用します。この指定は「45度回転させる」スタイルです。つまり水平線(ー)だったものをバックスラッシュ(\)のような線に変えています。

同様に三本線の下の白線(span:after)も top:0; で真ん中の白線と同じ高さに移動した上で transform: rotate( -45deg ); を適用してます。この指定は「-45度回転させる」スタイルであり、水平線(ー)だったものをスラッシュ(/)のような線に変えています。
2021022005


その結果が×印になっていたのでした。眼からウロコ、そんな方法だったのか・・・


(4) JavaScript なしでどうやってメニュー本体の出し入れを実現しているのか?

最後のこの部分は、ちゃんと勉強し直す前から自分で理解できていた、という意味で「一番簡単な仕組み」でした。大まかな仕組みとしてはメニューそのものを作った後、チェックボックスにチェックが入っていない時は表示されない位置に配置し、チェックが入ったら表示される位置まで transition 指定でアニメーションをかけてあげればよいだけのことです(そしてチェックが外れたら再び表示されない位置までアニメーションで戻す)。

上記のコード内では以下のように指定しています:
/* メニュー装飾 */
.menu-content{
  width: 60%;
  height: 100%;
  position: fixed;
  top: 80;
  left: -100%; /* メニューを画面外へ */
  z-index: 80; /* 下のコンテンツの上にかぶせて表示する */
  background-color: #005B00;
  transition: all 0.5s; /* 0.5秒かけてアニメーションで出し入れする */
}
  :
  :
/* メニューの出し入れ */
#menu-btn-check:checked ~ .menu-content{
  left: 0;  /* チェックされたら画面内へ */
}


メニュー本体は .menu-content クラスで定義された部分です。チェックがついていない時点ではここに left: -100%; を指定することで画面の幅1つぶん左の位置に表示しておきます。

そしてチェックが付いた時のスタイルで left: 0; を適用します。これでチェックが付くと画面の左端からメニューが表示されるようになります(チェックが再び外れると、また画面の左端の更に左へ移動して見えなくなります)。この時に .menu-content クラス内で transition: all 0.5s; を適用しておくことで、これらの移動を 0.5 秒かけたアニメーション処理で行うようになります。つまりチェックが付くと左端からすーっと表示され、チェックが外れると左端にすーっと吸い込まれて見えなくなる、という視覚効果をつけることができるのでした。


これら (1), (2), (3), (4) をあわせて適用することで、(画像も JavaScript も使わずに)CSS だけでハンバーガーメニューを実現することができる、というものでした。この「CSS だけで実現する」というのは、「スマホに適した処理として実現できる」とも言えます(簡単に言うと「GPU 併用で処理できるので、比較的貧弱なスマホの CPU に大きな負担をかけずに実現できる」ということです。詳しくはこちら)。この CSS だけの方法だとハンバーガーメニューの見た目を更に変更するのはなかなか難しいなどの制約もありますが、「工夫次第でここまでできる」というクラフトマンシップというか、ある意味天才的な発想を垣間見たような気分で、今後もこの方法を使っていきたいと思う内容でした。

で、動作を確認し終わったら最初にコメントしたチェックボックスの非表示設定はもとに戻しておきましょう。
/* チェックを非表示にする */
#menu-btn-check{
  display: none;
}