18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TailwindCSSでのハンバーガーメニューの実装

Last updated at Posted at 2021-12-31

背景と実施したこと

以下のようなハンバーガメニューを実装をnext.js+TailwindCSSで実装しました。
メニューの数が多くなると横並び表示、縦並びの表示のいずれにしても限界が来るので、実装方法は知っておきたいと思ったからです。
この実装は簡単なようで意外と学ぶことが多かったため備忘として残します。

■スマホレイアウト
image.png

image.png

■PCレイアウト(参考)
image.png

使用している技術のver情報は以下の通りです。

  • next: 12.0.7
  • react: 17.0.2
  • TailwindCSS: 3.01

目次

  1. 概要
  2. ハンバーガメニューの実装
    1. PC版のメニューをスマホレイアウトの時に隠す
    2. ハンバーガメニューを設置する
    3. ハンバーガメニューをクリックした際の挙動を実装する
    4. stateに応じてメニューの表示、非表示をコントロールする
    5. closeメニューの追加、メニューをクリックした際の挙動の追加
  3. まとめ

概要

今回のサマリーとしては以下の3点がポイントになります。

  • 状態管理を行うuseStateを使ってメニューの開閉をコントロールする。
  • JSXではif文を書けないので三項演算子を使って、出し分けを行う。
  • メニューの重なりを表現するためz-indexを使う。

ハンバーガメニューの実装

PC版のメニューをスマホレイアウトの時に隠す

PCレイアウトでは普通のメニューが横並びで表示されており、スマホレイアウトの場合にまずこれを隠す必要があるため
ulタグにTailwindCSSでhiddenを指定します。
ちなみにメニューの中身はpropsで親コンポーネントから配列で受け取っています。
また配列の中身を取り出して表示を行うのにmap関数を使っています。

※詳細は以下を参照ください。
next.jsにおけるmapの使い方と「Each child in a list should have a unique "key" prop」の対応

header.js
import Link from 'next/link';
import Image from 'next/image';

export default function Header(props) {
  const data = props.list;
  console.log(data);

  return (
    <nav className='flex'>
      <div className='flex-none  sm:flex-1 md:flex-1 lg:flex-1 xl:flex-1'>
        <Link href='/'>
          <a>
            <Image src='/images/logo.png' alt='logo' width={200} height={100} />
          </a>
        </Link>
      </div>
     
      <div className='flex-initial text-[#abc5c5] font-bold m-5 '>
        <ul className='md:flex  hidden flex-initial text-left'>
          {data.map((value, index) => (
            <li key={index} className='p-4'>
              <a href={value.link}>{value.name} </a>
            </li>
          ))}
        </ul>
      </div>
    </nav>
  );
}
layout.js
export default function Layouut() {
  const menuList = [
    { name: 'about', link: '#about' },
    {
      name: 'skills',
      link: '#skills',
    },
    {
      name: 'values',
      link: '#values',
    },
    {
      name: 'future',
      link: '#future',
    },
  ];

  return (
    <>
      <Header list={menuList}></Header>
      <main>
        <Main />
        <About />
        <Skill />
        <Values />
        <Future />
      </main>
      <Footer />
    </>
  );
}

ハンバーガメニューを設置する

適当な画像を用意して、スマホレイアウトの際の右上に配置します。
ボタンタグに「absolute top-0 right-0」を指定することで右上での絶対値指定ができます。
またPCレイアウト時には非表示にしておきたいので「md:hidden」も合わせて指定します。

header.js
import Link from 'next/link';
import Image from 'next/image';
import { data } from 'autoprefixer';
import React, { useState } from 'react';

export default function Header(props) {
  console.log(openMenu);
  const data = props.list;
  console.log(data);

  return (
    <nav className='flex'>
      <div className='flex-none  sm:flex-1 md:flex-1 lg:flex-1 xl:flex-1'>
        <Link href='/'>
          <a>
            <Image src='/images/logo.png' alt='logo' width={200} height={100} />
          </a>
        </Link>
      </div>
     
      <div className='flex-initial text-[#abc5c5] font-bold m-5 '>
        <ul className='md:flex  hidden flex-initial text-left'>
          {data.map((value, index) => (
            <li key={index} className='p-4'>
              <a href={value.link}>{value.name} </a>
            </li>
          ))}
        </ul>
      </div>
      <button className='flex-initial absolute top-0 right-0 md:hidden'>
        <Image src='/images/menu.png' alt='menu' width={50} height={50} />
      </button>
    </nav>
  );
}

ハンバーガメニューをクリックした際の挙動を実装する

ここまでではボタンを配置しただけなので、クリック時のイベントを実装していきます。
ここでは、「クリックしたときにはメニューが表示され、もう一度クリックするとメニューが閉じる」という動きをイメージします。
状態が

  1. 開いたとき
  2. 閉じたとき

2パターンしかないのでtrue/falseで制御することにします。

ここで「状態管理」といえばReactのStateだと思い出しましょう。
(詳細はステートフックの利用法を参照)

header.js
  const [openMenu, setOpenMenu] = useState(false);

の記述を足します。
useStateの引数は初期値なので、ここではfalseにします。(デフォルトではメニューは閉じた状態)

そして、stateを更新するための関数setOpenMenuをmenuFunctionで呼び出すようにして
menuFunctionとボタンをクリックしたイベントを紐づけます。

setOpenMenuの引数は!openMenuとしており、クリックした際に現在のstateを反転させた状態(trueだったらfalse、falseだったらtrue)にします。

header.js
  const menuFunction = () => {
    setOpenMenu(!openMenu);
  };

途中省略
 <button onClick={menuFunction} className='flex-initial absolute top-0 right-0 md:hidden'>
   <Image src='/images/menu.png' alt='menu' width={50} height={50} />
 </button>

ここまでのコードは以下の通りです。

header.js
import Link from 'next/link';
import Image from 'next/image';
import { data } from 'autoprefixer';
import React, { useState } from 'react';

export default function Header(props) {
  const [openMenu, setOpenMenu] = useState(false);
  console.log(openMenu);
  const data = props.list;
  console.log(data);

  const menuFunction = () => {
    setOpenMenu(!openMenu);
  };

  return (
    <nav className='flex'>
      <div className='flex-none  sm:flex-1 md:flex-1 lg:flex-1 xl:flex-1'>
        <Link href='/'>
          <a>
            <Image src='/images/logo.png' alt='logo' width={200} height={100} />
          </a>
        </Link>
      </div>
     
      <div className='flex-initial text-[#abc5c5] font-bold m-5 '>
        <ul className='md:flex  hidden flex-initial text-left'>
          {data.map((value, index) => (
            <li key={index} className='p-4'>
              <a href={value.link}>{value.name} </a>
            </li>
          ))}
        </ul>
      </div>
      <button onClick={menuFunction} className='flex-initial absolute top-0 right-0 md:hidden'>
        <Image src='/images/menu.png' alt='menu' width={50} height={50} />
      </button>
    </nav>
  );
}

stateに応じてメニューの表示、非表示をコントロールする

クリックしてstateの値(openMenu)が変わることを確認できたら、次にこの値に応じてメニューの表示・非表示が実現できるように条件分岐を組んでいきます。
JSXでの条件分岐はif文を使えません。そのため三項演算子で組んでいきます
trueであればメニューを表示し、falseであれば何もしません。
またメニューの構成としては画面の横幅一枚の大きなdivを配置し、それを1:1の比率で縦に割ります。(basis-1/2を指定)
そのうちの右半分を実際のulの表示にあてるというイメージで作っています。

またメニューリストは画面の右上からハンバーガメニューの上位レイヤーに重ねて表示させるためz-10の指定をしています。

■以下の様なイメージ
image.png

header.js
      {openMenu ? (
        <div className='flex flex-row absolute z-10 top-0 right-0  min-h-fit min-w-full'>
          <div className='basis-1/2'></div>

          <div className='basis-1/2 bg-white'>
            <ul className=' text-center border-l-2 '>
              {data.map((value, index) => (
                <li key={index} className='p-2 border-b-2'>
                  <a href={value.link} >
                    {value.name}
                  </a>
                </li>
              ))}
            </ul>
          </div>
        </div>
      ) : undefined}

closeメニューの追加、メニューをクリックした際の挙動の追加

メニューが表示されることが確認できたら最後にメニューをクリックした際の挙動を整えます。
メニュークリックする際には、対象のページまでリンクで飛ばす(またはスクロールする)のが一般的なため、
このとき同時に開いたメニューを閉じるようにする必要があります。

(スクロールさせるための挙動に関してはTailwindCSSでのスムーススクロールの実装を参照してみてください。)

そのためaタグのクリックイベントにmenuFunctionを追加します。

またメニューを開いたものの、どこにも遷移せずメニューを閉じる動線を確保するため「close」を追加します。

header.js
      {openMenu ? (
        <div className='flex flex-row absolute z-10 top-0 right-0  min-h-fit min-w-full'>
          <div className='basis-1/2'></div>

          <div className='basis-1/2 bg-white'>
            <ul className=' text-center border-l-2 '>
              <li className='p-2 border-b-2'>
                <button onClick={menuFunction} className='font-bold'>
                  close
                </button>
              </li>
              {data.map((value, index) => (
                <li key={index} className='p-2 border-b-2'>
                  <a href={value.link} onClick={menuFunction}>
                    {value.name}
                  </a>
                </li>
              ))}
            </ul>
          </div>
        </div>
      ) : undefined}

これで完成です!
完成後のコードは以下の通りです。

header.js
import Link from 'next/link';
import Image from 'next/image';
import { data } from 'autoprefixer';
import React, { useState } from 'react';

export default function Header(props) {
  const [openMenu, setOpenMenu] = useState(false);
  console.log(openMenu);
  const data = props.list;
  console.log(data);

  const menuFunction = () => {
    setOpenMenu(!openMenu);
  };

  return (
    <nav className='flex'>
      <div className='flex-none  sm:flex-1 md:flex-1 lg:flex-1 xl:flex-1'>
        <Link href='/'>
          <a>
            <Image src='/images/logo.png' alt='logo' width={200} height={100} />
          </a>
        </Link>
      </div>
      {openMenu ? (
        <div className='flex flex-row absolute z-10 top-0 right-0  min-h-fit min-w-full'>
          <div className='basis-1/2'></div>

          <div className='basis-1/2 bg-white'>
            <ul className=' text-center border-l-2 '>
              <li className='p-2 border-b-2'>
                <button onClick={menuFunction} className='font-bold'>
                  close
                </button>
              </li>
              {data.map((value, index) => (
                <li key={index} className='p-2 border-b-2'>
                  <a href={value.link} onClick={menuFunction}>
                    {value.name}
                  </a>
                </li>
              ))}
            </ul>
          </div>
        </div>
      ) : undefined}
      <div className='flex-initial text-[#abc5c5] font-bold m-5 '>
        <ul className='md:flex  hidden flex-initial text-left'>
          {data.map((value, index) => (
            <li key={index} className='p-4'>
              <a href={value.link}>{value.name} </a>
            </li>
          ))}
        </ul>
      </div>
      <button onClick={menuFunction} className='flex-initial absolute top-0 right-0 md:hidden'>
        <Image src='/images/menu.png' alt='menu' width={50} height={50} />
      </button>
    </nav>
  );
}

まとめ

いかがでしたでしょうか?
レスポンシブ対応の基本をはじめ、stateを使った実装や三項演算子の使い方など細かい点で大事なテクニックが盛り込まれているため
初心者の方にとっては非常にいい勉強になると思います。

ハンバーガーメニューのデザイン自体はもっと洗練されたものがあると思うので、世の中のアプリを例にどんどんご自身でカスタマイズしてみてください!

18
15
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?