HTML,SCSS,Javascript를 이용한 반응형 header - 메뉴바 소스
HTML,SCSS,Javascript를 이용한 반응형 header - 메뉴바 소스를 공유합니다
반응형 메뉴바 소스입니다.
해당 소스에 대한 설명은 아래 링크에서 확인할 수 있습니다.
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
// 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);
Comments ()