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

9 mẫu List UI: accordion list css và checklist

Accordion list css là kiểu danh sách mà khi bạn bấm vào một dòng, chiều cao của nó tự giãn ra để lộ nội dung chi tiết — chín mẫu chạy thật dưới đây sẽ cho bạn

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín mục dưới đây không xếp theo độ phổ biến mà theo đúng trình tự mà một dòng danh sách thực sự trải qua. Bắt đầu từ thao tác cơ bản nhất — mở một dòng để xem chi tiết (01) — rồi đến nhóm các dòng lại để cuộn dễ hơn (02), chuyển hẳn sang màn hình khác (03). Tiếp theo là khi cần tải thêm dữ liệu (04), giấu bớt thao tác ít dùng (05), đổi thứ tự (06), sửa giá trị ngay tại chỗ (07), ghi lại việc đã xảy ra (08), và cuối cùng là đánh dấu hoàn thành (09).

01Bung chi tiết trong dòng

Bạn thấy gì

Bấm vào một dòng lịch sử đơn hàng sẽ xoay mũi tên 90 độ và mở nội dung chi tiết bên dưới.

Bung chi tiết trong dòng
Mở trong tab mới

01 · Bung chi tiết trong dòng — Diện tích đo được 19.034% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

grid-template-rows 0fr→1fraria-expandedcaret rotate transform

Dùng ở đâu

.rowx-item__body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .3s cubic-bezier(.2,.8,.2,1);
}
.rowx-item.is-open .rowx-item__body { grid-template-rows: 1fr; }
.rowx-item__body-inner { overflow: hidden; }

height: auto không animate được, nhưng grid-template-rows nội suy mượt từ 0fr sang 1fr, nên dòng vẫn mở êm dù không biết trước chiều cao nội dung.

02Header nhóm cố định

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

Trong danh bạ được nhóm theo chữ cái đầu, header của nhóm đang cuộn tới sẽ dính trên cùng cho đến khi nhóm kế tiếp đẩy nó lên.

Header nhóm cố định
Mở trong tab mới

02 · Header nhóm cố định — Diện tích đo được 25.533% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

position:sticky top:0그룹별 wrapper파스텔 톤

Chỗ dùng

.grp-group__header {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #ff9ebb;
}

position: sticky chỉ dính trong phạm vi chiều cao của phần tử cha, nên mỗi nhóm cần một khung bọc riêng để nhóm sau đẩy được header của nhóm trước lên.

03Chuyển slide từ list sang chi tiết

Chuyển động thế nào

Bấm vào một mục sẽ trượt panel danh sách sang trái trong khi panel chi tiết trượt vào từ phải — kiểu chuyển màn hay gặp trong cài đặt.

Chuyển slide từ list sang chi tiết
Mở trong tab mới

03 · Chuyển slide từ list sang chi tiết — Diện tích đo được 6.846% · 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

두 패널 transform:translateXoverflow:hidden 컨테이너history 뒤로가기 대응

Dùng khi nào

.ltd-track {
  display: flex;
  width: 200%;
  transition: transform .3s cubic-bezier(.2,.8,.2,1);
}
.ltd-viewport.is-open .ltd-track { transform: translateX(-50%); }

Danh sách và chi tiết nằm cạnh nhau trong một track duy nhất, nên chỉ cần một transform để đẩy cả hai thay vì animate riêng từng panel.

04Nút tải thêm danh sách

Bạn thấy gì

Bấm tải thêm sẽ quay spinner một chút, rồi ba dòng mới lần lượt trượt lên chứ không hiện ra cùng lúc.

Nút tải thêm danh sách
Mở trong tab mới

04 · Nút tải thêm danh sách — Diện tích đo được 43.192% · khung hình chuyển động 13/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

stagger translateY+opacity버튼 로딩 상태 토글신뢰감 있는 밝은 톤

Dùng ở đâu

btn.addEventListener('click', () => {
  list.classList.add('is-loading');
  setTimeout(() => {
    list.classList.remove('is-loading');
    list.classList.add('has-loaded');
  }, 500);
});

Ba dòng mới bắt đầu ở trạng thái max-height: 0, mỗi dòng có transition-delay khác nhau một chút để chúng hiện lần lượt thay vì cùng lúc.

05Menu hành động khi nhấn giữ

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

Nhấn giữ một dòng nửa giây sẽ bật lên menu biểu tượng yêu thích/lưu trữ/xóa với hiệu ứng nảy.

Menu hành động khi nhấn giữ
Mở trong tab mới

05 · Menu hành động khi nhấn giữ — Diện tích đo được 2.742% · 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

pointerdown 타이머 500msscale bounce keyframes키보드용 더보기 버튼 대체

Chỗ dùng

row.addEventListener('pointerdown', (e) => {
  if (e.target.closest('button')) return;
  timer = setTimeout(openMenu, 500);
});
['pointerup', 'pointerleave'].forEach((ev) =>
  row.addEventListener(ev, () => clearTimeout(timer))
);

Thả tay sớm sẽ hủy bộ đếm giờ ngay, nên chạm nhẹ để cuộn không vô tình mở menu. Nút "···" luôn hiển thị cũng mở cùng menu đó, dành cho những ai không nhấn giữ được.

06Sắp xếp lại có vạch báo vị trí thả

Chuyển động thế nào

Kéo tay cầm sáu chấm sẽ hiện một vạch trống màu xanh báo trước vị trí sẽ thả, thay vì chỉ đẩy các dòng khác sang một bên.

Sắp xếp lại có vạch báo vị trí thả
Mở trong tab mới

06 · Sắp xếp lại có vạch báo vị trí thả — Diện tích đo được 19.851% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

drop-indicator 엘리먼트pointermove 중간 인덱스 계산화살표 키보드 이동

Dùng khi nào

handle.addEventListener('keydown', (e) => {
  if (!e.altKey || (e.key !== 'ArrowUp' && e.key !== 'ArrowDown')) return;
  e.preventDefault();
  const targetIdx = order().indexOf(item) + (e.key === 'ArrowUp' ? -1 : 1);
  list.insertBefore(item, order()[targetIdx]);
});

Ai không kéo chuột được vẫn có thể focus vào tay cầm rồi dùng Alt+phím mũi tên để làm được điều tương tự — đó là điều khiến danh sách sắp xếp lại thực sự dùng được cho mọi người.

07Sửa trực tiếp trong danh sách

Bạn thấy gì

Bấm vào ô tên, ô đó sẽ biến ngay thành ô nhập liệu; nhấn Enter hoặc bấm ra ngoài để lưu, biểu tượng bút chì đổi thành dấu tick.

Sửa trực tiếp trong danh sách
Mở trong tab mới

07 · Sửa trực tiếp trong danh sách — Diện tích đo được 6.801% · khung hình chuyển động 12/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

contenteditable 대신 input 치환Escape 로 되돌리기신뢰감 있는 밝은 SaaS 톤

Dùng ở đâu

input.addEventListener('click', () => { if (input.readOnly) startEdit(); });
input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') save();
  else if (e.key === 'Escape') cancel();
});
input.addEventListener('blur', () => { if (!input.readOnly) save(); });

Dùng <input readonly> bình thường thay vì contenteditable nghĩa là việc kiểm tra giá trị và hủy bằng Escape đã có sẵn nhờ hành vi mặc định của input, nhờ vậy code ngắn hơn hẳn.

08Dòng thời gian hoạt động

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

Các chấm biểu tượng nằm dọc theo đường kẻ đứng lần lượt mờ dần hiện lên từ trên xuống dưới.

Dòng thời gian hoạt động
Mở trong tab mới

08 · Dòng thời gian hoạt động — Diện tích đo được 6.644% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

stagger fade+translateY타임라인 세로선 + 아이콘 점파스텔 톤

Chỗ dùng

.tl-item {
  opacity: 0;
  transform: translateY(10px);
  animation: tl-in .6s ease-out forwards;
  animation-delay: calc(var(--n) * 140ms);
}
@keyframes tl-in { to { opacity: 1; transform: translateY(0); } }

Truyền số thứ tự của mỗi mục qua biến --n rồi tính độ trễ bằng calc() giúp bao nhiêu mục cũng hiện lần lượt mà không cần vòng lặp trong chính animation.

09Checklist gạch ngang khi hoàn thành

Chuyển động thế nào

Bấm checkbox sẽ tô đầy màu và bật ra dấu tick, chữ bị gạch ngang từ trái sang phải rồi mờ dần.

Checklist gạch ngang khi hoàn thành
Mở trong tab mới

09 · Checklist gạch ngang khi hoàn thành — Diện tích đo được 0.621% · khung hình chuyển động 13/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scale(1.15) 체크scaleX 취소선캔디 파스텔 톤

Dùng khi nào

.ck-item__text::after {
  content: "";
  position: absolute; inset: 50% 0 auto 0; height: 2px;
  background: #ff9ebb;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .3s;
}
.ck-item.is-checked .ck-item__text::after { transform: scaleX(1); }

text-decoration-line không animate mượt trên mọi trình duyệt, nên thay vào đó dùng một đường gạch giả co giãn từ scaleX(0) sang scaleX(1). Cố định transform-origin: left là lý do đường kẻ vẽ từ bên trái.

Accordion list dễ vỡ ở đâu

Animate grid-template-rows tới auto

Cái bẫy thật sự ở mục 01 là thử animate grid-template-rows thẳng tới auto — auto không phải giá trị đầu hay cuối mà trình duyệt nội suy được, nên dòng bật mở tức thì thay vì giãn êm. Phải animate giữa hai giá trị tỷ lệ lưới như 0fr và 1fr. Cái thứ hai gặp ở mục 04: giấu các dòng mới bằng display: none rồi mới đổi class khiến trình duyệt hoàn tất thay đổi layout trước khi kịp chuyển tiếp, nên dòng hiện ra đột ngột không animate — phải bắt đầu từ max-height: 0, vốn vẫn được render nhưng kích thước bằng 0, animation mới hiện được. Mật khẩu giải nén chín file này là pw5ht9pc, giải nén xong bạn có thể so sánh trực tiếp hai bản vanilla và react của cả chín mục.

Các chủ đề list và table khác nằm ở danh mục UI thực chiến, chín mẫu khác về vuốt, sắp xếp và phân trang nằm ở 9 mẫu List UI: swipe to delete css và phân trang, còn trang này là gì thì xem ở trang giới thiệu. Cách grid-template-rows nội suy đơn vị fr được ghi ở trang grid-template-rows của MDN, phạm vi hoạt động của position: sticky ở trang position của MDN.

Khả năng tiếp cận — reduced-motion tắt gì ở từng mục

Trạng thái cuối của từng demo

Khi bật giảm chuyển động trong hệ thống, cả chín demo đều tắt transition và animation, nhảy thẳng đến trạng thái cuối. Các mục như 01, 03 nơi chiều cao hay vị trí thay đổi sẽ chuyển thẳng đến trạng thái mở/đã chuyển; các mục như 05, 09 nơi hiệu ứng nảy là trọng tâm sẽ mất hiệu ứng đó nhưng kết quả (menu mở, checkbox tick) vẫn còn nguyên. Mục 06 sắp xếp lại và 07 sửa trực tiếp vốn đã thao tác được hoàn toàn bằng bàn phím nên không đổi gì cả.

FAQ

Có dùng thẳng thẻ <details> của HTML thay vì tự làm được không?

<details>/<summary> cho sẵn việc mở/đóng, nhưng animation chiều cao vẫn phải tự thêm và giao diện mặc định khác nhau tùy trình duyệt — vì vậy chín demo này dùng button cùng grid-template-rows để kiểm soát chi tiết hơn. Nếu ưu tiên khả năng tiếp cận, gắn style lên trên <details> cũng là một lựa chọn hợp lý.

Danh sách có vài trăm mục thì các mẫu này còn dùng được không?

Với 01, 02, 08 — nơi mỗi dòng hiển thị đều mang animation riêng — danh sách lên tới vài trăm mục cần thêm tối ưu như virtual scroll (chỉ render phần đang hiện trên màn hình). Code trong zip được làm cho quy mô vài chục mục trên một trang.

Menu nhấn giữ có hoạt động giống vậy trên di động không?

Có — sự kiện pointerdown/pointerup xử lý chuột, cảm ứng và bút bằng cùng một đoạn code. Tuy nhiên trình duyệt di động có thể bật menu ngữ cảnh hoặc chọn văn bản riêng khi nhấn giữ, nên demo dùng touch-action: manipulation để tránh xung đột đó.

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.