Stagger: Làm danh sách trở nên sống động
Stagger là kỹ thuật đơn giản nhất để làm animation của một nhóm element trông có chủ ý. Thay vì tất cả xuất hiện cùng lúc (chaos) hoặc tuần tự hoàn toàn (quá chậm), stagger tạo ra một cascade có nhịp điệu.
Click Replay để xem lại cascade — 8 pill với 60ms interval
Công thức
/* CSS */
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 50ms; }
.item:nth-child(3) { animation-delay: 100ms; }
/* ... */
// JavaScript (dynamic)
items.forEach((el, i) => {
el.style.animationDelay = `${i * 50}ms`;
})
Hai biến quan trọng: base animation (mỗi element làm gì) và interval (delay giữa các element).
Chọn interval đúng
Interval quá ngắn (< 20ms): người dùng không nhận ra pattern — trông như cùng lúc.
Interval đúng (30–80ms): cảm giác cascade có nhịp điệu, não parse được sequence.
Interval quá dài (> 120ms): mỗi element trông như animation riêng lẻ — chậm và rời rạc.
| Số lượng items | Interval đề xuất | Tổng thời gian chờ |
|---|---|---|
| 3–5 | 60–80ms | 180–320ms |
| 6–10 | 40–60ms | 200–500ms |
| 11–20 | 20–40ms | 200–760ms |
| 20+ | 10–20ms | Giữ tổng < 400ms |
Quy tắc: tổng thời gian từ item đầu đến item cuối bắt đầu không nên quá 400ms. Nếu danh sách dài, giảm interval thay vì để người dùng chờ.
Easing cho stagger
Mỗi element nên dùng ease-out — nhanh vào, chậm settle. Phù hợp với pattern “xuất hiện từ không khí”.
.item {
animation: slide-up 0.4s cubic-bezier(0, 0, 0.2, 1) both;
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(16px);
}
}
Dùng both để fill-mode áp dụng trạng thái before và after animation. Không cần forwards riêng.
Tham khảo thêm các curve phù hợp trong Cubic Bezier chuyên sâu.
Direction của stagger
Top-down (mặc định): item trên xuất hiện trước. Phù hợp với list đọc theo chiều dọc.
Bottom-up: item dưới xuất hiện trước. Dùng khi content quan trọng ở dưới (ít gặp).
Center-out: item giữa xuất hiện trước, lan ra hai phía. Tạo focal point mạnh ở trung tâm.
Random: mỗi item nhận delay ngẫu nhiên trong range cho phép. Phù hợp với grid không có hierarchy rõ ràng (gallery, mosaic).
// Random stagger
items.forEach(el => {
el.style.animationDelay = `${Math.random() * 300}ms`;
})
Stagger với scroll trigger
Stagger thường kết hợp với Intersection Observer để trigger khi element vào viewport:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const items = entry.target.querySelectorAll('.item');
items.forEach((el, i) => {
el.style.animationDelay = `${i * 50}ms`;
el.classList.add('animate');
});
observer.unobserve(entry.target);
});
}, { threshold: 0.1 });
Xem thêm pattern scroll trigger trong Scroll Reveal Patterns.
Spring cho từng item
Stagger delay thường dùng với CSS animation (bezier-based). Nhưng nếu dùng JS (Framer Motion), có thể kết hợp stagger delay với spring physics cho từng element:
// Framer Motion
container.variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } }
}
item.variants = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 300, damping: 24 } }
}
Kết quả: cascade timing từ stagger, feel vật lý từ spring trong từng element. Tham khảo spring config trong Spring Parameters.
Sai lầm thường gặp
Stagger cho re-render: nếu list thay đổi nội dung (filter, sort), stagger lại toàn bộ gây choáng ngợp. Stagger nên chỉ chạy lần đầu (mount) hoặc khi scroll trigger.
Quên animation-fill-mode: both: item sẽ flash visible rồi mới bắt đầu animation vì delay chưa chạy nhưng không có initial state.
Translate quá lớn: translateY(40px) cho mỗi item tạo cảm giác “rơi từ trên cao”. 8–16px thường đủ để tạo directional cue mà không gây mất phương hướng.