05Các mục xuất hiện lần lượt
Khi menu mở, các mục không xuất hiện cùng lúc mà lần lượt từ trên xuống, mỗi mục cách nhau 20ms.
Dùng choMenu cài đặt có nhiều mục, dropdown menu bộ lọc
transition-delay20ms 간격순차 등장

<main class="stage">
<nav class="menu is-demo">
<button class="menu__trigger" type="button" aria-expanded="false">Cài đặt</button>
<ul class="menu__panel">
<li><a href="#">Chung</a></li>
<li><a href="#">Bảo mật</a></li>
<li><a href="#">Thông báo</a></li>
<li><a href="#">Thanh toán</a></li>
</ul>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick; // 항목 하나가 나타나는 시간
$easing: $ease-out; // 시작 빠르고 끝은 부드럽게
$stagger: 20ms; // 항목 사이 시차
$color: $subject-blue-deep; // 트리거 버튼 색 (크림 글자가 얹히므로 진한 파랑)
@include stage($stage-ink);
.menu {
position: relative;
align-self: flex-start; // 패널이 4개 항목만큼 길어 트리거를 무대 중앙에 두면 아래로 넘친다 — 위로 붙여 여유를 둔다 (실측 320px 재검증 2026-09-10)
font-weight: 500;
font-size: clamp(13px, 3.6vw, 16px);
line-height: 1.4;
font-family: inherit;
&__trigger {
border: 0;
border-radius: $r-control;
padding: clamp(9px, 3vw, 13px) clamp(16px, 4vw, 22px);
font-weight: 700;
font-family: inherit;
color: $subject-cream;
background: $color;
cursor: pointer;
}
&__panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: max-content;
max-width: min(160px, 78vw);
margin: 0;
padding: 6px;
list-style: none;
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
visibility: hidden;
}
&.is-open &__panel,
&.is-demo &__panel { visibility: visible; }
&__panel li a {
display: block;
padding: 6px 12px; // 320px 재검증: align-self:flex-start 로도 4항목분 높이가 8px 모자랐다 — 항목당 4px(2px×2) 씩 줄여 20px 확보 (실측 2026-09-10)
border-radius: $r-control - 4px;
color: $stage-ink;
text-decoration: none;
}
&__panel li a:hover { background: rgba($stage-ink, .06); }
// 항목마다 20ms 씩 늦게 등장 — transform · opacity 만 움직인다
&__panel li { opacity: 0; transform: translateY(-6px); }
&.is-open &__panel li { opacity: 1; transform: none; transition: opacity $duration $easing, transform $duration $easing; }
&.is-demo &__panel li { animation: item-loop $dur-loop $easing infinite; }
@for $i from 1 through 4 {
&.is-open &__panel li:nth-child(#{$i}) { transition-delay: ($i - 1) * $stagger; }
&.is-demo &__panel li:nth-child(#{$i}) { animation-delay: ($i - 1) * $stagger; }
}
}
@keyframes item-loop {
0%, 30% { opacity: 0; transform: translateY(-6px); }
40%, 78% { opacity: 1; transform: none; }
88%, 100% { opacity: 0; transform: translateY(-6px); }
}
@media (prefers-reduced-motion: reduce) {
.menu__panel li { transition: none !important; }
.menu.is-demo .menu__panel li { animation: none; opacity: 0; transform: none; }
.menu.is-demo .menu__panel { visibility: hidden; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.menu {
position: relative;
align-self: flex-start;
font-weight: 500;
font-size: clamp(13px, 3.6vw, 16px);
line-height: 1.4;
font-family: inherit;
}
.menu__trigger {
border: 0;
border-radius: 12px;
padding: clamp(9px, 3vw, 13px) clamp(16px, 4vw, 22px);
font-weight: 700;
font-family: inherit;
color: #fff7e6;
background: #0a4fe8;
cursor: pointer;
}
.menu__panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: max-content;
max-width: min(160px, 78vw);
margin: 0;
padding: 6px;
list-style: none;
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
visibility: hidden;
}
.menu.is-open .menu__panel, .menu.is-demo .menu__panel {
visibility: visible;
}
.menu__panel li a {
display: block;
padding: 6px 12px;
border-radius: 8px;
color: #17141a;
text-decoration: none;
}
.menu__panel li a:hover {
background: rgba(23, 20, 26, 0.06);
}
.menu__panel li {
opacity: 0;
transform: translateY(-6px);
}
.menu.is-open .menu__panel li {
opacity: 1;
transform: none;
transition: opacity 220ms ease-out, transform 220ms ease-out;
}
.menu.is-demo .menu__panel li {
animation: item-loop 2s ease-out infinite;
}
.menu.is-open .menu__panel li:nth-child(1) {
transition-delay: 0ms;
}
.menu.is-demo .menu__panel li:nth-child(1) {
animation-delay: 0ms;
}
.menu.is-open .menu__panel li:nth-child(2) {
transition-delay: 20ms;
}
.menu.is-demo .menu__panel li:nth-child(2) {
animation-delay: 20ms;
}
.menu.is-open .menu__panel li:nth-child(3) {
transition-delay: 40ms;
}
.menu.is-demo .menu__panel li:nth-child(3) {
animation-delay: 40ms;
}
.menu.is-open .menu__panel li:nth-child(4) {
transition-delay: 60ms;
}
.menu.is-demo .menu__panel li:nth-child(4) {
animation-delay: 60ms;
}
@keyframes item-loop {
0%, 30% {
opacity: 0;
transform: translateY(-6px);
}
40%, 78% {
opacity: 1;
transform: none;
}
88%, 100% {
opacity: 0;
transform: translateY(-6px);
}
}
@media (prefers-reduced-motion: reduce) {
.menu__panel li {
transition: none !important;
}
.menu.is-demo .menu__panel li {
animation: none;
opacity: 0;
transform: none;
}
.menu.is-demo .menu__panel {
visibility: hidden;
}
}// 트리거를 클릭하면 열리고 닫힌다. 데모(.is-demo)는 스스로 반복한다.
var m = document.querySelector('.menu'), t = m.querySelector('.menu__trigger');
t.addEventListener('click', function () {
m.classList.remove('is-demo');
t.setAttribute('aria-expanded', m.classList.toggle('is-open'));
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./StaggerItemsDropdown.module.scss";
export type StaggerItemsDropdownProps = {
triggerLabel: string;
items: string[];
/** 항목 하나가 나타나는 시간 (기본 220ms) */
duration?: number;
easing?: string;
/** 항목 사이 시차 ms (기본 20ms) */
stagger?: number;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function StaggerItemsDropdown({
triggerLabel,
items,
duration = 220,
easing = "ease-out",
stagger = 20,
color = "#0a4fe8",
}: StaggerItemsDropdownProps) {
const [open, setOpen] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<nav className={styles.menu} style={vars}>
<button type="button" className={styles.trigger} aria-expanded={open} onClick={() => setOpen((v) => !v)}>
{triggerLabel}
</button>
<ul className={open ? `${styles.panel} ${styles.panelOpen}` : styles.panel}>
{items.map((label, i) => (
<li key={label} style={{ transitionDelay: open ? `${i * stagger}ms` : "0ms" }}>
<a href="#">{label}</a>
</li>
))}
</ul>
</nav>
);
}05 Các mục xuất hiện lần lượt — Diện tích đo được 1.174% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 css dropdown menu animation — không JS, chỉ CSS
Bài viết chứa demo này
9 css dropdown menu animation — không JS, chỉ CSSMột css dropdown menu animation là chuyển động khi menu điều hướng ẩn hiện, dựng bằng transition hoặc keyframes thuần CS Xem tất cả demo trong bài này


