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.

Motion Spring Easing Stagger Gesture Physics Bezier Timing

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 itemsInterval đề xuấtTổng thời gian chờ
3–560–80ms180–320ms
6–1040–60ms200–500ms
11–2020–40ms200–760ms
20+10–20msGiữ 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.