햄버거 메뉴 만들기 | HTML·CSS·JavaScript로 모바일 메뉴 열고 닫기
기술정보 26.11.03 조회 23
모바일 홈페이지를 만들다 보면 화면이 좁아지면서 PC에서 사용하던 메뉴를 그대로 보여주기 어려운 경우가 있습니다.
이때 자주 사용하는 방식이 햄버거 메뉴입니다. 버튼을 누르면 숨겨져 있던 메뉴가 열리고, 다시 누르거나 메뉴 바깥 영역을 선택하면 닫히도록 구성할 수 있습니다.
햄버거 메뉴의 모양 자체를 만드는 것은 어렵지 않지만 실제 홈페이지에 적용할 때는 몇 가지 동작을 같이 고려해야 합니다.
메뉴 버튼을 다시 눌렀을 때 닫혀야 하고, 메뉴 바깥을 클릭했을 때도 닫히는 편이 자연스럽습니다. 메뉴가 열린 동안 뒤쪽 페이지가 함께 스크롤되지 않도록 처리하거나 키보드의 ESC 키로 닫을 수 있게 만드는 것도 확인할 부분입니다.
이번 글에서는 별도의 라이브러리 없이 HTML·CSS·JavaScript를 이용해 모바일 햄버거 메뉴를 열고 닫는 기본 구조를 만들어보겠습니다.
햄버거 메뉴의 HTML 구조부터 만듭니다
먼저 메뉴를 여는 버튼과 실제 내비게이션 영역을 준비합니다.
<header class="site-header">
<a href="/" class="logo">LOGO</a>
<button
type="button"
class="menu-toggle"
aria-expanded="false"
aria-controls="mobileNav"
aria-label="메뉴 열기"
>
<span></span>
<span></span>
<span></span>
</button>
</header>
<nav
id="mobileNav"
class="mobile-nav"
aria-label="모바일 주요 메뉴"
>
<ul>
<li><a href="#company">회사소개</a></li>
<li><a href="#service">서비스</a></li>
<li><a href="#portfolio">포트폴리오</a></li>
<li><a href="#contact">문의하기</a></li>
</ul>
</nav>
<div class="menu-dim"></div>
여기서 .menu-toggle은 햄버거 메뉴 버튼이고,
#mobileNav는 버튼을 눌렀을 때 나타날 메뉴 영역입니다.
.menu-dim은 메뉴가 열렸을 때 본문 위에 표시되는 반투명 배경입니다. 이 영역을 클릭하면 메뉴가 닫히도록 사용할 예정입니다.
접근성 상태값도 같이 준비합니다
버튼에는 aria-expanded="false"를 넣었습니다.
메뉴가 닫혀 있을 때는 false, 열려 있을 때는 true로 변경해 현재 상태를 전달합니다.
aria-controls="mobileNav"는 이 버튼이 어떤 영역을 제어하는지 연결하는 데 사용할 수 있습니다.
일반적인 홈페이지 내비게이션이라면 메뉴라는 이름 때문에 무조건
role="menu"를 추가할 필요는 없습니다.
일반 사이트의 링크 내비게이션은 기본적인
<nav>,
<ul>,
<a>
구조를 유지하고 열림·닫힘 상태를 명확하게 처리하는 방식으로 구성할 수 있습니다.
CSS로 닫힌 메뉴와 열린 메뉴를 구분합니다
이제 메뉴가 처음에는 화면 오른쪽 밖에 있다가 .is-open 클래스가 추가되면 나타나도록 만들어보겠습니다.
body {
margin: 0;
}
.site-header {
position: relative;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
padding: 0 24px;
}
.logo {
color: #111;
font-weight: 700;
text-decoration: none;
}
.menu-toggle {
display: none;
position: relative;
z-index: 300;
width: 42px;
height: 42px;
padding: 8px;
border: 0;
background: transparent;
cursor: pointer;
}
.menu-toggle span {
display: block;
width: 100%;
height: 2px;
margin: 6px 0;
background: #111;
transition: 0.3s;
}
.mobile-nav {
position: fixed;
top: 0;
right: 0;
z-index: 200;
width: min(82vw, 360px);
height: 100dvh;
padding: 100px 32px 40px;
box-sizing: border-box;
background: #fff;
transform: translateX(100%);
visibility: hidden;
pointer-events: none;
transition:
transform 0.3s ease,
visibility 0s linear 0.3s;
}
.mobile-nav.is-open {
transform: translateX(0);
visibility: visible;
pointer-events: auto;
transition: transform 0.3s ease;
}
.mobile-nav ul {
margin: 0;
padding: 0;
list-style: none;
}
.mobile-nav li + li {
margin-top: 24px;
}
.mobile-nav a {
color: #111;
font-size: 20px;
text-decoration: none;
}
.menu-dim {
position: fixed;
inset: 0;
z-index: 150;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: 0.3s;
}
.menu-dim.is-active {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
body.menu-open {
overflow: hidden;
}
@media (max-width: 768px) {
.menu-toggle {
display: block;
}
}
이번 예제에서는 메뉴 영역에 transform: translateX(100%)을 적용해 화면 오른쪽으로 숨겼습니다.
메뉴에 .is-open 클래스가 추가되면 translateX(0)으로 변경되면서 화면 안쪽으로 들어옵니다.
단순히 위치만 화면 밖으로 이동시키지 않고 닫힌 상태에서
visibility: hidden과
pointer-events: none도 함께 적용했습니다.
보이지 않는 메뉴가 클릭되거나 키보드 탐색 대상처럼 남는 상황을 줄이기 위해서입니다.
메뉴가 열릴 때는 배경의 .menu-dim도 활성화합니다.
JavaScript로 메뉴를 열고 닫습니다
HTML과 CSS를 준비했다면 JavaScript에서 버튼 클릭에 따라 클래스를 추가하거나 제거합니다.
const menuToggle = document.querySelector('.menu-toggle');
const mobileNav = document.querySelector('.mobile-nav');
const menuDim = document.querySelector('.menu-dim');
const menuLinks = mobileNav.querySelectorAll('a');
function openMenu() {
mobileNav.classList.add('is-open');
menuDim.classList.add('is-active');
document.body.classList.add('menu-open');
menuToggle.setAttribute('aria-expanded', 'true');
menuToggle.setAttribute('aria-label', '메뉴 닫기');
}
function closeMenu() {
mobileNav.classList.remove('is-open');
menuDim.classList.remove('is-active');
document.body.classList.remove('menu-open');
menuToggle.setAttribute('aria-expanded', 'false');
menuToggle.setAttribute('aria-label', '메뉴 열기');
}
menuToggle.addEventListener('click', function () {
const isOpen = mobileNav.classList.contains('is-open');
if (isOpen) {
closeMenu();
} else {
openMenu();
}
});
openMenu()에서는 메뉴, 배경,
<body>에 각각 필요한 클래스를 추가합니다.
반대로 closeMenu()에서는 추가했던 클래스를 제거합니다.
메뉴 버튼을 클릭할 때 현재 .is-open 클래스가 있는지 확인하면 같은 버튼으로 열기와 닫기를 모두 처리할 수 있습니다.
이때 화면에 보이는 상태만 바꾸지 않고 aria-expanded 값도 같이 변경합니다. 메뉴가 열렸다면 true, 닫혔다면 다시 false가 됩니다.
메뉴 바깥을 클릭하면 닫히도록 합니다
모바일 메뉴에서는 메뉴 버튼을 다시 찾지 않고 주변의 어두운 영역을 눌러 닫는 동작도 많이 사용합니다.
앞에서 만든 .menu-dim에 클릭 이벤트를 연결하면 간단하게 처리할 수 있습니다.
menuDim.addEventListener('click', closeMenu);
이제 메뉴가 열린 상태에서 반투명 배경을 누르면 closeMenu()가 실행됩니다.
이처럼 닫기 동작을 별도의 함수로 만들어두면 버튼 클릭, 바깥 영역 클릭, ESC 키 등 여러 상황에서 같은 코드를 반복하지 않고 사용할 수 있습니다.

메뉴 링크를 선택한 뒤에도 닫아줍니다
한 페이지 안에서 특정 영역으로 이동하는 메뉴라면 링크를 선택한 뒤에도 모바일 메뉴가 화면에 그대로 남아 있을 수 있습니다.
각 링크를 클릭했을 때 메뉴를 닫도록 추가할 수 있습니다.
menuLinks.forEach(function (link) {
link.addEventListener('click', closeMenu);
});
특히 원페이지 형식의 홈페이지에서 #company,
#service처럼 같은 페이지의 특정 위치로 이동하도록 만들었다면 함께 적용하기 좋은 동작입니다.
다른 페이지로 완전히 이동하는 링크에서는 페이지가 새로 열리기 때문에 반드시 필요한 처리는 아니지만, 같은 페이지 내 이동을 함께 사용하는 구조라면 넣어두는 편이 자연스럽습니다.
메뉴가 열렸을 때 뒤쪽 화면의 스크롤을 막습니다
모바일 메뉴가 화면 전체 높이로 열렸는데 뒤쪽의 본문까지 같이 움직이면 사용하면서 어색하게 느껴질 수 있습니다.
앞에서 CSS에 다음 코드를 넣은 이유입니다.
body.menu-open {
overflow: hidden;
}
그리고 메뉴를 열 때는 다음 클래스를 추가했습니다.
document.body.classList.add('menu-open');
메뉴를 닫을 때는 다시 제거합니다.
document.body.classList.remove('menu-open');
이렇게 하면 메뉴가 열려 있는 동안에는 뒤쪽 페이지의 일반적인 스크롤을 막고, 메뉴를 닫은 뒤 다시 원래 상태로 돌릴 수 있습니다.
여기서 menu-open 클래스를 제거하는 코드를 빠뜨리면 메뉴를 닫은 뒤에도 페이지가 스크롤되지 않을 수 있으므로 열기와 닫기 처리를 한 쌍으로 확인해야 합니다.
ESC 키로도 메뉴를 닫을 수 있게 합니다
마우스나 터치뿐 아니라 키보드로 페이지를 이용하는 상황도 생각해볼 수 있습니다.
메뉴가 열린 상태에서 Escape 키를 누르면 닫히도록 이벤트를 추가합니다.
document.addEventListener('keydown', function (event) {
if (
event.key === 'Escape' &&
mobileNav.classList.contains('is-open')
) {
closeMenu();
menuToggle.focus();
}
});
ESC 키를 누르면 메뉴를 닫은 뒤 다시 메뉴 버튼으로 포커스를 이동하도록 했습니다. 메뉴가 사라진 뒤 사용자가 현재 위치를 잃지 않고 버튼부터 이어서 탐색할 수 있게 하기 위한 처리입니다.
화면이 PC 크기로 바뀌었을 때 상태도 초기화합니다
브라우저 크기를 조절하면서 작업하다 보면 모바일 메뉴를 열어둔 상태에서 화면을 PC 크기로 넓히는 경우가 있습니다.
이때 <body>에
menu-open이 남아 있으면 스크롤 잠금 같은 상태도 그대로 유지될 수 있습니다.
간단하게 화면 너비가 기준보다 커졌을 때 메뉴를 닫도록 처리할 수 있습니다.
window.addEventListener('resize', function () {
if (window.innerWidth > 768) {
closeMenu();
}
});
이번 CSS에서 모바일 기준을 768px로 사용했으므로 JavaScript에서도 같은 기준을 사용했습니다.
실제 홈페이지에 적용할 때는 프로젝트에서 사용하고 있는 반응형 분기점에 맞춰 값을 조정하면 됩니다.
햄버거 아이콘도 열린 상태에 맞게 바꿀 수 있습니다
메뉴가 열렸을 때 세 줄 모양의 햄버거 아이콘을 X 형태로 바꾸고 싶다면 버튼 상태에 맞춰 CSS를 추가할 수 있습니다.
예를 들어 <body>에
menu-open 클래스가 추가되는 구조를 이용하면 다음처럼 만들 수 있습니다.
body.menu-open .menu-toggle span:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
body.menu-open .menu-toggle span:nth-child(2) {
opacity: 0;
}
body.menu-open .menu-toggle span:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
이 부분은 메뉴의 동작에 반드시 필요한 기능은 아닙니다.
사이트 디자인에 따라 아이콘을 그대로 유지하거나 별도의 닫기 아이콘을 사용할 수도 있으므로 전체 UI에 맞춰 선택하면 됩니다.
햄버거 메뉴가 제대로 닫히지 않는다면 확인할 부분
코드를 적용했는데 메뉴가 열리기만 하고 닫히지 않거나, 배경을 눌러도 반응하지 않는다면 먼저 클래스 이름과 선택자를 확인합니다.
z-index 순서를 확인합니다.
menu-open 클래스가 남아 있는지 확인합니다.
HTML에는 .menu-toggle이라고 작성했는데 JavaScript에서 다른 클래스 이름을 찾고 있으면 클릭 이벤트가 연결되지 않습니다.
menu-dim이 메뉴보다 위에 겹쳐 메뉴 링크를 누를 수 없는 경우에는
z-index 순서도 살펴봐야 합니다.
이번 예제의 배치 순서
반투명 배경 → 메뉴 영역 → 메뉴 버튼
또 메뉴를 닫았는데 페이지가 움직이지 않는다면
<body>에
menu-open 클래스가 남아 있는지 확인합니다.
반응형 화면에서는 CSS와 JavaScript에서 사용하는 기준이 서로 다른지도 체크해보세요. CSS는 768px인데 JavaScript에서는 다른 너비를 기준으로 상태를 초기화하면 특정 화면 크기에서 예상과 다르게 동작할 수 있습니다.
접근성을 고려할 때 aria-expanded도 함께 변경합니다
햄버거 메뉴를 구현할 때 화면에서 열리고 닫히는 모습만 바뀌는 것으로 끝내지 않고 버튼 상태도 함께 전달할 수 있습니다.
처음 버튼에는 다음과 같이 설정했습니다.
<button
type="button"
class="menu-toggle"
aria-expanded="false"
aria-controls="mobileNav"
aria-label="메뉴 열기"
>
메뉴가 열리면 JavaScript에서 다음과 같이 변경합니다.
menuToggle.setAttribute('aria-expanded', 'true');
menuToggle.setAttribute('aria-label', '메뉴 닫기');
닫을 때는 다시 false와
메뉴 열기로 되돌립니다.
aria-expanded는 버튼이 제어하는 영역이 현재 펼쳐져 있는지 접혀 있는지를 전달하는 데 사용하는 속성입니다.
또한 일반적인 홈페이지 내비게이션을 구현하면서 단순히 메뉴라는 이유로 ARIA의
menu 역할을 추가하면 기대되는 키보드 동작까지 달라질 수 있습니다.
일반적인 사이트 링크 메뉴라면 기본 HTML 내비게이션 구조를 유지하면서 필요한 열림·닫힘 상태를 정확하게 제공하는 방식이 더 적절합니다.
실제 홈페이지에 적용할 때는 기존 구조부터 확인합니다
지금까지의 코드는 햄버거 메뉴가 동작하는 기본 예제입니다.
실제 홈페이지에서는 이미 사용 중인 헤더 HTML, 메뉴 클래스, 반응형 CSS, JavaScript가 있을 수 있습니다.
따라서 기존 홈페이지에 코드를 그대로 덮어쓰기보다 먼저 현재 메뉴가 어떤 구조로 되어 있는지 확인해야 합니다.
프로젝트마다 달라질 수 있는 부분
- 모바일 화면으로 전환되는 너비
- 메뉴가 나타나는 방향
- 1차·2차 메뉴 구조
- 메뉴 내부 스크롤 여부
- 기존 JavaScript의 메뉴 이벤트
- 헤더와 메뉴의
z-index - 서브메뉴 열림·닫힘 방식
템플릿을 수정하는 경우에도 동일합니다. 상품이나 제작 환경마다 메뉴 구조와 클래스 이름이 다를 수 있으므로 예제 코드를 그대로 복사하기보다 현재 HTML 구조에 맞춰 선택자와 동작을 조정해야 합니다.
모바일 메뉴는 열기보다 닫기 동작까지 같이 확인합니다
햄버거 메뉴의 기본 동작은 버튼을 눌러 메뉴를 보여주는 것입니다.
실제 홈페이지에서는 여기서 한 단계 더 확인해야 합니다.
버튼 다시 클릭 → 바깥 영역 클릭 → 메뉴 링크 선택 → ESC 키 → 스크롤 복원 → 화면 크기 변경
버튼을 다시 눌렀을 때 닫히는지, 바깥 영역을 눌러도 닫히는지, 메뉴 링크를 선택한 뒤에도 남아 있지 않은지, ESC 키를 사용할 수 있는지, 메뉴를 닫은 뒤 페이지 스크롤이 정상적으로 돌아오는지까지 살펴보면 모바일에서 사용하기 훨씬 자연스러운 메뉴를 만들 수 있습니다.
또 화면 크기가 바뀌었을 때 모바일 메뉴 상태가 남아 있지 않은지도 함께 확인하는 것이 좋습니다.
이번 예제처럼 열기와 닫기 기능을 각각 함수로 분리해두면 이후 메뉴 애니메이션이나 서브메뉴 기능을 추가할 때도 구조를 정리하기 쉽습니다.
DESIGNKITS HTML SOURCE
디자인키트 HTML 소스 가격 개편
HTML을 직접 수정해 홈페이지를 제작하고 있다면 디자인키트의 웹사이트 HTML 소스도 함께 살펴볼 수 있습니다.
디자인키트에서는 홈페이지 제작에 활용할 수 있는 HTML 소스를 제공하고 있으며, 실제 작업에서는 구매한 소스의 HTML·CSS·JavaScript 구조를 확인한 뒤 필요한 부분을 프로젝트에 맞게 수정해 사용할 수 있습니다.
디자인키트 HTML 소스 가격은 165,000원에서 39,000원으로 개편되었습니다.
가격 개편 내용 자세히 보기 →자주 묻는 질문
햄버거 메뉴는 JavaScript 없이도 만들 수 있나요?
CSS만으로 열림·닫힘 상태를 만드는 방식도 사용할 수 있습니다. 다만 메뉴 바깥 클릭, ESC 키 처리, 상태 초기화처럼 여러 동작을 함께 제어하려면 JavaScript를 사용하는 방식이 관리하기 편리합니다.
메뉴가 열렸는데 뒤쪽 페이지도 같이 스크롤됩니다. 어떻게 막나요?
메뉴가 열릴 때 <body>에 별도의 클래스를 추가하고
overflow: hidden을 적용할 수 있습니다.
메뉴를 닫을 때는 해당 클래스를 반드시 제거해야 페이지 스크롤이 정상적으로 돌아옵니다.
햄버거 메뉴 버튼에 aria-expanded를 넣어야 하나요?
메뉴처럼 다른 영역을 펼치고 접는 버튼에서는 현재 상태를 전달하기 위해
aria-expanded를 사용할 수 있습니다.
닫힌 상태에서는 false,
열린 상태에서는 true가 실제 화면 상태와 일치하도록 변경합니다.
모바일 메뉴에 role="menu"를 넣는 것이 좋은가요?
일반 홈페이지의 내비게이션 링크라면 단순히 메뉴라는 이유만으로 ARIA의
menu 역할을 추가할 필요는 없습니다.
menu 역할에는 별도의 키보드 상호작용이 기대되기 때문에 일반적인 사이트 내비게이션은 기본
<nav>와 링크 구조를 유지하고 필요한 상태 정보를 제공하는 방식으로 구성할 수 있습니다.
작성자 디자인키트
작성일 2026-11-03


