logo

【コピペだけ!】高機能ハンバーガーメニュー完全レスポンシブ対応(React & Sass 使用)

【コピペだけ!】高機能ハンバーガーメニュー完全レスポンシブ対応(REACT & SASS 使用)...
プログラミング
目次
  • 使用したライブラリー

  • ファイル構成

  • React VITE と各ライブラリーのセットアップ

  • 各ファイルのコード

  • 終わり

  • タブレット、スマホサイズで、ハンバーガーメニューが必要なことが多いとは思いますが実装が面倒ですよね。

    Tailwindを使ったハンバーガーメニューはこちら⬇︎

    今回はコピペするだけで、完全レスポンシブ対応のハンバーガーメニューのコードを紹介します。
    React.js & Sassを使っていて、TypeScript の tsx ファイルにも対応しています。

    この記事のコードを使うことで、簡単にハンバーガーメニューを実装できるかと思います。

    完全に好みですがCSSのアニメーションを使ってレインボータイトルも実装しています!

    デモサイトURL

    注意:これらのコードは 2024年2月の時点でのコードです。(下記機能がプロジェクトに沿う場合のみお使いください。)

    このハンバーガーメニューの機能一覧です:

    • 完全レスポンシブ対応
    • 下にスクロールしたら、メニュー全体に透明が少しかかる
    • タイトルはCSSのアニメーションでレインボーに輝く
    • メニュー内の TEST は、URLが “/contact” と “/about” の時は非表示になる
    • リンクタグを押した時に画面上部にスクロール
    • Fixed Header

    使用したライブラリー

    • React.js (VITE)
    • react-touter-dom
    • sass
    • react-icons

    ファイル構成

    src
    -- App.tsx
    -- components folder
    -- Header.tsx
    -- MenuSp
    -- sass
    -- main.scss
    -- _setting.scss
    -- _Hamburger.scss
    -- _Header.scss
    -- _CommonParts.scss

    React VITE と各ライブラリーのセットアップ

    React VITE のインストール

    npm crate vite@latest

    ページ遷移で必要になるのでインストール

    npm i react-router-dom

    今回は sass を使用しています

    npm install -D sass

    これはオプションです。なくても問題ありません

    npm react icons

    各ファイルのコード

    ハンバーガーメニュー

    React のファイル

    App.tsx
    import { BrowserRouter, Routes } from "react-router-dom";
    // components
    import Header from "./components/Header";
    // sass
    import "./sass/main.scss";
    
    function App() {
      return (
        <>
          <BrowserRouter>
            <Header />
            <Routes>
              {/* ここにコンポーネントがないと下記の warning が出る */}
              {/* No routes matched location "/"  */}
              {/* ここに必要なコンポーネントを記載の例: */}
              {/* <Route path="/" element={<Home />} /> */}
            </Routes>
          </BrowserRouter>
        </>
      );
    }
    export default App;
    
    components / Header.tsx のコード

    import { useState, useEffect } from "react";
    import { useLocation } from "react-router-dom";
    import { Link } from "react-router-dom"; // react-router-dom
    import { AiFillGithub } from "react-icons/ai"; // react-icons
    
    // go to top
    function ChangePageTop() {
      window.scroll(0, 0); // ページの一番上に移動
    }
    
    function Header() {
      const location = useLocation();
      // console.log(location.pathname); // ex) /about
    
      // hamburger menu
      const [isMenuOpen, setIsMenuOpen] = useState(false);
    
      // Headerの透明化
      const [isTransparent, setIsTransparent] = useState(false);
    
      // hamburger menu 開閉の管理
      const toggleHam = () => {
        setIsMenuOpen(!isMenuOpen);
      };
    
      // スクロールでHeaderの透明化
      useEffect(() => {
        function handleScroll() {
          if (window.scrollY > 0) {
            setIsTransparent(true);
          } else {
            setIsTransparent(false);
          }
        }
        window.addEventListener("scroll", handleScroll);
        return () => {
          window.removeEventListener("scroll", handleScroll);
        };
      }, []);
    
      return (
        <>
          <header className={isTransparent ? "header scrolled" : "header"}>
            <span
              onClick={toggleHam}
              className={isMenuOpen ? "header unShownLayer" : "header"}
            ></span>
    
            <nav className="nav">
              {/* LOGO */}
              <h1 className="navLogo">
                <Link to="/" onClick={ChangePageTop}>
                  <div
                    className="commonTitle"
                    style={{ padding: 0, fontSize: "2rem" }}
                  >
                    KZ-DEV
                  </div>
                </Link>
              </h1>
    
              <div className="navLinks">
                <Link to="/" onClick={ChangePageTop}>
                  DOCS
                </Link>
                <Link to="/about" onClick={ChangePageTop}>
                  ABOUT
                </Link>
                <Link to="/contact" onClick={ChangePageTop}>
                  CONTACT
                </Link>
                {/* about & contact ページでは非表示 */}
                <Link to="/test" onClick={ChangePageTop}>
                  {location.pathname !== "/about" && location.pathname !== "/contact" && (
                    <>
                      TEST
                    </>
                  )}
                </Link>
              </div>
    
              {/* React Icons 使用  */}
              <div className="navSNS">
                <a
                  target="blank"
                  href="https://github.com/TechnoEmpire"
                  className="navSNS__link"
                >
                  <AiFillGithub style={{ color: "white", fontSize: "2rem" }} />
                </a>
              </div>
    
              {/* ======================= */}
              {/* <!-- Hamburger Icon --> */}
              {/* ======================= */}
              <div className="headerHam">
                <button
                  onClick={toggleHam}
                  className={
                    isMenuOpen ? "hamOpen headerHam__icon" : "headerHam__icon"
                  }
                >
                  <span
                    className={
                      isMenuOpen
                        ? "headerHam__icon--bar top iconWhite"
                        : "headerHam__icon--bar top"
                    }
                  ></span>
                  <span className="headerHam__icon--bar middle"></span>
                  <span
                    className={
                      isMenuOpen
                        ? "headerHam__icon--bar bottom iconWhite"
                        : "headerHam__icon--bar bottom"
                    }
                  ></span>
                </button>
              </div>
    
              {/* ======================= */}
              {/*  Hamburger Hidden Menu */}
              {/* ======================= */}
              <div
                className={
                  isMenuOpen ? "hamOpen overWrap active hiddenMenu" : "overWrap"
                }
              >
                <ul className="overWrap__box">
                  <li className="overWrap__box--item">
                    <Link
                      to="/"
                      onClick={() => {
                        ChangePageTop();
                        toggleHam();
                      }}
                      className="overWrap__box--link"
                    >
                      DOCUMENTATION
                    </Link>
                  </li>
    
                  <li className="overWrap__box--item">
                    <Link
                      to="/contact"
                      onClick={() => {
                        ChangePageTop();
                        toggleHam();
                      }}
                      className="overWrap__box--link"
                    >
                      CONTACT
                    </Link>
                  </li>
                  <li className="overWrap__box--item">
                    <Link
                      to="/about"
                      onClick={() => {
                        ChangePageTop();
                        toggleHam();
                      }}
                      className="overWrap__box--link"
                    >
                      ABOUT
                    </Link>
                  </li>
    
                  {/* about & contact ページでは非表示 */}
                  {location.pathname !== "/about" && location.pathname !== "/contact" && (
                    <>
                      <li className="overWrap__box--item">
                        <Link
                          to="/test"
                          onClick={() => {
                            ChangePageTop();
                            toggleHam();
                          }}
                          className="overWrap__box--link"
                        >
                          TEST
                        </Link>
                      </li>
                    </>
                  )}
                </ul>
    
                {/* ================================== */}
                {/* React Icons  */}
                {/* ================================== */}
                <div className="InHamburger">
                  <a href="https://github.com/TechnoEmpire" className="sns icon">
                    <AiFillGithub style={{ color: "white", fontSize: "2rem" }} />
                  </a>
                </div>
              </div>
            </nav>
          </header>
        </>
      );
    }
    export default Header;
    

    Sass のファイル

    main.scss
    @use "./Header" as *;
    
    @use "./Hamburger" as *;
    
    @use "./CommonParts" as *;
    _Setting.scss
    /* Mixin---------------------------------------------- */
    $breakpoints: (
      's': 'screen and (max-width:480px)',
      'm': 'screen and (max-width: 767px) and (min-width:480px)',
      'l': 'screen and (max-width: 1000px) and (min-width: 767px)',
      'xl': 'screen and (min-width: 1001px) ',
    ) !default;
    
    @mixin mq($breakpoint: md) {
      @media #{map-get($breakpoints, $breakpoint)} {
        @content;
      }
    }
    
    /* Common---------------------------------------------- */
    html {
      scroll-behavior: smooth;
    }
    
    body {
      -ms-text-size-adjust: 100%;
      -webkit-text-size-adjust: 100%;
      -moz-osx-font-smoothing: grayscale;
      overflow: hidden auto;  
      color: white;
      background: #091c35;
      font-family: 'Open Sans',sans-serif;
      text-rendering: optimizeLegibility;
      -webkit-font-smoothing: antialiased;
    }
    
    _Heading.scss

    @use "./setting" as *;
    
    .header {
      z-index: 2;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      background-color: rgb(23, 23, 23);
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      
      &.scrolled {
        opacity: 0.9;
      }
    
      .nav {
        display: grid;
        grid-template-columns: 0.6fr 0.8fr 0.6fr;
        place-items: center;
    
        @include mq(s) {
          grid-template-columns: 1fr 1fr ;
        }
        
        @include mq(m) {
          grid-template-columns: 1fr 1fr ;
          column-gap: 10%;
        }
    
        @include mq(l) {
          grid-template-columns: 1fr 1fr ;
          column-gap: 40%;
        }
      }
    
      .navLinks {
    
        @include mq(l) {
          display: none;
        }
    
        @include mq(m) {
          display: none;
        }
    
        @include mq(s) {
          display: none;
        }
    
        a {
          text-decoration: none;
          color: rgb(221, 221, 221);
          font-weight: 600;
          font-size: 1.3rem;
          margin: 0 12px;
          display: inline-block;
        }
      }
    
      .navLogo {
        margin-top: 12px;
    
        a {
          text-decoration: none;
          color: black;
          font-size: 2rem;
          margin: 0 12px;
          display: inline-block;
    
          @include mq(s) {
            font-size: 1.6rem;
            margin-top: 4px;
          }
          
          @include mq(m) {
            font-size: 2rem;
            margin-top: 6px;
          }
          @include mq(l) {
            margin-top: 6px;
          }
        }
      }
    
      .navSNS {
        @include mq(l) {
          display: none;
    
        }
        @include mq(m) {
          display: none;
    
        }
        @include mq(s) {
          display: none;
        }
    
        a {
          img {
            width: 30px;
            height: 30px;
          }
        }
      }
    }
    Details

    _Hamburger.scss

    @use "./setting" as *;
    
    // 画面全体を覆う膜
    // JS (useState)
    .unShownLayer {
      width: 100vw;
      height: 100vh;
      background-color: rgba(0, 0, 0, 0.5);
      cursor: pointer;
      display: block;
      transition: 0.6 all ease;
    }
    
    // ================================
    // when a hamburger menu is open
    // JS (useState)
    // ================================
    .hamOpen {
      .headerHam__icon--bar.top {
        transform: translateY(8px) rotate(45deg);
      }
      .headerHam__icon--bar.middle {
        display: none;
      }
      .headerHam__icon--bar.bottom {
        transform: translateY(-5px) rotate(132deg);
      }
    }
    // ================================
    // when a hamburger menu is closed
    // ================================
    .headerHam {
      cursor: pointer;
      display: none;
      align-items: center;
      z-index: 11;
      transform: translate(0, -8px);
    
      @include mq(s) {
        display: block;
      }
    
      @include mq(m) {
        display: block;
      }
      @include mq(l) {
        display: block;
      }
    
      &__icon {
        cursor: pointer;
        background-color: none;
        background: none;
        border: none;
        appearance: none;
        outline: none;
        padding: 0;
        cursor: pointer;
        width: 30px;
        height: 13px;
        position: relative;
        margin-right: 6px;
    
        &--bar {
          background: rgb(223, 223, 223);
          width: inherit;
          height: 2px;
          transition: transform 0.5s, opacity 0.5s;
          display: block;
    
          // JS (useState)
          &.iconWhite {
            background: white;
          }
    
          &.top {
            position: absolute;
            top: 0;
            left: 0;
          }
    
          &.middle {
            position: absolute;
            top: 7px;
            left: 0;
          }
    
          &.bottom {
            position: absolute;
            top: 14px;
            left: 0;
          }
        }
      }
    }
    
    // =====================
    // JS (useState)
    // =====================
    .mask {
      opacity: 0.4;
      background-color: rgba(0, 0, 0, 0.8);
    }
    
    .overWrap {
      position: fixed;
      top: 0;
      right: -20%;
      height: 100vh;
      opacity: 0;
      pointer-events: none; // 押していない時は無効に
      // transition: 0.2s all ease; // 開閉時のアニメーション
    
      &__box {
        color: white;
        margin-top: 100px;
    
        @include mq(s) {
          margin-top: 60px;
          margin-left: 20px;
        }
    
        @include mq(m) {
          margin-left: 40px;
        }
    
        &--item {
          margin: 36px 0;
          list-style: none;
        }
    
        &--link {
          color: white;
          text-decoration: none;
          font-size: 1.25rem;
          letter-spacing: 1px;
          list-style: none;
    
          @include mq(s) {
            font-size: 1rem;
          }
        }
      }
    }
    
    // js
    .overWrap.active {
      pointer-events: auto !important; // burgerメニュー内を押せるようにする
      z-index: 10;
    }
    
    // Hidden Menu
    .hiddenMenu {
      overflow-y: scroll;
      position: fixed;
      top: 0;
      display: block;
      opacity: 1;
      z-index: 1;
      height: 180vw;
      background-color: rgba(0, 0, 0, 0.8);
      width: 70vw;
      height: 100vh;
    
      @include mq(s) {
        width: 92vw;
        padding: 20px 0;
      }
      @include mq(m) {
        width: 85vw;
      }
    }
    
    .InHamburger {
      margin-top: 48px;
      text-align: center;
    
      @include mq(s) {
        margin-left: -4rem;
      }
    
      @include mq(m) {
        margin-left: -10rem;
      }
    
      @include mq(l) {
        margin-left: -10rem;
      }
    }
    
    //! SP ONLY ACCORDION
    .menuSp {
      display: none;
    
      @include mq(s) {
        display: block;
      }
    
      @include mq(m) {
        display: block;
      }
    
      @include mq(l) {
        display: block;
      }
    }
    

    終わり

    今回は React でハンバーガーメニューを実装しました。
    上記のコード実装の負担が減れば幸いです。

    人気記事

    メルボルン移住(ワーホリ)を検討している方へ
    オーストリア
    メルボルン移住(ワーホリ)を検討している方へ
    メルボルンで絶対に行きたい場所10選
    オーストリア
    メルボルンで絶対に行きたい場所10選
    『ネイティブ監修』 オーストラリア厳選スラング30
    英語
    『ネイティブ監修』 オーストラリア厳選スラング30
    海外のWEBエンジニア特化スクールの費用や授業内容や費用など完全解説(オーストラリア・メルボルン)
    オーストリア
    海外のWEBエンジニア特化スクールの費用や授業内容や費用など完全解説(オーストラリア・メルボルン)
    • Kazu

      Kazu

      オーストラリア在住歴7年。現地で英語学習後に日本語教師 → 工場就職 → 海外WEB開発ディプロマを取得→フリーランス。現在は余った時間をプログラミング&ブログに費やして生きています。

    276