9 hiệu ứng css accordion animation không JS
Css accordion animation là hiệu ứng chuyển chiều cao nội dung từ 0 sang auto, dựng bằng interpolate-size và calc-size() của CSS thay vì JavaScript.
Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip
- 01 Chuyển height:auto với interpolate-size
- 02 Chỉ mở một mục nhờ thuộc tính name
- 03 Xoay icon mũi tên
- 04 Nội dung fade + trượt
- 05 Viền nhấn mạnh mục đang mở
- 06 Accordion lồng nhau
- 07 Fallback calc-size()
- 08 Mở với hiệu ứng đàn hồi
- 09 Tái dùng làm menu sidebar
Thứ tự chín mục đi theo mức độ bạn cần can thiệp vào hành vi mặc định của accordion. Từ 01 đến 05 vẫn là một khối đóng/mở đơn, chỉ khác ở phần trang trí — chiều cao, icon, nội dung bên trong, viền. 06 và 09 đổi cấu trúc: một cái lồng bên trong cái kia, hoặc cả khối biến thành menu sidebar. 07 quay lại vấn đề gốc — trình duyệt cũ xử lý sao khi chưa hỗ trợ calc-size() — còn 08 thêm một chút vật lý giả vào lúc mở. Ở lưới phía trên, mỗi ô tự mở ra rồi đóng lại theo một vòng lặp cố định; bạn chỉ xem chuyển động chạy, chưa cần bấm gì cả.
01Chuyển height:auto với interpolate-size
Mục đang đóng cao 0, và khi bạn mở nó, interpolate-size: allow-keywords cho phép height chuyển thẳng từ 0 sang calc-size(auto, size) bằng một transition CSS bình thường, không cần đo chiều cao bằng JavaScript trước khi chạy animation. Dùng cho danh sách FAQ, chi tiết điều khoản, hoặc phần hướng dẫn trong form, những nơi bạn không biết trước nội dung dài bao nhiêu.
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height $duration $easing;
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel { height: auto; }
}
02Chỉ mở một mục nhờ thuộc tính name
Cách gọn nhất để chỉ giữ một mục accordion mở tại một thời điểm là gom nhiều thẻ <details> vào cùng một name. Trình duyệt tự đóng mục cũ khi bạn mở mục mới mà không cần một dòng JavaScript nào (tính đến 9/2026 chạy trên Chrome 120+, Safari 17.2+, Firefox 130+ — xem name trên MDN). Bản demo dưới vẫn dùng cùng cơ chế calc-size() cho chiều cao, chỉ thêm một lớp JS mỏng đóng mục còn lại khi bạn mở mục này, vì cần giữ đúng nhịp lặp 2 giây lúc xem trước trong lưới.
header.addEventListener('click', function () {
panel.classList.remove('is-demo-a', 'is-demo-b');
var willOpen = !acc.classList.contains('is-open');
accs.forEach(function (other) {
other.classList.remove('is-open');
other.querySelector('.accordion__header')
.setAttribute('aria-expanded', 'false');
});
if (willOpen) {
acc.classList.add('is-open');
header.setAttribute('aria-expanded', 'true');
}
});
03Xoay icon mũi tên
Chevron ở góc phải header xoay từ 45 độ sang -135 độ khi mục mở ra, cùng nhịp với panel, nên bạn biết trạng thái mà không cần đọc chữ. Dùng cho header accordion và menu dropdown, nơi cần một tín hiệu thị giác nhanh hơn màu chữ.
.accordion__chevron {
flex: none;
width: 9px;
height: 9px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
}
.accordion__chevron.is-demo {
animation: chevron-loop $dur-loop $easing infinite;
}
@keyframes chevron-loop {
0%, 8% { transform: rotate(45deg); }
42%, 58% { transform: rotate(-135deg); }
92%, 100% { transform: rotate(45deg); }
}
04Nội dung fade + trượt
Ngay khi panel mở ra, đoạn nội dung bên trong fade in trong lúc trượt xuống 8px từ phía trên, nên nội dung như rơi vào đúng chỗ thay vì bật ra đột ngột. Hợp cho mô tả chi tiết sản phẩm hoặc phần mở rộng tùy chọn.
.accordion__body.is-demo {
animation: content-loop $dur-loop $easing infinite;
}
@keyframes content-loop {
0%, 8% { opacity: 0; transform: translateY(-8px); }
30%, 70% { opacity: 1; transform: translateY(0); }
92%, 100% { opacity: 0; transform: translateY(-8px); }
}
05Viền nhấn mạnh mục đang mở
Trong lúc mục đang mở, viền trái dày 3px và nền của cả khối accordion đổi màu theo đúng nhịp với panel, giúp mục đang chọn nổi bật giữa những mục còn đóng. Hợp cho danh sách cài đặt hoặc bất cứ chỗ nào cần đánh dấu section đang xem.
.accordion {
border-left: 3px solid transparent;
}
.accordion.is-demo-border {
animation: border-loop $dur-loop $easing infinite;
}
@keyframes border-loop {
0%, 8% { border-left-color: transparent; background: $subject-cream; }
42%, 58% { border-left-color: $color; background: $subject-cream; }
92%, 100% { border-left-color: transparent; background: $subject-cream; }
}
06Accordion lồng nhau
Một accordion nhỏ hơn nằm ngay trong nội dung của mục cha, và nó tự mở đóng độc lập với mục cha nhờ lặp lại đúng cấu trúc .accordion ở cấp trong, chỉ đổi tên biến thể. Hợp cho FAQ nhiều cấp hoặc menu dạng cây danh mục.
.accordion--inner {
margin: 4px clamp(14px, 4vw, 20px) 12px;
background: rgba(255, 255, 255, .08);
border-radius: $r-control;
}
.accordion__panel--inner {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion__panel--inner.is-demo-inner {
animation: accordion-loop-inner $dur-loop $easing infinite;
}
@keyframes accordion-loop-inner {
0%, 20% { height: 0; }
32%, 46% { height: calc-size(auto, size); }
58%, 100% { height: 0; }
}
07Fallback calc-size()
Rule dùng calc-size(auto, size) chạy trước, nhưng nếu trình duyệt chưa hiểu nó, khối @supports not phía dưới thay bằng một chiều cao cố định theo pixel ở đúng cùng mốc phần trăm của vòng lặp, nên chuyển động trông gần như giống hệt dù công nghệ khác nhau. Hợp cho form cần chạy ổn định trên trình duyệt cũ.
.accordion__panel {
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.accordion.is-open .accordion__panel {
height: calc-size(auto, size);
transition: height $duration $easing;
}
@supports not (height: calc-size(auto, size)) {
.accordion.is-open .accordion__panel { height: auto; }
}
.accordion__panel.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
@supports not (height: calc-size(auto, size)) {
@keyframes accordion-loop {
0%, 8% { height: 0; }
42%, 58% { height: 220px; }
92%, 100% { height: 0; }
}
}
08Mở với hiệu ứng đàn hồi
Khi panel mở, nội dung bên trong phóng to nhẹ lên 106% rồi co lại đúng 100%, tạo một nhịp nảy trước khi ổn định mà không cần bất kỳ thư viện spring animation nào, chỉ CSS thuần. Hợp cho accordion khuyến mãi hoặc card onboarding cần một điểm nhấn lúc mở ra.
.accordion__body.is-demo {
transform-origin: top center;
animation: bounce-loop $dur-loop $easing infinite;
}
@keyframes bounce-loop {
0%, 8% { transform: scale(1); opacity: 0; }
36% { transform: scale(1.06); opacity: 1; }
50%, 58% { transform: scale(1); opacity: 1; }
92%, 100% { transform: scale(1); opacity: 0; }
}
09Tái dùng làm menu sidebar
Cùng cấu trúc header rồi panel cao 0 chuyển sang calc-size(auto, size) đó, khi đặt trong một <nav> sidebar sẽ trở thành mục menu thu gọn cho các mục con, không cần viết lại animation riêng. Hợp cho sidebar dashboard hoặc mục lục tài liệu dài.
.sidebar__list {
list-style: none;
margin: 0;
padding: 0 clamp(14px, 4vw, 18px) 10px clamp(26px, 8vw, 32px);
height: 0;
overflow: hidden;
interpolate-size: allow-keywords;
}
.sidebar__list.is-demo {
animation: accordion-loop $dur-loop $easing infinite;
}
Chỗ dễ hỏng ở đâu? — cái bẫy
Cả chín demo đều dựa vào interpolate-size và calc-size() để height chuyển từ 0 sang auto bằng một transition CSS thuần, và đây chính là chỗ dễ hỏng nhất: tính đến 9/2026, cặp thuộc tính này mới chạy ổn định trên Chrome bản 129 trở lên, xem bảng hỗ trợ ở calc-size() trên MDN. Vì vậy mỗi mục đều đi kèm một khối @supports not (height: calc-size(auto, size)) trả về chiều cao cố định theo pixel ở đúng mốc phần trăm cũ, để trình duyệt chưa hỗ trợ vẫn mở đóng được, chỉ bớt mượt một chút thôi.
Cái bẫy thứ hai nằm ngay ở lưới xem trước phía trên: khung đó là một iframe cố định 480×300, nên mỗi ô tự chuyển sang một vòng lặp .is-demo dài 2 giây thay vì chờ bạn bấm thật. Bạn đang xem chuyển động lặp lại ở đây thôi, chưa phải tương tác thật; muốn tự bấm mở đóng và nghe đúng độ trễ 300ms hay 600ms như chip ghi, hãy mở riêng file vanilla trong tab của bạn.
Nếu bạn không muốn đọc thêm lý thuyết mà chỉ cần mở file ra xem ngay, mật khẩu của zip là 34fn5mpm, gõ đúng ký tự là vào được.
Khả năng tiếp cận
Chín demo đều tắt animation khi prefers-reduced-motion: reduce, nhưng trạng thái mở không biến mất — panel chỉ nhảy thẳng tới chiều cao cuối cùng thay vì chuyển động dần, xem chi tiết mức hỗ trợ ở prefers-reduced-motion trên MDN. Mục 03 giữ nguyên chevron ở góc đã xoay xong, mục 04 và 08 bỏ hẳn bước fade/bounce và hiện luôn nội dung ở trạng thái cuối, còn mục 05 vẫn giữ viền và nền đã đổi màu vì đó là thông tin trạng thái, không phải trang trí đơn thuần.
@media (prefers-reduced-motion: reduce) {
.accordion__panel,
.accordion__chevron,
.accordion__body,
.accordion.is-open .accordion__panel {
animation: none !important;
transition: none !important;
}
.accordion__panel.is-demo { height: auto; }
}
Bạn có thể xem các hiệu ứng CSS khác trong chuyên mục CSS, còn trang giới thiệu giải thích rõ site này bán gì và vì sao file lại có mật khẩu.
FAQ
Css accordion animation có cần JavaScript không?
Không bắt buộc cho phần chuyển động. Cả chín demo đều chạy chiều cao bằng CSS thuần qua interpolate-size và calc-size(); JavaScript chỉ dùng để thêm hoặc bỏ class is-open khi bạn click. Thư mục react/ trong zip bọc đúng cùng đoạn SCSS vào component, không đổi engine chuyển động.
interpolate-size và calc-size() khác gì cách đo chiều cao bằng JavaScript kiểu cũ?
Cách cũ phải đọc scrollHeight bằng JS rồi gán vào style trước khi animate. interpolate-size: allow-keywords báo cho engine CSS biết auto là một giá trị có thể nội suy được, nên calc-size(auto, size) chuyển thẳng từ 0 sang chiều cao thật mà bạn không cần đo trước bằng tay.
Có thể chỉ giữ một mục accordion mở tại một thời điểm mà không dùng JavaScript không?
Có, bằng cách gom các thẻ <details> lại cùng một name, trình duyệt tự đóng mục cũ khi bạn mở mục mới. Mục 02 ở trên dùng đúng cơ chế đóng loại trừ đó, chỉ thêm JS để giữ đúng nhịp lặp 2 giây cho phần xem trước trong lưới.