CSS 햄버거 메뉴 애니메이션 9종 — JS 없이, CSS만으로
CSS 햄버거 메뉴 애니메이션은 클릭 한 번으로 세 줄 아이콘이 X로 바뀌고 메뉴가 열리는 전환 효과입니다. 아홉 개를 모았고 전부 transform과 opacity만 움직이며, JS 없이 CSS만으로 돌아갑니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 3줄이 X로 변환
- 02 회전하며 X로
- 03 화살표로 변형
- 04 중간 줄 슬라이드 아웃
- 05 풀스크린 오버레이 메뉴
- 06 Popover API로 JS 0줄 메뉴
- 07 링크 순차 등장
- 08 아이콘에서 원형으로 확산
- 09 배경 흐리고 어둡게
아홉 개는 흔히 보는 순서가 아니라 아이콘이 겪는 순서로 놓았습니다. 줄 세 개가 어떻게 X나 화살표로 바뀌는지(01~04), 메뉴가 화면에서 어떻게 나타나는지(05~08), 그리고 메뉴가 열린 동안 뒤 화면이 어떻게 반응하는지(09) 순서입니다. 항목마다 실제로 쓴 코드에서 핵심 규칙만 옮겼고, 완결된 아홉 개 파일은 zip 안에 있습니다.
013줄이 X로 변환
위·아래 줄이 각각 45도 돌아 가운데서 겹치며 X가 되고, 가운데 줄은 옆으로 사라집니다. 헤더에 얹는 가장 기본적인 햄버거 버튼에 씁니다.
.bar1 { transform: translate(-50%, -50%) translateY(-7px) rotate(0deg); }
.bar2 { transform: translate(-50%, -50%) scaleX(1); opacity: 1; }
.bar3 { transform: translate(-50%, -50%) translateY(7px) rotate(0deg); }
.stage.is-open .bar1 { transform: translate(-50%, -50%) translateY(0) rotate(45deg); }
.stage.is-open .bar2 { transform: translate(-50%, -50%) scaleX(0); opacity: 0; }
.stage.is-open .bar3 { transform: translate(-50%, -50%) translateY(0) rotate(-45deg); }
02회전하며 X로
아이콘 전체가 180도 돌면서 동시에 줄 세 개가 X로 겹칩니다. 강조가 필요한 모바일 내비게이션에 씁니다.
.burger { transform: rotate(0deg); transition: transform $duration $easing; }
.burger.is-open { transform: rotate(180deg); }
.bar1 { transform: translate(-50%, -50%) translateY(-7px) rotate(0deg); }
.bar3 { transform: translate(-50%, -50%) translateY(7px) rotate(0deg); }
.burger.is-open .bar1 { transform: translate(-50%, -50%) translateY(0) rotate(45deg); }
.burger.is-open .bar3 { transform: translate(-50%, -50%) translateY(0) rotate(-45deg); }
03화살표로 변형
줄의 기준점을 오른쪽 끝으로 두고 위·아래 줄을 반대로 돌리면 왼쪽이 모여 화살표 모양이 됩니다. 뒤로가기를 겸하는 메뉴 버튼에 씁니다.
.bar { transform-origin: right center; }
.bar1 { transform: translate(50%, -50%) translateY(-7px) rotate(0deg); }
.bar3 { transform: translate(50%, -50%) translateY(7px) rotate(0deg); }
.stage.is-open .bar1 { transform: translate(50%, -50%) translateY(-3px) rotate(-35deg); }
.stage.is-open .bar2 { transform: translate(50%, -50%) scaleX(.7); }
.stage.is-open .bar3 { transform: translate(50%, -50%) translateY(3px) rotate(35deg); }
04중간 줄 슬라이드 아웃
가운데 줄이 먼저 옆으로 빠지고, 그다음 위·아래 줄이 X로 겹칩니다. 닫힐 때는 순서가 반대라 X가 먼저 풀리고 가운데 줄이 마지막에 돌아옵니다.
@keyframes bar2-loop {
0% { transform: translate(-50%, -50%) translateX(0); opacity: 1; }
8% { transform: translate(-50%, -50%) translateX(140%); opacity: 0; }
78% { transform: translate(-50%, -50%) translateX(140%); opacity: 0; }
85% { transform: translate(-50%, -50%) translateX(0); opacity: 1; }
100% { transform: translate(-50%, -50%) translateX(0); opacity: 1; }
}
05풀스크린 오버레이 메뉴
버튼을 누르면 화면 전체를 덮는 색 패널이 아래에서 올라오고, 그 안에 큰 글자 링크가 가운데 정렬로 뜹니다. 링크가 많은 모바일 내비게이션에 씁니다.
.panel {
position: absolute; inset: 0;
background: $stage-orange;
transform: translateY(100%);
transition: transform $duration $easing;
}
.stage.is-open .panel { transform: translateY(0); }
06Popover API로 JS 0줄 메뉴
popover 속성만으로 메뉴가 열리고, 바깥을 클릭하거나 Esc를 누르면 브라우저가 알아서 닫아 주는 동작은 MDN Popover API 문서에 정리되어 있습니다. JS 이벤트 리스너 없이 메뉴 하나만 붙이면 되는 정적 사이트에 씁니다.
<button popovertarget="menu" aria-label="메뉴 열기">
<span class="bar"></span>
</button>
<div id="menu" popover>
<a href="#">홈</a>
<a href="#">소개</a>
</div>
07링크 순차 등장
메뉴가 열리면 링크마다 순서에 맞춰 30ms씩 늦게 나타납니다. 링크 4~6개짜리 드롭다운 메뉴에 씁니다.
.menu__link {
opacity: 0; transform: translateY(-6px);
transition: opacity $duration $ease-spring, transform $duration $ease-spring;
transition-delay: calc(var(--i) * #{$stagger});
}
.menu.is-open .menu__link { opacity: 1; transform: translateY(0); }
08아이콘에서 원형으로 확산
아이콘이 있는 모서리를 중심으로 원이 화면 전체를 덮을 때까지 커지고, 그 안에 링크 목록이 나타납니다. 로고 옆 아이콘 버튼처럼 임팩트가 필요한 랜딩에 씁니다.
.reveal {
clip-path: circle(0% at calc(100% - 30px) 30px);
transition: clip-path $duration $easing;
}
.stage.is-open .reveal {
clip-path: circle(150% at calc(100% - 30px) 30px);
}
09배경 흐리고 어둡게
메뉴가 열리면 뒤 콘텐츠가 흐려지고 어두워져 시선이 메뉴로 모입니다. 콘텐츠가 많은 페이지의 오버레이 메뉴에 씁니다.
.page {
filter: blur(0) brightness(1);
transition: filter $dur-base $ease-spring;
}
.stage.is-open .page {
filter: blur($blur) brightness(1 - $dim);
}
어디서 깨지는가 — 함정 하나
아홉 개 전부 transform과 opacity만 움직이는 데는 이유가 있습니다. width나 top을 애니메이션하면 브라우저가 매 프레임 레이아웃을 다시 계산하지만, transform과 opacity는 합성 단계에서만 처리됩니다. 그 근거는 MDN이 정리한 CSS·JS 애니메이션 성능 가이드에서 자세히 볼 수 있습니다.
챙길 게 하나 더 있는데, 함정이라기보다는 크기 문제입니다. 아이콘 크기입니다. 줄 두께를 2px 밑으로 줄이면 480×300 무대에서는 괜찮아 보여도 실제 헤더의 44px 터치 영역 안에서는 손가락에 가려 잘 안 보입니다. 줄 두께는 3px 이상으로 두고, 버튼의 클릭 가능 영역은 최소 40px 정사각형을 유지하십시오.
진짜 함정은 따로 있습니다. 01~05와 07~09는 메뉴 바깥을 클릭해도 저절로 닫히지 않습니다. 여기서는 오버레이에 클릭 핸들러를 따로 달아야 하고, 06만 popover 속성 하나로 이 문제가 공짜로 풀립니다. 지금 보는 아홉 개 파일을 잠근 압축 비밀번호는 2cpj9dyd이며, 풀어서 그대로 프로젝트에 옮기면 됩니다.
접근성
시스템에서 동작 줄이기를 켠 사용자를 위해 아홉 개 전부 prefers-reduced-motion: reduce를 처리합니다. 아이콘 전환과 메뉴 등장 애니메이션은 꺼지고, 최종 상태(열림 또는 닫힘)만 즉시 나타납니다. 08 원형 확산은 clip-path가 순간 전환되어도 메뉴 내용은 그대로 읽을 수 있고, 09 배경 흐림은 대비를 해치지 않도록 최종 밝기값만 남습니다. 브라우저별 처리 방식은 MDN의 @media prefers-reduced-motion 문서를 참고하십시오.
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .bar1, .stage.is-demo .bar2, .stage.is-demo .bar3, .stage.is-demo .menu { animation: none; }
.bar, .menu { transition-duration: 1ms; }
}
메뉴 말고 다른 전환 애니메이션은 CSS 애니메이션 카테고리에서 더 볼 수 있고, 이 블로그가 무엇을 다루는지는 소개 페이지에 정리했습니다.
FAQ
아홉 개를 한 프로젝트에 다 넣어도 됩니까?
한 사이트에는 보통 하나만 씁니다. 헤더 하나에 햄버거 메뉴가 두 종류로 겹치면 사용자가 어떤 게 진짜 메뉴 버튼인지 헷갈립니다. 모바일용과 데스크톱용을 나누고 싶다면 07 링크 순차 등장처럼 가벼운 것과 05 풀스크린 오버레이처럼 무거운 것을 화면 폭 기준으로 갈라 쓰십시오.
React에서는 클래스 토글 대신 무엇을 씁니까?
zip의 react/ 폴더는 열림 상태를 useState로 관리합니다. 링크 목록은 문자열 배열 props로 받아 map으로 렌더링하고, 06 Popover 메뉴만 실제 DOM 요소에 ref를 걸어 showPopover·hidePopover를 호출합니다.
메뉴가 열려 있을 때 뒤 페이지가 같이 스크롤됩니다.
메뉴 바깥 영역에 overflow: hidden을 걸지 않으면 뒤 페이지가 같이 스크롤됩니다. 메뉴가 열리는 순간 body에 스크롤 잠금 클래스를 하나 추가하고, 닫힐 때 제거하면 해결됩니다.