GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

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

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

Bạn thấy gì

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.

Chuyển height:auto với interpolate-size
Mở trong tab mới

01 · Chuyển height:auto với interpolate-size — Diện tích đo được 11.691% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

300ms느리게 600msallow-keywords

Dùng ở đâu

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

Chuyện gì diễn ra trên màn hình

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).

Chỉ mở một mục nhờ thuộc tính name
Mở trong tab mới

02 · Chỉ mở một mục nhờ thuộc tính name — Diện tích đo được 32.087% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

name 속성닫힘 300ms라디오형

Chỗ dùng

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

Chuyển động thế nào

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ữ.

Xoay icon mũi tên
Mở trong tab mới

03 · Xoay icon mũi tên — Diện tích đo được 18.795% · khung hình chuyển động 19/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

180deg머리 오른쪽색 변화 추가

Dùng khi nào

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

Bạn thấy gì

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.

Nội dung fade + trượt
Mở trong tab mới

04 · Nội dung fade + trượt — Diện tích đo được 18.717% · khung hình chuyển động 19/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

8px200ms늦게 시작

Dùng ở đâu

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ở

Chuyện gì diễn ra trên màn hình

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.

Viền nhấn mạnh mục đang mở
Mở trong tab mới

05 · Viền nhấn mạnh mục đang mở — Diện tích đo được 12.473% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

3px배경 포함색만

Chỗ dù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

Chuyển động thế nào

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ể.

Accordion lồng nhau
Mở trong tab mới

06 · Accordion lồng nhau — Diện tích đo được 37.072% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

들여쓰기 16px2단계동시 허용

Dùng khi nào

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()

Bạn thấy gì

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.

Fallback calc-size()
Mở trong tab mới

07 · Fallback calc-size() — Diện tích đo được 18.438% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@supportsheight: auto 폴백auto 보간

Dùng ở đâu

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

Chuyện gì diễn ra trên màn hình

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.

Mở với hiệu ứng đàn hồi
Mở trong tab mới

08 · Mở với hiệu ứng đàn hồi — Diện tích đo được 19.022% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

overshoot 6%cubic-bezier닫힘은 즉시

Chỗ dùng

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

Chuyển động thế nào

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.

Tái dùng làm menu sidebar
Mở trong tab mới

09 · Tái dùng làm menu sidebar — Diện tích đo được 20.065% · khung hình chuyển động 18/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

들여쓰기 12px아이콘 없이항상 하나 열림

Dùng khi nào

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

interpolate-size và calc-size

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.

Lưới xem trước và vòng lặp is-demo

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

Chiều cao cuối khi giảm chuyển động

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.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.