9 css dropdown menu animation — không JS, chỉ CSS
Mộ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 CSS, không cần thư viện JS nào để
Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip
- 01 Chuyển đổi height:auto
- 02 Fade + slide
- 03 Phóng to từ góc (transform-origin)
- 04 Mở bằng clip-path
- 05 Các mục xuất hiện lần lượt
- 06 Nền mega menu lan rộng
- 07 Mở trễ (transition-delay)
- 08 Dùng thuộc tính popover gốc
- 09 Sheet toàn màn hình trên mobile
Chín kiểu dưới đây đi từ cách làm quen thuộc nhất đến những cái ít ai chạm tới. 01 đến 05 vẫn là những gì nav bar nào cũng cần: height, opacity, transform, clip-path, và stagger từng mục. 06 đến 07 mang tính ý tưởng hơn — mega menu và độ trễ hover-intent. 08 và 09 đẩy xa hơn nữa, dùng popover gốc của trình duyệt và sheet toàn màn hình cho mobile. Bạn có thể xem lưới phía trên tự chạy lần lượt cả chín kiểu — mỗi css dropdown menu animation dưới đây nằm trong một iframe cố định, không có gì để bấm, nên nó cứ lặp một mình.
01Chuyển đổi height:auto
Chiều cao menu được nội suy từ 0 đến chiều cao nội dung thực qua grid-template-rows, nên việc mở rộng height:auto trông mượt như có animation thật sự — không cần đo chiều cao bằng JS. Bạn dùng kiểu này cho dropdown điều hướng, hoặc menu kiêm luôn accordion lọc.
.menu {
&__panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows $duration $easing;
}
&__list {
overflow: hidden;
min-height: 0;
}
&.is-open &__panel { animation: none; grid-template-rows: 1fr; }
}
@keyframes reveal-loop {
0%, 30% { grid-template-rows: 0fr; }
45%, 80% { grid-template-rows: 1fr; }
95%, 100% { grid-template-rows: 0fr; }
}
02Fade + slide
Menu vừa mờ dần vừa trượt xuống khoảng 8px từ phía trên, nên phần xuất hiện không bị đột ngột. Bạn đặt kiểu này cho menu tài khoản trên header hoặc dropdown menu điều hướng phía trên cùng trang.
.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;
}
03Phóng to từ góc (transform-origin)
Menu phóng to mở ra từ đúng góc gần nút trigger nhất, góc đó được đặt làm transform-origin nên hướng mở luôn khớp với vị trí trigger dù trigger nằm bên trái hay phải. Bạn dùng cho menu hồ sơ ở góc trên bên phải, hoặc menu tùy chọn trên toolbar.
.menu__panel {
transform-origin: top right;
transform: scale(.9);
opacity: 0;
pointer-events: none;
}
.menu.is-open .menu__panel {
opacity: 1;
transform: scale(1);
pointer-events: auto;
transition: opacity $duration $easing, transform $duration $easing;
}
04Mở bằng clip-path
Hình chữ nhật clip-path mở rộng dần từ phía dưới trigger xuống, để lộ nội dung menu như đang cắt ra từng chút một thay vì hiện nguyên khối. Bạn đặt kiểu này cho context menu bên trong card, hoặc menu overflow của toolbar quá hẹp.
.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;
}
@keyframes reveal-loop {
0%, 30% { clip-path: inset(0 0 100% 0); }
45%, 80% { clip-path: inset(0 0 0% 0); }
95%, 100% { clip-path: inset(0 0 100% 0); }
}
05Các mục xuất hiện lần lượt
Khi menu mở, các mục không hiện cùng lúc mà lần lượt từ trên xuống, mỗi mục cách nhau 20ms nhờ transition-delay tính theo nth-child. Bạn dùng cho menu cài đặt có nhiều mục, hoặc dropdown menu bộ lọc dài.
.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;
}
}
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 bằng scaleX, trải rộng nhiều cột danh mục trên một mặt phẳng thay vì mở từng cột riêng. Bạn dùng cho mega menu danh mục trên header trang thương mại điện tử, hoặc điều hướng sản phẩm nhiều cột.
.menu__fill {
position: absolute;
inset: 0;
transform: scaleX(0);
transform-origin: left;
}
.menu__grid { opacity: 0; }
.menu.is-open .menu__fill {
transform: scaleX(1);
transition: transform $duration $easing;
}
.menu.is-open .menu__grid {
opacity: 1;
transition: opacity $duration $easing $duration * .4;
}
07Mở trễ (transition-delay)
Menu chờ 150ms sau khi con trỏ đi vào mới mở, nhờ transition-delay đặt trên trạng thái :hover — nên con trỏ chỉ lướt qua sẽ không làm menu bật ra ngoài ý muốn. Bạn dùng khi có nhiều menu header xếp cạnh nhau, hoặc điều hướng không nên mở nhầm.
.menu__panel {
opacity: 0;
transform: translateY(-6px);
pointer-events: none;
transition: opacity $duration $easing, transform $duration $easing;
}
.menu:hover .menu__panel,
.menu.is-open .menu__panel {
opacity: 1;
transform: none;
pointer-events: auto;
transition-delay: $delay;
}
08Dùng thuộc tính popover gốc
Dùng thuộc tính popover cùng ::backdrop, trình duyệt tự lo việc mở/đóng, click ra ngoài và focus — không cần viết một dòng JS toggle nào. Bạn dùng cho menu hành động nhẹ, hoặc trang không muốn tăng thêm JS bundle.
<button class="menu__trigger" popovertarget="menu-panel">Tùy chọn</button>
<ul class="menu__panel" id="menu-panel" popover>
<li><a href="#">Chia sẻ</a></li>
<li><a href="#">Đánh dấu</a></li>
</ul>
.menu__panel {
transform: scale(.9);
opacity: 0;
}
.menu__panel::backdrop { background: rgba($stage-ink, .18); }
.menu__panel:popover-open { transform: scale(1); opacity: 1; }
09Sheet toàn màn hình trên mobile
Trên màn hình hẹp, thay vì dropdown, menu trượt lên thành một sheet rộng hết chiều ngang từ dưới màn hình, đủ lớn để chạm bằng ngón tay thay vì con trỏ chuột nhỏ xíu. Bạn dùng cho menu điều hướng trên web mobile, hoặc menu overflow của header responsive.
.menu__sheet {
position: absolute;
left: 0; right: 0; bottom: 0;
transform: translateY(100%);
}
.menu__sheet.is-open {
transform: translateY(0);
transition: transform $duration $easing;
}
@media (max-width: 360px) {
.menu__sheet { padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); }
}
Chỗ dễ hỏng — cái bẫy
Cái bẫy hay gặp nhất nằm ở kiểu 01: nếu bạn quên đặt overflow: hidden trên phần tử con của grid-template-rows, quá trình chuyển 0fr → 1fr sẽ không cắt gọn nội dung mà để nó tràn ra ngoài suốt lúc chuyển động, khiến animation trông giật thay vì mượt — lỗi này không báo gì cả, chỉ âm thầm hỏng. Bẫy thứ hai nằm ở kiểu 07: nếu bạn đặt transition-delay luôn cả cho trạng thái đóng, menu sẽ đóng trễ 150ms sau khi con trỏ rời đi, cảm giác như bị dính lại — cần tách delay ra để chỉ áp dụng lúc mở. Toàn bộ chín kiểu, kèm cả phần vá hai lỗi này, nằm sẵn trong file nén — bạn chỉ cần giải nén bằng wncdnnc7, gõ đúng như nó xuất hiện ở dòng này, không thêm khoảng trắng nào.
Khả năng tiếp cận thế nào
Cả chín kiểu đều tôn trọng prefers-reduced-motion, nhưng mỗi kiểu bỏ bớt một lượng chuyển động khác nhau chứ không phải tắt sạch như nhau. 01, 04 và 05 dừng animation lặp lại và đứng yên ở trạng thái đóng, vì một menu không tự động mở-đóng vẫn hoạt động bình thường khi người dùng bấm thật. 02, 03 và 07 giữ transition khi mở thật (đó là phản hồi cần thiết cho thao tác) nhưng bỏ animation lặp trình diễn. 09 giữ transform tức thời, không kèm easing kéo dài. Bạn có thể xem quy tắc đầy đủ tại MDN về prefers-reduced-motion.
@media (prefers-reduced-motion: reduce) {
.menu__panel,
.menu__fill,
.menu__grid,
.menu__sheet {
transition: none !important;
animation: none !important;
}
}
Toàn bộ bộ sưu tập CSS còn lại nằm ở chuyên mục CSS, và trang giới thiệu nói rõ trang này bán gì và không bán gì.
FAQ
css dropdown menu animation có cần thư viện JS nào không?
Không. Cả chín kiểu trên đều chỉ dùng thuần CSS — transition, animation, grid-template-rows, clip-path, hoặc thuộc tính popover gốc của trình duyệt — không cần jQuery hay bất kỳ thư viện dropdown nào. Phần JS trong demo chỉ để toggle class is-open, và với kiểu 08 thì ngay cả việc đó cũng do trình duyệt tự làm.
Hỗ trợ trình duyệt của các kiểu này thế nào?
Phần lớn dùng transform và opacity nên chạy được ở hầu hết trình duyệt hiện đại. Riêng kiểu 08 dựa vào thuộc tính popover, còn kiểu 01 dựa vào grid-template-rows — bạn nên kiểm tra bảng tương thích trên MDN cho từng thuộc tính CSS trước khi đưa vào sản phẩm thật.
Có bản React cho từng kiểu không?
Có. File nén đi kèm có cả thư mục vanilla/ (HTML + SCSS thuần) lẫn thư mục react/ (mỗi kiểu là một component riêng), nên bạn ghép trực tiếp vào dự án React mà không phải viết lại animation từ đầu.