GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

CSS 드롭다운 메뉴 애니메이션 9종 — JS 없이, CSS만으로

CSS 드롭다운 메뉴 애니메이션은 트리거를 누르거나 올렸을 때 패널이 어떻게 나타나고 사라지는지를 정하는 짧은 전환입니다. 아홉 개 중 하나(08)는 자바스크립트를 한 줄도 안 씁니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 흔히 보는 순서가 아니라 메뉴가 실제로 붙는 자리 순서로 놓았습니다. 일반 내비게이션(01~05), 넓은 카테고리 메뉴(06), 헤더에 여러 개가 나란한 경우(07), 가벼운 액션 메뉴(08), 좁은 화면(09)입니다. 아래 코드는 여닫는 핵심 규칙만 뽑았고, 트리거·마크업까지 다 붙은 파일은 zip 안에 있습니다. 세로로 항목을 늘어뜨리는 쪽이라면 CSS 아코디언 애니메이션이 01의 그리드 트릭을 더 깊게 파고, 클릭 뒤 떠서 사라질 알림이 필요하면 웹 토스트 알림 CSS가 그 역할입니다.

01height:auto 트랜지션

화면에서 일어나는 일

height: auto는 애니메이션할 수 없다는 게 CSS의 오래된 제약입니다.

height:auto 트랜지션
새 창에서 열기

01 · height:auto 트랜지션 — 측정 누적면적 29.538% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-template-rows0fr→1froverflow:hidden

css 드롭다운 메뉴 부드럽게 여는 법

대신 grid-template-rows를 0fr에서 1fr로 트랜지션하면 실제 콘텐츠 높이까지 부드럽게 늘어나는 것처럼 보입니다. 자식 요소에 overflow: hidden을 걸어야 늘어나는 중간 과정이 잘려 보이지 않습니다.

.menu__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows $duration $easing;
}
.menu__list {
  overflow: hidden;              // grid-template-rows 트릭엔 필수
  min-height: 0;
}
.menu.is-open .menu__panel { grid-template-rows: 1fr; }

02페이드+슬라이드

어떤 움직임인가

메뉴가 투명도와 동시에 위쪽에서 8px 미끄러져 내려오며 나타납니다.

페이드+슬라이드
새 창에서 열기

02 · 페이드+슬라이드 — 측정 누적면적 11.872% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

opacity+translateY0.18초ease-out

언제 쓰나

헤더의 계정 메뉴처럼 클릭 한 번으로 여닫히는 자리에 씁니다. 메뉴 바깥을 클릭하면 닫히는 리스너 하나면 충분합니다.

.menu__panel {
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
}
.menu.is-open .menu__panel {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  transition: opacity $duration $easing, transform $duration $easing;
}

03모서리 기준 확대

무엇이 보이나

메뉴가 트리거 버튼이 붙은 모서리를 transform-origin으로 삼아 그 지점에서부터 확대되며 열립니다.

모서리 기준 확대
새 창에서 열기

03 · 모서리 기준 확대 — 측정 누적면적 12.244% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transform-originscale(0.95→1)0.15초

어디에 쓰나

우측 상단 프로필 메뉴처럼 화면 끝에 붙는 메뉴에 잘 맞습니다.

.menu__panel {
  transform-origin: top right;    // 트리거가 붙은 모서리
  transform: scale(.9);
  opacity: 0;
}
.menu.is-open .menu__panel {
  transform: scale(1);
  opacity: 1;
  transition: transform $duration $easing, opacity $duration $easing;
}

04클립패스 오픈

화면에서 일어나는 일

clip-path 사각형이 트리거 아래쪽에서부터 펼쳐지며 메뉴 내용을 잘라낸 채로 드러냅니다.

클립패스 오픈
새 창에서 열기

04 · 클립패스 오픈 — 측정 누적면적 1.538% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

clip-pathinset()0.2초

쓰는 자리

카드 안 컨텍스트 메뉴처럼 좁은 자리에 어울립니다.

.menu__panel {
  clip-path: inset(0 0 100% 0);
  pointer-events: none;
}
.menu.is-open .menu__panel {
  clip-path: inset(0 0 0% 0);
  pointer-events: auto;
  transition: clip-path $duration $easing;
}

05항목 순차 등장

어떤 움직임인가

메뉴가 열리면 항목들이 한꺼번에 나타나지 않고 위에서부터 시차를 두고 하나씩 등장합니다.

항목 순차 등장
새 창에서 열기

05 · 항목 순차 등장 — 측정 누적면적 1.174% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transition-delay20ms 간격순차 등장

언제 쓰나

항목이 많은 설정 메뉴에서 한눈에 다 쏟아지는 느낌을 줄여줍니다.

.menu__panel li {
  opacity: 0;
  transform: translateY(-6px);
}
.menu.is-open .menu__panel li {
  opacity: 1;
  transform: none;
  transition: opacity $duration $easing, transform $duration $easing;
}
@for $i from 1 through 4 {
  .menu.is-open .menu__panel li:nth-child(#{$i}) {
    transition-delay: ($i - 1) * $stagger;
  }
}

06메가메뉴 배경 채우기

무엇이 보이나

메가메뉴 배경 패널이 항목 폭에 맞춰 옆으로 번지듯 채워지며 다단 카테고리를 한 판에 펼칩니다.

메가메뉴 배경 채우기
새 창에서 열기

06 · 메가메뉴 배경 채우기 — 측정 누적면적 25.75% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

width 확장배경 번짐다단 그리드

어디에 쓰나

쇼핑몰 헤더의 메가 메뉴 ui처럼 카테고리가 많을 때 씁니다.

.menu__fill {
  position: absolute;
  inset: 0;
  transform: scaleX(0);
  transform-origin: left;
}
.menu.is-open .menu__fill {
  transform: scaleX(1);
  transition: transform $duration $easing;
}
.menu__grid { opacity: 0; }
.menu.is-open .menu__grid {
  opacity: 1;
  transition: opacity $duration $easing $duration * .4;
}

07지연 열림(transition-delay)

화면에서 일어나는 일

마우스가 지나가기만 해도 안 열리게 transition-delay로 진입 후 150ms를 기다렸다가 메뉴를 엽니다.

지연 열림(transition-delay)
새 창에서 열기

07 · 지연 열림(transition-delay) — 측정 누적면적 10.278% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transition-delay150ms 지연:hover 진입

쓰는 자리

헤더에 메뉴 여러 개가 나란할 때 커서가 스치기만 해도 전부 열려버리는 걸 막습니다.

.menu__panel {
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity $duration $easing, transform $duration $easing; // 닫힐 땐 지연 없음
}
.menu:hover .menu__panel {
  opacity: 1;
  transform: none;
  transition-delay: $delay;       // 열릴 때만 기다린다
}

08popover 속성 사용

어떤 움직임인가

popover 속성과 ::backdrop를 쓰면 JS 토글 없이 브라우저가 열고 닫기·바깥 클릭·포커스를 대신 관리합니다.

popover 속성 사용
새 창에서 열기

08 · popover 속성 사용 — 측정 누적면적 8.969% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

popover::backdropJS 없음

언제 쓰나

트리거 버튼에 popovertarget만 걸면 됩니다.

<button popovertarget="menu-panel">옵션</button>
<ul id="menu-panel" popover>
  <li><a href="#">공유</a></li>
  <li><a href="#">북마크</a></li>
  <li><a href="#">신고</a></li>
</ul>
.menu__panel {
  transform: scale(.9);
  opacity: 0;
}
.menu__panel::backdrop {
  background: rgba($stage-ink, .18);
  transition: background $duration $easing;
}
.menu__panel:popover-open {
  transform: scale(1);
  opacity: 1;
}

09모바일 전체 시트

무엇이 보이나

좁은 화면에서는 드롭다운 대신 메뉴가 화면 아래에서 전체 폭 시트로 올라와 손가락으로 누르기 쉬운 크기가 됩니다.

모바일 전체 시트
새 창에서 열기

09 · 모바일 전체 시트 — 측정 누적면적 62.561% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

미디어쿼리env(safe-area-inset-bottom)전체 폭

어디에 쓰나

반응형 헤더의 더보기 메뉴에 씁니다.

.menu__sheet {
  position: absolute;
  inset: auto 0 0 0;
  transform: translateY(100%);
}
.menu__sheet.is-open {
  transform: translateY(0);
  transition: transform $duration $easing;
}

어디서 깨지는가 — 함정 하나

그리드 트랙 재계산 비용

01의 grid-template-rows 트릭은 height나 top을 직접 애니메이션하지 않는데도 브라우저가 매 프레임 그리드 트랙 크기를 다시 계산합니다. MDN의 CSS 그리드 레이아웃 문서가 이 값의 계산 비용을 설명합니다. 항목이 아주 많은 메뉴(수십 줄)에서는 이 트릭 대신 interpolate-size나 calc-size()를 쓰는 게 낫지만, 두 속성 모두 아직 지원이 갈립니다(caniuse의 interpolate-size 지원표).

popover 의 기본 위치

함정은 아니지만 짚어야 할 게 두 개 있습니다. 08 popover는 기본 상태로 두면 화면 한가운데 뜨는 UA 기본 위치를 씁니다. 트리거 바로 아래에 붙이려면 CSS 앵커 포지셔닝이 필요한데, 이 기능은 브라우저마다 지원 범위가 다릅니다(caniuse의 CSS 앵커 포지셔닝 지원표). 이 값들이 이미 반영된 아홉 개 파일의 압축 비밀번호는 wncdnnc7이고, 아래 버튼으로 받을 수 있습니다.

지연은 여는 쪽에만

다른 하나는 07 지연 열림입니다. transition-delay를 여는 쪽에만 걸고 닫는 쪽엔 걸지 않아야 마우스를 빠르게 치웠을 때 메뉴가 곧장 닫힙니다. 두 방향에 같은 지연을 걸면 사용자는 메뉴가 "안 닫힌다"고 느낍니다. 메뉴 항목 자체를 계속 흘려 보여야 하는 자리라면 여닫이 대신 CSS 무한 롤링 배너가 같은 헤더 자리의 다른 답입니다.

접근성

동작 줄이기를 켰을 때

아홉 개 전부 prefers-reduced-motion: reduce를 처리해서, 운영체제에서 "동작 줄이기"를 켠 사람에게는 트랜지션 없이 상태만 즉시 바뀝니다. 열고 닫는 기능 자체는 지우지 않는 게 원칙입니다. 01은 트랜지션 없이 즉시 접히고 펴지며, 07은 지연 없이 즉시 열립니다. 트리거 버튼에는 항상 aria-expanded를 붙이고, 08의 popover는 Esc로 닫히는 것까지 브라우저가 맡아 처리합니다. 값 조합은 MDN의 @media prefers-reduced-motion 문서에 정리돼 있습니다.

@media (prefers-reduced-motion: reduce) {
  .menu__panel { transition: none !important; }
  .menu.is-demo .menu__panel { animation: none; }
}

다른 전환·등장 주제는 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

css 전체 드롭다운 메뉴에 꼭 JS가 필요합니까?

아닙니다. 08처럼 popover 속성만으로 열고 닫기·바깥 클릭·포커스 이동까지 브라우저가 처리합니다. 다만 caniuse의 popover 지원표를 먼저 확인하십시오. 나머지 여덟 개는 클릭 토글에 짧은 JS 몇 줄만 씁니다. 06 메가메뉴의 배경 채우기처럼 이 규칙들을 유틸 클래스로 옮겨 쓰고 싶다면 Tailwind 애니메이션 유틸이 그 이동을 보여 줍니다.

09 모바일 시트와 01 데스크톱 드롭다운을 같이 써도 됩니까?

네, 흔한 조합입니다. 미디어 쿼리로 좁은 화면에서는 09의 마크업을, 넓은 화면에서는 01이나 02의 마크업을 보여주면 됩니다. zip의 react/ 폴더는 각각 별도 컴포넌트라 화면 폭에 따라 골라 렌더링하면 됩니다.

메뉴가 버튼 위치와 다른 곳에 뜹니다.

트리거를 감싸는 요소에 position: relative가 있는지 먼저 확인하십시오. 08 popover는 예외로, 기본은 화면 중앙에 뜨고 트리거 옆에 붙이려면 앵커 포지셔닝이 따로 필요합니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.