06Nền mega menu lan rộng
Bảng nền của mega menu lan sang hai bên theo chiều rộng các mục, trải rộng nhiều cột danh mục trên một mặt phẳng.
Dùng choMega menu danh mục trên header trang thương mại điện tử, điều hướng sản phẩm nhiều cột
width 확장배경 번짐다단 그리드

<main class="stage">
<nav class="menu is-demo">
<button class="menu__trigger" type="button" aria-expanded="false">Danh mục</button>
<div class="menu__panel">
<div class="menu__fill"></div>
<div class="menu__grid">
<a href="#">Nữ</a>
<a href="#">Nam</a>
<a href="#">Trẻ em</a>
<a href="#">Trang chủ</a>
</div>
</div>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 배경이 번지는 시간
$easing: $ease-out; // 시작 빠르고 끝은 부드럽게
$color: $stage-ink; // 트리거 버튼 색 (노란 무대와 대비)
@include stage($stage-yellow);
.menu {
position: relative;
align-self: flex-start; // 패널이 트리거 아래로 열려 무대 중앙에 두면 320px 에서 아래로 넘친다 — 위로 붙여 여유를 둔다 (실측 320px 재검증 2026-09-10)
justify-self: start; // 패널이 260px 까지 오른쪽으로 늘어나는데 트리거가 무대 가운데라 320px 에서 오른쪽으로도 넘친다 — 왼쪽에 붙인다
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: min(260px, 90vw);
padding: 4px;
pointer-events: none;
}
// 배경이 왼쪽부터 옆으로 번지듯 채워진다 (transform 만 움직인다)
&__fill {
position: absolute;
inset: 0;
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
transform: scaleX(0);
transform-origin: left; // 규칙8: 왼쪽부터 번짐
}
&__grid {
position: relative;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
padding: 8px;
opacity: 0;
}
&__grid a {
padding: 8px 10px;
border-radius: $r-control - 4px;
color: $stage-ink;
text-decoration: none;
text-align: center;
}
&__grid a:hover { background: rgba($stage-ink, .06); }
&.is-open &__fill { transform: scaleX(1); pointer-events: auto; transition: transform $duration $easing; }
&.is-open &__grid { opacity: 1; pointer-events: auto; transition: opacity $duration $easing $duration * .4; }
&.is-demo &__fill { animation: fill-loop $dur-loop $easing infinite; }
&.is-demo &__grid { animation: grid-loop $dur-loop $easing infinite; }
}
@keyframes fill-loop {
0%, 25% { transform: scaleX(0); }
45%, 80% { transform: scaleX(1); }
95%, 100% { transform: scaleX(0); }
}
@keyframes grid-loop {
0%, 33% { opacity: 0; }
50%, 78% { opacity: 1; }
92%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.menu__fill, .menu__grid { transition: none !important; }
.menu.is-demo .menu__fill, .menu.is-demo .menu__grid { animation: none; }
.menu.is-demo .menu__fill { transform: scaleX(0); }
.menu.is-demo .menu__grid { opacity: 0; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.menu {
position: relative;
align-self: flex-start;
justify-self: 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: #17141a;
cursor: pointer;
}
.menu__panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: min(260px, 90vw);
padding: 4px;
pointer-events: none;
}
.menu__fill {
position: absolute;
inset: 0;
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
transform: scaleX(0);
transform-origin: left;
}
.menu__grid {
position: relative;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
padding: 8px;
opacity: 0;
}
.menu__grid a {
padding: 8px 10px;
border-radius: 8px;
color: #17141a;
text-decoration: none;
text-align: center;
}
.menu__grid a:hover {
background: rgba(23, 20, 26, 0.06);
}
.menu.is-open .menu__fill {
transform: scaleX(1);
pointer-events: auto;
transition: transform 300ms ease-out;
}
.menu.is-open .menu__grid {
opacity: 1;
pointer-events: auto;
transition: opacity 300ms ease-out 120ms;
}
.menu.is-demo .menu__fill {
animation: fill-loop 2s ease-out infinite;
}
.menu.is-demo .menu__grid {
animation: grid-loop 2s ease-out infinite;
}
@keyframes fill-loop {
0%, 25% {
transform: scaleX(0);
}
45%, 80% {
transform: scaleX(1);
}
95%, 100% {
transform: scaleX(0);
}
}
@keyframes grid-loop {
0%, 33% {
opacity: 0;
}
50%, 78% {
opacity: 1;
}
92%, 100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.menu__fill, .menu__grid {
transition: none !important;
}
.menu.is-demo .menu__fill, .menu.is-demo .menu__grid {
animation: none;
}
.menu.is-demo .menu__fill {
transform: scaleX(0);
}
.menu.is-demo .menu__grid {
opacity: 0;
}
}// 트리거를 클릭하면 열리고 닫힌다. 데모(.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 "./MegaMenuFillDropdown.module.scss";
export type MegaMenuFillDropdownProps = {
triggerLabel: string;
categories: string[];
/** 배경이 번지는 시간 (기본 300ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function MegaMenuFillDropdown({
triggerLabel,
categories,
duration = 300,
easing = "ease-out",
color = "#17141a",
}: MegaMenuFillDropdownProps) {
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>
<div className={open ? `${styles.panel} ${styles.panelOpen}` : styles.panel}>
<div className={styles.fill} />
<div className={styles.grid}>
{categories.map((label) => (
<a key={label} href="#">{label}</a>
))}
</div>
</div>
</nav>
);
}06 Nền mega menu lan rộng — Diện tích đo được 25.75% · khung hình chuyển động 11/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


