Page Transition Patterns

Page transition là animation đắt nhất trong UI: nó chiếm toàn bộ viewport trong khoảng 300–500ms. Làm đúng, nó cho người dùng ngữ cảnh spatial — họ biết mình đang đi đâu trong app. Làm sai, nó chỉ là delay.

Overview
Typography
Color
Spacing

Chọn kiểu transition ở trên, rồi click nav bên trái để chuyển trang

Mục tiêu của page transition

Transition không phải để “đẹp”. Mục tiêu là:

  1. Xác nhận action: nhấn nút → có gì đó xảy ra (tránh cảm giác freeze)
  2. Spatial context: trang mới ở đâu so với trang cũ (drill-in, go back, peer navigation)
  3. Mask loading: che đi khoảng thời gian fetch data

Nếu transition không phục vụ ít nhất một trong ba mục tiêu này, cân nhắc bỏ luôn.

Pattern 1: Fade

Đơn giản nhất, phù hợp nhất cho navigation không có spatial relationship.

@keyframes page-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.page-enter {
  animation: page-fade-in 0.2s ease-out;
}

Dùng khi: tab navigation, top-level menu items, modal-to-modal. Khi hai trang không có quan hệ cha–con hoặc trước–sau.

Tránh: fade quá dài (> 300ms) — người dùng cảm nhận như lag, không phải transition.

Pattern 2: Slide (direction-aware)

Trang mới slide vào từ hướng tương ứng với action. Tạo spatial map trong đầu người dùng.

/* Drill in: new page comes from right */
.page-enter-right {
  animation: slide-from-right 0.3s cubic-bezier(0.2, 0, 0, 1);
}

/* Go back: new page comes from left */
.page-enter-left {
  animation: slide-from-left 0.3s cubic-bezier(0.2, 0, 0, 1);
}

@keyframes slide-from-right {
  from { transform: translateX(24px); opacity: 0; }
  to   { transform: translateX(0);    opacity: 1; }
}

@keyframes slide-from-left {
  from { transform: translateX(-24px); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

Lưu ý quan trọng: không slide quá xa. translateX(24px) đủ để tạo directional cue — translateX(100vw) gây chóng mặt và quá chậm.

Dùng khi: có breadcrumb hierarchy (list → detail → deeper detail), wizard/onboarding steps, back navigation.

Pattern 3: Shared element transition

Element từ trang cũ “trở thành” element trên trang mới. Tạo continuity mạnh nhất.

// View Transition API (native browser, Chrome 111+)
document.startViewTransition(() => {
  // Update DOM here
  navigateTo(newPage);
});
/* CSS để define shared element */
.card-image   { view-transition-name: hero-image; }
.detail-image { view-transition-name: hero-image; }

Browser tự interpolate vị trí và kích thước giữa hai element có cùng view-transition-name.

Dùng khi: card → detail page, thumbnail → full view, product list → product page.

Fallback: nếu browser không hỗ trợ View Transition API, chạy fade thay thế.

if (document.startViewTransition) {
  document.startViewTransition(() => navigateTo(newPage));
} else {
  navigateTo(newPage); // instant, no transition
}

Timing guidelines

Loại transitionDurationEasing
Fade150–200msease-out
Slide (small offset)250–350mscubic-bezier(0.2, 0, 0, 1)
Slide (full page)300–400mscubic-bezier(0.2, 0, 0, 1)
Shared element350–500mscubic-bezier(0.2, 0, 0, 1)

Luôn dùng ease-out (nhanh vào, chậm settle) cho transition vào. Trang đang “đến” — nhanh vào cảm giác responsive, chậm settle cảm giác controlled.

Exit (trang cũ rời đi) thường dùng ease-in hoặc đơn giản là fade-out nhanh (100ms).

Sai lầm phổ biến

Transition hai chiều cùng lúc: trang cũ slide ra trái ĐỒNG THỜI trang mới slide vào phải → quá nhiều chuyển động, não không biết focus vào đâu. Tốt hơn: trang cũ fade-out nhanh, trang mới slide vào.

Transition trên mobile quá lớn: translateX(100vw) trên màn hình nhỏ → cần > 400ms để đi hết màn hình → cảm giác chậm. Thu nhỏ xuống còn 20–30% width.

Không disable khi prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  .page-enter { animation: none; }
}

Tham khảo thêm: Cubic Bezier chuyên sâu, Ease vs Spring, Scroll Reveal Patterns.