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.
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à:
- Xác nhận action: nhấn nút → có gì đó xảy ra (tránh cảm giác freeze)
- Spatial context: trang mới ở đâu so với trang cũ (drill-in, go back, peer navigation)
- 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 transition | Duration | Easing |
|---|---|---|
| Fade | 150–200ms | ease-out |
| Slide (small offset) | 250–350ms | cubic-bezier(0.2, 0, 0, 1) |
| Slide (full page) | 300–400ms | cubic-bezier(0.2, 0, 0, 1) |
| Shared element | 350–500ms | cubic-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.