HTML,SCSS,Javascript를 이용한 반응형 header - 메뉴바 소스

HTML,SCSS,Javascript를 이용한 반응형 header - 메뉴바 소스를 공유합니다

HTML,SCSS,Javascript를 이용한 반응형 header - 메뉴바 소스
Photo by BoliviaInteligente / Unsplash

반응형 메뉴바 소스입니다.

해당 소스에 대한 설명은 아래 링크에서 확인할 수 있습니다.

UI Dev. : 네이버 블로그

사용된 라이브러리는 아래와 같습니다.

  • vite
  • sass
  • iconify-icon
  • pretendard(font)
  • poppins(font)

HTML

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>반응형 네비게이션 메뉴바</title>
</head>
<body>
<header class="header">
    <div class="header__logo">
        brand
    </div>
    <nav id="menu" class="menu">
        <header class="menu__header">
            <div class="header__logo">
                brand
            </div>
            <button class="button button--icon" id="menuCloseButton">
                <iconify-icon icon="tabler:x" width="24" height="24"></iconify-icon>
            </button>
        </header>
        <ul class="menu__container">
            <li class="menu__item">
                <a href="#" class="menu__link">
                    제품소개
                    <button class="button button--icon button--ghost ">
                        <iconify-icon icon="tabler:chevron-up" width="24" height="24"></iconify-icon>
                    </button>
                </a>
                <ul class="submenu">
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                <button class="button button--icon">
                                    <iconify-icon icon="tabler:graph" width="24" height="24"></iconify-icon>
                                </button>
                                <div class="submenu__content-desc">
                                    <h3>제품</h3>
                                    <p>제품소개</p>
                                </div>
                            </div>
                        </a>
                    </li>
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                <button class="button button--icon">
                                    <iconify-icon icon="tabler:graph" width="24" height="24"></iconify-icon>
                                </button>
                                <div class="submenu__content-desc">
                                    <h3>제품</h3>
                                    <p>제품소개</p>
                                </div>
                            </div>
                        </a>
                    </li>
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                <button class="button button--icon">
                                    <iconify-icon icon="tabler:graph" width="24" height="24"></iconify-icon>
                                </button>
                                <div class="submenu__content-desc">
                                    <h3>제품</h3>
                                    <p>제품소개</p>
                                </div>
                            </div>
                        </a>
                    </li>
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                <button class="button button--icon">
                                    <iconify-icon icon="tabler:graph" width="24" height="24"></iconify-icon>
                                </button>
                                <div class="submenu__content-desc">
                                    <h3>제품</h3>
                                    <p>제품소개 </p>
                                </div>
                            </div>
                        </a>
                    </li>
                </ul>
            </li>
            <li class="menu__item"><a href="#" class="menu__link">블로그</a></li>
            <li class="menu__item">
                <a href="#" class="menu__link">회사소개
                    <button class="button button--icon button--ghost ">
                        <iconify-icon icon="tabler:chevron-up" width="24" height="24"></iconify-icon>
                    </button>
                </a>
                <ul class="submenu">
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                인삿말
                            </div>
                        </a>
                    </li>
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                연혁
                            </div>
                        </a>
                    </li>
                </ul>
            </li>
            <li class="menu__item">
                <a href="#" class="menu__link">고객센터
                    <button class="button button--icon button--ghost ">
                        <iconify-icon icon="tabler:chevron-up" width="24" height="24"></iconify-icon>
                    </button>
                </a>
                <ul class="submenu">
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                FAQ
                            </div>
                        </a>
                    </li>
                    <li class="submenu__item">
                        <a href="#">
                            <div class="submenu__content">
                                1:1 문의
                            </div>
                        </a>
                    </li>
                </ul>
            </li>
            <li class="menu__item"><a href="#" class="menu__link">로그인</a></li>
        </ul>
    </nav>
    <button class="button button--icon" id="menuOpenButton">
        <iconify-icon icon="tabler:menu" width="24" height="24"></iconify-icon>
    </button>
</header>
<main></main>
<script src="main.js" type="module"></script>

</body>
</html>

SASS(SCSS)

/* 색상 변수 정의 */
:root {
  --gray-50: #eef3fa;
  --gray-100: #d4d9de;
  --gray-200: #b9c0c5;
  --gray-300: #9da8ad;
  --gray-400: #819096;
  --gray-500: #68787d;
  --gray-600: #515e62;
  --gray-700: #394546;
  --gray-800: #212b2b;
  --gray-900: #04120f;
}

/* 전역 스타일 리셋 */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* 기본 폰트 설정 */
html, body {
  font-family: "Poppins", 'Pretendard Variable', sans-serif;
}

/* 링크 스타일 리셋 */
a {
  text-decoration: none;
  color: inherit;
}

/* 헤더 스타일 */
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
}

.header__logo {
  font-size: 1.5rem;
  font-weight: 600;
}

/* 메뉴 스타일 */
.menu {
  position: fixed;
  top: 0;
  right: 0;
  width: 100%;
  max-width: 425px;
  height: 100svh;
  border-left: 1px solid var(--gray-100);
  background-color: white;
  transform: translateX(100%);
  z-index: 999;

  &.active {
    transform: translateX(0);
  }

  &__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem;
    border-bottom: 1px solid var(--gray-100);
  }
}

/* 메뉴 컨테이너 스타일 */
.menu__container {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  overflow-x: hidden;
  overflow-y: auto;
  font-size: 1rem;
  flex: 1 0 auto;

  * {
    font-size: inherit;
    line-height: 1;
  }

  li > a {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem 1rem;
    border-radius: 0.25rem;
    overflow: hidden;
    font-weight: 600;
    &:active{
      background-color: var(--gray-50);
      .button--icon {
        background-color: white;
      }
    }

    &:has(.button--icon) {
      justify-content: space-between;
    }
  }
}

/* 서브메뉴 스타일 */
.submenu {
  list-style: none;
  display: none;

  &.active {
    display: block;
  }

  &__item {
    font-size: 0.875rem;
    border-radius: 0.25rem;
    overflow: hidden;
  }

  &__content {
    display: flex;
    align-items: center;
    gap: 1rem;

    &:not(:has(.button--icon)) {
      padding-inline: 1rem;
    }

    &-desc {
      display: flex;
      flex-direction: column;
      gap: 0.75rem;

      p {
        font-weight: 400;
        font-size: 0.875rem;
        color: var(--gray-500);
      }
    }
  }
}

/* 버튼 스타일 */
.button {
  &--icon {
    border: none;
    background-color: var(--gray-50);
    width: 48px;
    height: 48px;
    border-radius: 0.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
  }

  &--ghost {
    width: 24px;
    height: 24px;
    padding: 0.5rem;
    background-color: transparent;
  }
}

/* 데스크톱 스타일 (1024px 이상) */
@media (min-width: 1024px) {
  .header {
    width: 100%;
  }

  #menuOpenButton {
    display: none;
  }

  .menu {
    transform: translateX(0);
    border-left: none;
    background-color: transparent;
    height: auto;
    width: 100%;
    max-width: 100%;
    border-bottom: 1px solid var(--gray-100);

    &__item {
      position: relative;
    }

    .submenu {
      display: none;
      position: absolute;
      top: calc(100%);
      left: 0;
      width: 100%;
      min-width: 220px;
      background-color: white;
      box-shadow: 0 0 0 1px var(--gray-100);
      border-top: none;
      border-radius: 0.5rem;
      z-index: 10;

      &.active {
        display: block;
      }
    }
  }

  .menu__header {
    display: none;
  }

  .submenu__item {
    &:hover {
      background-color: var(--gray-50);
      color: inherit;

      .button--icon {
        background-color: white;
      }
    }
  }

  .menu__container {
    flex-direction: row;
    align-items: center;
    justify-content: flex-end;
    gap: 1rem;
    overflow: unset;

    li > a:hover {
      background-color: var(--gray-50);
      color: inherit;
    }
  }
}
// menu.js
export default class Menu {
    /**
     * 메뉴 클래스의 생성자입니다.
     * @param {HTMLElement} menu - 메뉴 요소
     * @param {HTMLElement} link - 서브메뉴 요소
     */
    constructor(menu, link) {
        // 메뉴 요소를 클래스 변수에 저장합니다.
        this.menu = menu;
        this.link = link;
        // 레이아웃 재조정 여부를 클래스 변수에 저장합니다.
        this.ticking = false;

        // 메서드에 this 바인딩을 합니다.
        // 이렇게 하면 메서드 내에서 this가 클래스 인스턴스를 가리키게 됩니다.
        this.removeActiveClass = this.removeActiveClass.bind(this);
        this.handleResize = this.handleResize.bind(this);
    }

    /**
     * 메뉴를 열기 위한 메서드입니다.
     * 메뉴 요소에 'active' 클래스를 추가합니다.
     */
    openMenu() {
        // 'active' 클래스를 추가합니다.
        this.menu.classList.add('active');
    }

    /**
     * 메뉴를 닫기 위한 메서드입니다.
     * 메뉴 요소에서 'active' 클래스를 제거합니다.
     */
    closeMenu() {
        this.menu.classList.remove('active');
    }

    /**
     * 'active' 클래스를 제거하는 메서드입니다.
     * 창 너비가 1024보다 크거나 같은 경우 'active' 클래스를 제거합니다.
     * ticking 값을 false로 설정합니다.
     */
    removeActiveClass() {
        if (window.innerWidth >= 1024) {
            this.menu.classList.remove('active');
            this.link.forEach((btn) => {
                if (btn.nextElementSibling === null || btn.nextElementSibling === undefined) {
                    return;
                }
                btn.nextElementSibling?.classList.remove('active');
                btn.querySelector('iconify-icon').setAttribute('icon', 'tabler:chevron-up');
            });
        }
        this.ticking = false;
    }

    /**
     * 창 크기가 변경될 때 호출되는 메서드입니다.
     * 창 너비가 768보다 크거나 같은 경우 'active' 클래스를 제거합니다.
     * 레이아웃 재조정 여부를 확인하고, 필요한 경우 'removeActiveClass' 메서드를 호출합니다.
     */
    handleResize() {
        // 레이아웃 재조정 여부를 확인합니다.
        if (!this.ticking) {
            // 'removeActiveClass' 메서드를 호출합니다.
            window.requestAnimationFrame(this.removeActiveClass);
            // 레이아웃 재조정 여부를 설정합니다.
            this.ticking = true;
        }
    }
}

Main.js

// main.js
import '@fontsource/poppins'
import 'pretendard/dist/web/variable/pretendardvariable.css'
import './style.scss'
import 'iconify-icon'
import Menu from './menu.js'

const init = () => {
    // 메뉴 버튼과 메뉴 엘리먼트 가져오기
    const menuBtn = document.getElementById('menuOpenButton');
    const menu = document.getElementById('menu');
    const menuClose = document.getElementById('menuCloseButton');
    const submenuToggleButtons = Array.from(document.querySelectorAll('.menu__link'));

    // 메뉴 인스턴스 생성 및 서브메뉴 토글 버튼 가져오기
    const menuInstance = new Menu(menu,submenuToggleButtons);

    // 서브메뉴 토글 버튼에 클릭 이벤트 추가
    submenuToggleButtons.forEach((button) => {
        button.addEventListener('click', (e) => {
            // 서브메뉴가 없으면 종료
            if (button.nextElementSibling === null || button.nextElementSibling === undefined) {
                return;
            }
            e.preventDefault();


            const icon = button.querySelector('iconify-icon');

            if(button.nextElementSibling.classList.contains('active')){
                button.nextElementSibling?.classList.remove('active');
                icon.setAttribute('icon', 'tabler:chevron-up');
            } else {
                // 서브메뉴 토글 버튼 로직
                submenuToggleButtons.forEach((btn) => {
                    if (btn.nextElementSibling === null || btn.nextElementSibling === undefined) {
                        return;
                    }
                    btn.nextElementSibling?.classList.remove('active');
                    btn.querySelector('iconify-icon').setAttribute('icon', 'tabler:chevron-up');
                });
                button.nextElementSibling?.classList.add('active');
                icon.setAttribute('icon', 'tabler:chevron-down')
            }
        });
    });

    // 메뉴 인스턴스의 활성 클래스 제거 및 이벤트 리스너 추가
    menuInstance.removeActiveClass();
    menuBtn.addEventListener('click', () => menuInstance.openMenu());
    menuClose.addEventListener('click', () => menuInstance.closeMenu());
    window.addEventListener('resize', menuInstance.handleResize);
}

window.addEventListener('DOMContentLoaded', init);