Nắm Vững View Transitions API để Tạo Trải Nghiệm Web Mượt Mà
Trong hành trình xây dựng các ứng dụng web mượt mà và phản hồi nhanh như ứng dụng native, các nhà phát triển thường đối mặt với một tình huống khó xử: làm thế nào để tạo ra các thay đổi UI liền mạch, không gây gián đoạn mà không cần dùng đến các framework định tuyến client-side phức tạp hoặc đánh đổi hiệu suất cảm nhận. Các điều hướng trang truyền thống thường gây giật cục, dẫn đến một khoảng trắng ngắn hoặc việc vẽ lại đột ngột. Mặc dù Single Page Applications (SPAs) mang lại khả năng cập nhật động, chúng lại đi kèm với những phức tạp riêng. View Transitions API ra đời như một tính năng trình duyệt mạnh mẽ được thiết kế để mang lại sự mượt mà như ứng dụng native cho *bất kỳ* trang web nào, dù đó là SPA, MPA hay chỉ là một trang tĩnh có cập nhật nội dung động.
View Transitions API cho phép bạn tạo các hiệu ứng chuyển đổi động giữa các trạng thái DOM khác nhau. Thay vì cập nhật DOM trực tiếp và ngay lập tức, bạn sẽ bọc quá trình cập nhật DOM của mình trong một view transition. Trình duyệt sau đó tự động chụp "ảnh chụp nhanh" của trạng thái cũ và mới, rồi xử lý việc tạo hiệu ứng chuyển động giữa chúng. Điều này cải thiện đáng kể hiệu suất cảm nhận và trải nghiệm người dùng bằng cách biến các thay đổi đột ngột thành các chuyển động thanh lịch, có kiểm soát.
Cơ Chế Cốt Lõi: document.startViewTransition()
Về cơ bản, API này cực kỳ đơn giản. Bạn bọc logic cập nhật DOM của mình trong một lời gọi document.startViewTransition(). Hàm này nhận một callback sẽ thực hiện các thay đổi DOM thực tế.
// Ví dụ: Cập nhật nội dung khi nhấp chuột
const updateContent = async () => {
const newContent = await fetch('/api/getNewContent').then(res => res.text());
// Giả sử có một div với id 'content-area'
document.getElementById('content-area').innerHTML = newContent;
};
document.getElementById('my-button').addEventListener('click', () => {
if (!document.startViewTransition) {
// Phương án dự phòng cho các trình duyệt không hỗ trợ View Transitions
updateContent();
return;
}
document.startViewTransition(async () => {
await updateContent(); // Thực hiện cập nhật DOM bên trong quá trình chuyển đổi
});
});
Khi startViewTransition được gọi, trình duyệt sẽ chụp ảnh màn hình của trạng thái trang hiện tại. Sau đó, nó thực thi hàm callback của bạn (trong trường hợp này là updateContent()), hàm này sẽ cập nhật DOM sang trạng thái mới. Sau khi cập nhật DOM hoàn tất, trình duyệt sẽ chụp thêm một ảnh màn hình của trạng thái mới. Cuối cùng, nó sử dụng hai ảnh chụp nhanh này để tạo ra một hiệu ứng chuyển đổi. Mặc định, đây là một hiệu ứng cross-fade đơn giản, nhưng điều kỳ diệu thực sự bắt đầu khi bạn tùy chỉnh nó bằng CSS.
Tùy Chỉnh Chuyển Đổi với CSS
Sức mạnh thực sự của View Transitions nằm ở sự tích hợp với CSS. Trình duyệt tạo ra một cây pseudo-element cho quá trình chuyển đổi, cho phép bạn nhắm mục tiêu và tạo kiểu cho các phần cụ thể của hiệu ứng. Chìa khóa ở đây là thuộc tính CSS view-transition-name.
Xác Định Các Phần Tử để Chuyển Đổi: view-transition-name
Để một phần tử "tham gia" vào một view transition, hãy gán cho nó một view-transition-name duy nhất. Điều này báo cho trình duyệt biết để theo dõi phần tử này trong quá trình cập nhật DOM và tạo hiệu ứng xuất hiện/biến mất hoặc di chuyển của nó.
/* Trong CSS của bạn */
.thumbnail {
view-transition-name: thumbnail-image;
}
.product-title {
view-transition-name: product-title-text;
}
Khi một view transition bắt đầu, các phần tử có view-transition-name sẽ được trích xuất vào các "nhóm view transition" riêng của chúng. Trình duyệt tạo ra các pseudo-element như ::view-transition-old(thumbnail-image) và ::view-transition-new(thumbnail-image) đại diện cho trạng thái của phần tử trước và sau khi cập nhật DOM. Sau đó, bạn có thể tạo hiệu ứng cho các pseudo-element này.
Tạo Kiểu Chuyển Đổi: Pseudo-elements và Animations
Các pseudo-element chính bạn sẽ tương tác là:
::view-transition: Pseudo-element gốc cho toàn bộ quá trình chuyển đổi.::view-transition-group(<name>): Bao bọc các ảnh chụp nhanh cũ và mới của một phần tử được đặt tên.::view-transition-old(<name>): Đại diện cho ảnh chụp nhanh của phần tử trước khi cập nhật DOM.::view-transition-new(<name>): Đại diện cho ảnh chụp nhanh của phần tử sau khi cập nhật DOM.
Giả sử chúng ta muốn một hình ảnh được chia sẻ mở rộng mượt mà từ ảnh thumbnail thành ảnh kích thước đầy đủ, và tiêu đề trượt vào.
/* common.css */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
/* CSS riêng cho trang để chuyển đổi */
::view-transition-old(product-image) {
animation: 200ms cubic-bezier(0.4, 0, 0.2, 1) fade-out;
/* Đảm bảo nó không di chuyển */
animation-fill-mode: forwards;
}
::view-transition-new(product-image) {
animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) fade-in;
/* Đảm bảo nó không di chuyển */
animation-fill-mode: forwards;
}
::view-transition-old(product-title-text) {
animation: 150ms cubic-bezier(0.4, 0, 0.2, 1) fade-out;
}
::view-transition-new(product-title-text) {
animation: 300ms cubic-bezier(0.0, 0, 0.2, 1) slide-from-right;
}
/* Fallback toàn cục cho hiệu ứng cross-fade mặc định */
::view-transition-old(root) {
animation: 150ms ease-in fade-out;
}
::view-transition-new(root) {
animation: 200ms ease-out fade-in;
}
Tên root là đặc biệt; nó là tên mặc định được gán cho toàn bộ tài liệu. Nếu bạn không chỉ định view-transition-name cho bất kỳ phần tử nào, toàn bộ trang sẽ thực hiện hiệu ứng cross-fade sử dụng ::view-transition-old(root) và ::view-transition-new(root).
Triển Khai Thực Tế: Từ Chế Độ Xem Danh Sách đến Chế Độ Xem Chi Tiết
Hãy xem xét một kịch bản phổ biến: nhấp vào một mục trong danh sách để xem chi tiết của nó. Chúng ta muốn hình ảnh và tiêu đề chuyển đổi mượt mà.
Cấu Trúc HTML (đơn giản hóa)
<div id="product-list">
<div class="product-card" data-product-id="1">
<img src="/img/thumb-1.jpg" alt="Sản phẩm 1" class="product-image-thumb">
<h3 class="product-title-thumb">Sản phẩm 1</h3>
</div>
</div>
<div id="product-detail" style="display: none;">
<img src="/img/full-1.jpg" alt="Sản phẩm 1" class="product-image-full">
<h1 class="product-title-full">Sản phẩm 1</h1>
<p>Mô tả chi tiết cho Sản phẩm 1.</p>
</div>
Trong ví dụ này, view-transition-name sẽ được đặt động thông qua JavaScript. Điều này báo cho trình duyệt biết rằng các phần tử có cùng tên là "cùng" một phần tử trong suốt quá trình chuyển đổi. view-transition-name phải là duy nhất trên trang *tại bất kỳ thời điểm nào*, nhưng nó có thể thay đổi phần tử liên kết trong quá trình cập nhật DOM.
JavaScript cho Chuyển Đổi Từ Danh Sách sang Chi Tiết
document.querySelectorAll('.product-card').forEach(card => {
card.addEventListener('click', async (event) => {
const productId = card.dataset.productId;
const thumbnailImage = card.querySelector('.product-image-thumb');
const thumbnailTitle = card.querySelector('.product-title-thumb');
// Đặt view-transition-name động trước khi chuyển đổi bắt đầu
// Quan trọng: các tên này phải khớp với tên trên phần tử trang chi tiết
const imageTransitionName = `product-image-${productId}`;
const titleTransitionName = `product-title-${productId}`;
thumbnailImage.style.viewTransitionName = imageTransitionName;
thumbnailTitle.style.viewTransitionName = titleTransitionName;
// Xóa bất kỳ tên nào đã đặt trước đó trên các thẻ khác để đảm bảo tính duy nhất
document.querySelectorAll('[style*="view-transition-name"]').forEach(el => {
if (el !== thumbnailImage && el !== thumbnailTitle) {
el.style.viewTransitionName = '';
}
});
if (!document.startViewTransition) {
// Fallback: điều hướng trực tiếp hoặc cập nhật nội dung
loadProductDetail(productId);
return;
}
document.startViewTransition(async () => {
// Ẩn danh sách, hiển thị chi tiết và cập nhật nội dung
document.getElementById('product-list').style.display = 'none';
const detailView = document.getElementById('product-detail');
detailView.style.display = 'block';
// Cập nhật nội dung chi tiết (ví dụ: fetch và render)
await loadProductDetail(productId, imageTransitionName, titleTransitionName);
});
});
});
async function loadProductDetail(productId, imageTransitionName, titleTransitionName) {
// Mô phỏng việc lấy dữ liệu chi tiết
const detailData = await fetch(`/api/products/${productId}`).then(res => res.json());
const detailImage = document.getElementById('product-detail').querySelector('.product-image-full');
const detailTitle = document.getElementById('product-detail').querySelector('.product-title-full');
const detailDescription = document.getElementById('product-detail').querySelector('p');
detailImage.src = detailData.fullImageUrl;
detailImage.alt = detailData.title;
detailImage.style.viewTransitionName = imageTransitionName; // Đặt tên trên phần tử đích
detailTitle.textContent = detailData.title;
detailTitle.style.viewTransitionName = titleTransitionName; // Đặt tên trên phần tử đích
detailDescription.textContent = detailData.description;
}
Trong ví dụ này, view-transition-name được đặt động. Điều này rất quan trọng đối với các kịch bản mà bạn có nhiều mục và muốn một mục cụ thể chuyển đổi. Các tên phải được đặt trên cả phần tử "cũ" (thumbnail) trước khi chuyển đổi và phần tử "mới" (kích thước đầy đủ) sau khi cập nhật DOM bên trong callback startViewTransition. Đảm bảo xóa view-transition-name khỏi các phần tử khác không còn tham gia vào quá trình chuyển đổi để tránh các hiệu ứng không mong muốn.
Bài Học Sản Xuất và Đánh Đổi
- Progressive Enhancement (Nâng Cao Dần Dần): API được thiết kế với tư duy nâng cao dần dần. Luôn bao gồm một phương án dự phòng cho các trình duyệt không hỗ trợ nó (ví dụ: kiểm tra
if (!document.startViewTransition)). Điều này đảm bảo trải nghiệm chức năng cho tất cả người dùng, ngay cả khi họ không nhận được các hoạt ảnh đẹp mắt. - Hiệu Suất: Mặc dù mạnh mẽ, việc tạo hiệu ứng cho nhiều phần tử phức tạp cùng lúc có thể ảnh hưởng đến hiệu suất, đặc biệt trên các thiết bị cấu hình thấp. Hãy kiểm tra kỹ lưỡng các chuyển đổi của bạn. Giữ cho hoạt ảnh ngắn gọn và đơn giản. Trình duyệt xử lý phần lớn công việc chụp ảnh nhanh, nhưng các hoạt ảnh CSS phức tạp vẫn tiêu tốn tài nguyên.
- Tên Duy Nhất: Mỗi
view-transition-namephải là duy nhất trên trang tại bất kỳ thời điểm nào. Nếu bạn có nhiều phần tử với cùng tên, chỉ phần tử cuối cùng được tìm thấy trong DOM sẽ được chuyển đổi. Việc đặt tên động (như trong ví dụ) thường là cần thiết. - Khả Năng Tiếp Cận (Accessibility): Đảm bảo các chuyển đổi của bạn không gây mất phương hướng cho người dùng hoặc làm cho nội dung khó hiểu hơn. Đối với những người dùng thích giảm chuyển động, hãy tôn trọng truy vấn phương tiện
prefers-reduced-motiontrong các hoạt ảnh CSS của bạn.@media (prefers-reduced-motion) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; } } - Tích Hợp với Frameworks: Hầu hết các framework frontend hiện đại (React, Vue, Angular, Svelte) đều có thể tích hợp với View Transitions. Bạn thường kích hoạt
startViewTransitionxung quanh các bản cập nhật trạng thái của framework gây ra thay đổi DOM. Các thư viện như React Router có hỗ trợ thử nghiệm hoặc các gói cộng đồng giúp đơn giản hóa điều này.
Điểm Cốt Lõi
- View Transitions API cho phép các hoạt ảnh UI mượt mà, giống như ứng dụng native trong các thay đổi DOM cho cả MPA và SPA.
- Sử dụng
document.startViewTransition()để bọc logic cập nhật DOM của bạn. - Áp dụng thuộc tính CSS
view-transition-namecho các phần tử bạn muốn tạo hiệu ứng, đảm bảo các tên duy nhất cho mỗi chuyển đổi. - Tùy chỉnh chuyển đổi bằng cách sử dụng các pseudo-element CSS như
::view-transition-old()và::view-transition-new(). - Luôn triển khai nâng cao dần dần với các phương án dự phòng và xem xét khả năng tiếp cận (
prefers-reduced-motion). - Việc đặt tên động cho
view-transition-namethường được yêu cầu đối với các chuyển đổi phần tử cụ thể trong danh sách/lưới.

Hãy là người đầu tiên bình luận.
Để lại bình luận