Hiệu ứng chuyển đổi chế độ xem cùng tài liệu cho các ứng dụng trang đơn

Khi một lượt chuyển đổi thành phần hiển thị chạy trên một tài liệu, hiệu ứng này được gọi là chuyển đổi thành phần hiển thị cùng một tài liệu. Điều này thường xảy ra trong các ứng dụng trang đơn (SPA) nơi JavaScript được sử dụng để cập nhật DOM. Kể từ Chrome 111, Chrome hỗ trợ chuyển đổi chế độ xem cùng tài liệu.

Để kích hoạt hiệu ứng chuyển đổi chế độ xem cùng tài liệu, hãy gọi document.startViewTransition:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

Khi được gọi, trình duyệt sẽ tự động chụp ảnh nhanh tất cả các phần tử có thuộc tính CSS view-transition-name được khai báo trên đó.

Sau đó, hàm này thực thi lệnh gọi lại đã truyền vào để cập nhật DOM, sau đó chụp nhanh trạng thái mới.

Sau đó, các ảnh chụp nhanh này được sắp xếp trong một cây phần tử giả và tạo ảnh động bằng cách sử dụng sức mạnh của ảnh động CSS. Cặp ảnh chụp nhanh từ trạng thái cũ và trạng thái mới chuyển đổi suôn sẻ từ vị trí và kích thước cũ sang vị trí mới, trong khi nội dung của chúng biến mất. Nếu muốn, bạn có thể sử dụng CSS để tuỳ chỉnh ảnh động.


Chuyển đổi mặc định: Mờ dần

Hoạt động chuyển đổi khung hiển thị mặc định là chuyển đổi mờ dần, vì vậy, nó đóng vai trò như một phần giới thiệu tuyệt vời về API:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

Trong trường hợp updateTheDOMSomehow thay đổi DOM sang trạng thái mới. Bạn có thể thực hiện việc này theo cách bạn muốn. Ví dụ: bạn có thể thêm hoặc xoá các thành phần, thay đổi tên lớp hoặc thay đổi kiểu.

Và cứ thế, các trang mờ dần:

Mờ dần theo mặc định. Bản minh hoạ tối thiểu. Nguồn.

Được rồi, hiệu ứng chuyển màu không thật ấn tượng. Rất may là bạn có thể tuỳ chỉnh hiệu ứng chuyển đổi, nhưng trước tiên, bạn cần hiểu cách hoạt động của hiệu ứng chuyển cảnh chuyển đổi cơ bản này.


Cách hoạt động của những quá trình chuyển đổi này

Hãy cập nhật mã mẫu trước đó.

document.startViewTransition(() => updateTheDOMSomehow(data));

Khi .startViewTransition() được gọi, API sẽ ghi lại trạng thái hiện tại của trang. Điều này bao gồm cả việc chụp ảnh nhanh.

Sau khi hoàn tất, lệnh gọi lại truyền đến .startViewTransition() sẽ được gọi. Đó là nơi DOM bị thay đổi. Sau đó, API này sẽ ghi lại trạng thái mới của trang.

Sau khi ghi nhận được trạng thái mới, API sẽ tạo một cây phần tử giả như sau:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

::view-transition nằm trong một lớp phủ, trên mọi nội dung khác trên trang. Điều này rất hữu ích nếu bạn muốn đặt màu nền cho hiệu ứng chuyển đổi.

::view-transition-old(root) là ảnh chụp màn hình của chế độ xem cũ, còn ::view-transition-new(root) là ảnh biểu thị trực tiếp của chế độ xem mới. Cả hai đều hiển thị dưới dạng "nội dung được thay thế" của CSS (như <img>).

Khung hiển thị cũ tạo ảnh động từ opacity: 1 sang opacity: 0, trong khi khung hiển thị mới tạo hiệu ứng động từ opacity: 0 sang opacity: 1, tạo ra hiệu ứng chuyển đổi mờ dần.

Tất cả hoạt ảnh được thực hiện bằng cách sử dụng ảnh động CSS, vì vậy bạn có thể tuỳ chỉnh chúng bằng CSS.

Tuỳ chỉnh hiệu ứng chuyển đổi

Bạn có thể nhắm mục tiêu tất cả các phần tử giả chuyển đổi khung hiển thị có thể được nhắm mục tiêu bằng CSS và vì các ảnh động được xác định bằng CSS, bạn có thể sửa đổi chúng bằng cách sử dụng các thuộc tính ảnh động CSS hiện có. Ví dụ:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

Với một thay đổi đó, độ mờ giờ đây thực sự chậm:

Mờ dần. Bản minh hoạ tối thiểu. Nguồn.

Được rồi, vẫn chưa ấn tượng lắm. Thay vào đó, mã sau đây sẽ triển khai hiệu ứng chuyển đổi trục dùng chung của Material Design:

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

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

Và đây là kết quả:

Chuyển đổi trục dùng chung. Bản minh hoạ tối thiểu. Nguồn.

Chuyển đổi nhiều thành phần

Trong bản minh hoạ trước, toàn bộ trang tham gia vào quá trình chuyển đổi trục chung. Cách này phù hợp với hầu hết trang, nhưng có vẻ không phù hợp với tiêu đề vì nó trượt ra chỉ để trượt vào lại.

Để tránh tình trạng này, bạn có thể trích xuất tiêu đề từ phần còn lại của trang để tạo ảnh động riêng biệt. Bạn có thể thực hiện việc này bằng cách gán view-transition-name cho phần tử.

.main-header {
  view-transition-name: main-header;
}

Giá trị của view-transition-name có thể là bất kỳ giá trị nào bạn muốn (ngoại trừ none, có nghĩa là không có tên chuyển đổi). Thuộc tính này dùng để xác định riêng biệt phần tử trong quá trình chuyển đổi.

Và kết quả của việc đó:

Chuyển đổi trục chung với tiêu đề cố định. Bản minh hoạ tối thiểu. Nguồn.

Giờ đây, tiêu đề vẫn giữ nguyên và sẽ mờ dần.

Khai báo CSS đó đã khiến cây phần tử giả thay đổi:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

Hiện có hai nhóm chuyển đổi. Một cho tiêu đề và một cho phần còn lại. Bạn có thể nhắm mục tiêu những quảng cáo này một cách độc lập với CSS và có các cách chuyển đổi khác nhau. Mặc dù, trong trường hợp này, main-header vẫn giữ nguyên với hiệu ứng chuyển đổi mặc định, đó là chuyển đổi mờ dần.

Chà, hiệu ứng chuyển đổi mặc định không chỉ là hiệu ứng mờ dần, ::view-transition-group còn chuyển đổi:

  • Định vị và biến đổi (sử dụng transform)
  • Chiều rộng
  • Chiều cao

Cho đến giờ điều đó vẫn chưa quan trọng, vì tiêu đề có cùng kích thước và định vị cả hai bên của thay đổi DOM. Tuy nhiên, bạn cũng có thể trích xuất văn bản trong tiêu đề:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content được dùng để phần tử thể hiện kích thước của văn bản, thay vì kéo giãn đến chiều rộng còn lại. Nếu không, mũi tên quay lại sẽ giảm kích thước của thành phần văn bản tiêu đề, thay vì cùng kích thước ở cả hai trang.

Bây giờ, chúng ta có 3 phần để khám phá:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

Nhưng xin nhắc lại, tôi dùng chế độ mặc định:

Văn bản tiêu đề trượt. Bản minh hoạ tối thiểu. Nguồn.

Bây giờ, văn bản tiêu đề có trang trượt nhỏ thoải mái để nhường chỗ cho nút quay lại.


Tạo ảnh động cho nhiều phần tử giả theo cùng cách bằng view-transition-class

Hỗ trợ trình duyệt

  • 125
  • 125
  • x
  • x

Giả sử bạn có chuyển đổi chế độ xem với một loạt thẻ nhưng cũng có tiêu đề trên trang. Để tạo ảnh động cho tất cả các thẻ ngoại trừ tiêu đề, bạn phải viết một bộ chọn nhắm mục tiêu đến từng thẻ riêng lẻ.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

Bạn có 20 phần tử? Bạn cần viết 20 bộ chọn như vậy. Bạn muốn thêm một phần tử mới? Sau đó, bạn cũng cần phát triển bộ chọn áp dụng các kiểu ảnh động. Không thể mở rộng chính xác.

Bạn có thể sử dụng view-transition-class trong các phần tử giả chuyển đổi khung hiển thị để áp dụng cùng một quy tắc kiểu.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

Ví dụ về thẻ sau đây tận dụng đoạn mã CSS trước đó. Tất cả các thẻ (bao gồm cả các thẻ mới thêm) đều được áp dụng cùng một thời gian bằng một bộ chọn: html::view-transition-group(.card).

Ghi lại bản minh hoạ về Thẻ. Khi dùng view-transition-class, thuộc tính này sẽ áp dụng cùng một animation-timing-function cho tất cả các thẻ, ngoại trừ những thẻ đã thêm hoặc bị xoá.

Chuyển đổi gỡ lỗi

Vì hiệu ứng chuyển đổi khung hiển thị được tạo dựa trên ảnh động CSS, nên bảng điều khiển Ảnh động trong Công cụ của Chrome là phù hợp để gỡ lỗi chuyển đổi.

Trong bảng điều khiển Animations, bạn có thể tạm dừng ảnh động tiếp theo, sau đó tua đi tua lại qua ảnh động. Trong quá trình này, bạn có thể tìm thấy các phần tử giả chuyển đổi trong bảng điều khiển Phần tử.

Gỡ lỗi chuyển đổi chế độ xem bằng Công cụ của Chrome cho nhà phát triển.

Các phần tử chuyển tiếp không nhất thiết phải là phần tử DOM

Tính đến thời điểm này, chúng ta đã sử dụng view-transition-name để tạo các phần tử chuyển đổi riêng biệt cho tiêu đề và văn bản trong tiêu đề. Về mặt lý thuyết, các phần tử này là cùng một phần tử trước và sau khi thay đổi DOM, nhưng bạn có thể tạo các phần tử chuyển đổi mà không phải lúc nào cũng vậy.

Ví dụ: video nhúng chính có thể được gán view-transition-name:

.full-embed {
  view-transition-name: full-embed;
}

Sau đó, khi người dùng nhấp vào hình thu nhỏ, hình thu nhỏ đó có thể được cung cấp cùng một view-transition-name, chỉ trong khoảng thời gian chuyển đổi:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

Và kết quả:

Chuyển đổi một thành phần sang một thành phần khác. Bản minh hoạ tối thiểu. Nguồn.

Hình thu nhỏ giờ đây sẽ chuyển đổi thành hình ảnh chính. Mặc dù chúng là các phần tử khác nhau về mặt lý thuyết (và theo nghĩa đen), nhưng API chuyển đổi sẽ coi chúng là các phần tử giống nhau vì chúng dùng chung một view-transition-name.

Mã thực tế cho hiệu ứng chuyển đổi này phức tạp hơn một chút so với ví dụ trước, vì hiệu ứng này cũng xử lý việc chuyển đổi trở lại trang hình thu nhỏ. Xem nguồn để biết cách triển khai đầy đủ.


Hiệu ứng chuyển đổi vào và thoát tuỳ chỉnh

Hãy xem ví dụ sau:

Nhập và thoát khỏi thanh bên. Bản minh hoạ tối thiểu. Nguồn.

Thanh bên là một phần của quá trình chuyển đổi:

.sidebar {
  view-transition-name: sidebar;
}

Nhưng, không giống như tiêu đề trong ví dụ trước, thanh bên không xuất hiện trên tất cả các trang. Nếu cả hai trạng thái đều có thanh bên, thì các phần tử giả chuyển đổi sẽ có dạng như sau:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

Tuy nhiên, nếu thanh bên chỉ nằm trên trang mới, thì phần tử giả ::view-transition-old(sidebar) sẽ không xuất hiện ở đó. Vì không có hình ảnh "cũ" cho thanh bên, nên cặp hình ảnh sẽ chỉ có ::view-transition-new(sidebar). Tương tự, nếu thanh bên chỉ nằm trên trang cũ, thì cặp hình ảnh sẽ chỉ có ::view-transition-old(sidebar).

Trong bản minh hoạ trước, thanh bên chuyển đổi theo cách khác nhau, tuỳ thuộc vào việc thanh bên đang vào, thoát hay hiện diện ở cả hai trạng thái. Nó đi vào bằng cách trượt từ bên phải rồi hiện dần vào trong, nó thoát ra bằng cách trượt sang phải rồi mờ dần, và nó vẫn giữ nguyên vị trí khi xuất hiện ở cả hai trạng thái.

Để tạo hiệu ứng chuyển đổi vào và thoát cụ thể, bạn có thể sử dụng lớp giả :only-child để nhắm đến các phần tử giả cũ hoặc mới khi đó là phần tử con duy nhất trong cặp hình ảnh:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

Trong trường hợp này, không có chuyển đổi cụ thể khi thanh bên xuất hiện ở cả hai trạng thái, vì mặc định là hoàn hảo.

Cập nhật DOM không đồng bộ và đang chờ nội dung

Lệnh gọi lại được chuyển đến .startViewTransition() có thể trả về một lời hứa, cho phép cập nhật DOM không đồng bộ và chờ nội dung quan trọng sẵn sàng.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

Quá trình chuyển đổi sẽ không bắt đầu cho đến khi thực hiện lời hứa. Trong thời gian này, trang bị đóng băng, do đó, sự chậm trễ ở đây sẽ được giảm thiểu. Cụ thể, bạn nên thực hiện tìm nạp mạng trước khi gọi .startViewTransition(), trong khi trang vẫn có khả năng tương tác hoàn toàn, thay vì thực hiện những tìm nạp này trong lệnh gọi lại .startViewTransition().

Nếu bạn quyết định đợi hình ảnh hoặc phông chữ sẵn sàng, hãy nhớ sử dụng thời gian chờ tăng dần:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

Tuy nhiên, trong một số trường hợp, bạn nên tránh chậm trễ hoàn toàn và sử dụng nội dung bạn đã có.


Khai thác tối đa nội dung bạn đã có sẵn

Trong trường hợp hình thu nhỏ chuyển đổi sang hình ảnh lớn hơn:

Hình thu nhỏ chuyển đổi sang hình ảnh lớn hơn. Dùng thử trang web minh hoạ.

Hiệu ứng chuyển đổi mặc định là chuyển cảnh mờ dần, nghĩa là hình thu nhỏ có thể bị mờ dần khi hình ảnh đầy đủ chưa được tải.

Một cách để xử lý vấn đề này là chờ hình ảnh tải đầy đủ trước khi bắt đầu chuyển đổi. Tốt nhất là bạn nên thực hiện việc này trước khi gọi .startViewTransition() để trang vẫn có thể tương tác và có thể hiển thị một vòng quay để cho người dùng biết nội dung đang tải. Nhưng trong trường hợp này, có một cách hay hơn:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

Giờ đây, hình thu nhỏ không biến mất mà chỉ nằm bên dưới toàn bộ hình ảnh. Điều này có nghĩa là nếu khung hiển thị mới chưa tải, thì hình thu nhỏ sẽ hiển thị trong suốt quá trình chuyển đổi. Điều này có nghĩa là quá trình chuyển đổi có thể bắt đầu ngay lập tức và hình ảnh đầy đủ có thể tải theo thời gian riêng.

Điều này sẽ không hiệu quả nếu chế độ xem mới làm nổi bật tính minh bạch, nhưng trong trường hợp này, chúng ta biết rằng điều này không hoạt động, do đó chúng ta có thể thực hiện tối ưu hoá này.

Xử lý các thay đổi về tỷ lệ khung hình

Thật thuận tiện là tất cả hiệu ứng chuyển đổi từ trước đến nay đều là đối với các thành phần có cùng tỷ lệ khung hình, nhưng không phải lúc nào cũng như vậy. Điều gì sẽ xảy ra nếu hình thu nhỏ là 1:1 và hình ảnh chính là 16:9?

Một phần tử sẽ chuyển đổi sang phần tử khác và có sự thay đổi về tỷ lệ khung hình. Bản minh hoạ tối thiểu. Nguồn.

Trong hiệu ứng chuyển đổi mặc định, nhóm sẽ tạo ảnh động từ kích thước trước sang kích thước sau. Các chế độ xem cũ và mới chiếm 100% chiều rộng của nhóm và có chiều cao tự động, nghĩa là các chế độ này vẫn giữ nguyên tỷ lệ khung hình bất kể quy mô của nhóm.

Đây là một mặc định tốt, nhưng nó không phải là điều cần thiết trong trường hợp này. Vì vậy:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

Điều này có nghĩa là hình thu nhỏ vẫn ở giữa phần tử khi chiều rộng mở rộng, nhưng hình ảnh đầy đủ sẽ "không bị cắt" khi chuyển từ tỷ lệ 1:1 sang 16:9.

Để biết thêm thông tin chi tiết, hãy tham khảo phần (Xem phần chuyển đổi: Xử lý các thay đổi về tỷ lệ khung hình)(https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/)


Sử dụng các truy vấn nội dung nghe nhìn để thay đổi hiệu ứng chuyển đổi cho nhiều trạng thái thiết bị

Bạn nên sử dụng các hiệu ứng chuyển cảnh khác nhau trên thiết bị di động so với máy tính, chẳng hạn như ví dụ này thể hiện một trang trình bày đầy đủ từ một bên trên thiết bị di động, nhưng một trang trình bày tinh tế hơn trên máy tính để bàn:

Chuyển đổi một thành phần sang một thành phần khác. Bản minh hoạ tối thiểu. Nguồn.

Điều này có thể đạt được bằng cách sử dụng truy vấn phương tiện thông thường:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

Bạn cũng nên thay đổi các phần tử mà bạn chỉ định view-transition-name tuỳ thuộc vào các truy vấn nội dung nghe nhìn phù hợp.


Phản ứng với lựa chọn ưu tiên "chuyển động giảm"

Người dùng có thể cho biết họ muốn giảm chuyển động qua hệ điều hành và lựa chọn ưu tiên đó hiển thị trong CSS.

Bạn có thể chọn ngăn chặn mọi quá trình chuyển đổi đối với những người dùng này:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Tuy nhiên, lựa chọn ưu tiên cho "chuyển động giảm" không có nghĩa là người dùng muốn không muốn chuyển động. Thay vì đoạn mã trước, bạn có thể chọn một ảnh động tinh tế hơn, nhưng vẫn thể hiện mối quan hệ giữa các phần tử và luồng dữ liệu.


Xử lý nhiều kiểu chuyển đổi khung hiển thị bằng các loại chuyển đổi khung hiển thị

Đôi khi, quá trình chuyển đổi từ một chế độ xem cụ thể sang một chế độ xem khác cần phải có một sự chuyển đổi được điều chỉnh riêng. Ví dụ: khi chuyển đến trang tiếp theo hoặc trang trước trong trình tự phân trang, bạn có thể muốn trượt nội dung theo hướng khác tuỳ thuộc vào việc bạn sẽ đến trang cao hơn hay trang thấp hơn trong trình tự.

Ghi lại bản minh hoạ phân trang. Giao diện này sử dụng các hiệu ứng chuyển đổi khác nhau tuỳ thuộc vào trang bạn sẽ truy cập.

Để làm được điều này, bạn có thể sử dụng các loại chuyển đổi chế độ xem, cho phép bạn chỉ định một hoặc nhiều loại cho chuyển đổi chế độ xem đang hoạt động. Ví dụ: khi chuyển sang một trang cao hơn trong trình tự phân trang, hãy sử dụng loại forwards và khi chuyển sang trang thấp hơn, hãy sử dụng loại backwards. Các loại này chỉ hoạt động khi chụp hoặc thực hiện chuyển đổi, đồng thời mỗi loại có thể được tùy chỉnh thông qua CSS để sử dụng các ảnh động khác nhau.

Để sử dụng các loại trong hiệu ứng chuyển đổi chế độ xem cùng tài liệu, bạn sẽ truyền types vào phương thức startViewTransition. Để cho phép việc này, document.startViewTransition cũng chấp nhận một đối tượng: update là hàm callback cập nhật DOM và types là một mảng chứa các loại.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

Để phản hồi các loại này, hãy sử dụng bộ chọn :active-view-transition-type(). Truyền type mà bạn muốn nhắm mục tiêu vào bộ chọn. Điều này cho phép bạn tách biệt kiểu của nhiều hiệu ứng chuyển đổi thành phần hiển thị với nhau mà không cần khai báo thành phần này can thiệp vào các phần khai báo của thành phần hiển thị còn lại.

Vì các kiểu chỉ áp dụng khi chụp hoặc thực hiện hiệu ứng chuyển đổi, nên bạn có thể dùng bộ chọn để đặt – hoặc huỷ đặt – view-transition-name trên một phần tử chỉ dành cho hiệu ứng chuyển đổi thành phần hiển thị có loại đó.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

Trong bản minh hoạ phân trang sau đây, nội dung trang sẽ trượt về phía trước hoặc phía sau dựa trên số trang mà bạn đang truy cập. Các loại được xác định khi nhấp vào mà chúng được chuyển vào document.startViewTransition.

Để nhắm mục tiêu bất kỳ quá trình chuyển đổi chế độ xem đang kích hoạt nào, bất kể loại nào, bạn có thể sử dụng bộ chọn lớp giả :active-view-transition.

html:active-view-transition {
    …
}

Xử lý nhiều kiểu chuyển đổi khung hiển thị có tên lớp trên gốc chuyển đổi khung hiển thị

Đôi khi, quá trình chuyển đổi từ một kiểu chế độ xem cụ thể sang một chế độ xem khác phải có sự chuyển đổi được điều chỉnh riêng. Hoặc thao tác điều hướng "quay lại" phải khác với thao tác điều hướng "tiến lên".

Các hiệu ứng chuyển đổi khác nhau khi "quay lại". Bản minh hoạ tối thiểu. Nguồn.

Trước khi có các loại chuyển đổi, cách xử lý những trường hợp này là tạm thời đặt tên lớp trên gốc chuyển đổi. Khi gọi document.startViewTransition, gốc chuyển đổi này là phần tử <html>, có thể truy cập được bằng document.documentElement trong JavaScript:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

Để xoá các lớp sau khi quá trình chuyển đổi kết thúc, ví dụ này sử dụng transition.finished, một lời hứa sẽ được giải quyết sau khi quá trình chuyển đổi đạt đến trạng thái kết thúc. Các thuộc tính khác của đối tượng này được đề cập trong Tài liệu tham khảo API.

Bây giờ, bạn có thể sử dụng tên lớp đó trong CSS để thay đổi quá trình chuyển đổi:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

Tương tự như đối với truy vấn nội dung nghe nhìn, bạn cũng có thể dùng sự hiện diện của các lớp này để thay đổi phần tử nhận được view-transition-name.


Chạy hiệu ứng chuyển đổi mà không cần đóng băng các ảnh động khác

Hãy xem bản minh hoạ sau đây về vị trí chuyển đổi video:

Chuyển đổi video. Bản minh hoạ tối thiểu. Nguồn.

Bạn có thấy vấn đề gì không? Đừng lo lắng nếu bạn không làm được như vậy. Sau đây là tốc độ làm chậm lại:

Chuyển đổi video, chậm hơn. Bản minh hoạ tối thiểu. Nguồn.

Trong quá trình chuyển đổi, video có vẻ dừng lại, sau đó phiên bản đang phát của video đó mờ dần. Lý do là ::view-transition-old(video) là ảnh chụp màn hình của chế độ xem cũ, trong khi ::view-transition-new(video) là hình ảnh trực tiếp của chế độ xem mới.

Bạn có thể khắc phục vấn đề này, nhưng trước tiên, hãy tự hỏi xem có nên sửa hay không. Nếu bạn không thấy 'sự cố' khi quá trình chuyển đổi đang phát ở tốc độ bình thường, tôi sẽ không bận tâm đến việc thay đổi nó.

Nếu bạn thực sự muốn khắc phục vấn đề này, đừng hiển thị ::view-transition-old(video) mà hãy chuyển thẳng sang ::view-transition-new(video). Bạn có thể thực hiện việc này bằng cách ghi đè kiểu và ảnh động mặc định:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

Chỉ vậy thôi!

Chuyển đổi video, chậm hơn. Bản minh hoạ tối thiểu. Nguồn.

Giờ đây, video sẽ phát trong suốt quá trình chuyển đổi.


Tạo ảnh động bằng JavaScript

Cho đến nay, tất cả các hiệu ứng chuyển đổi đã được xác định bằng CSS, nhưng đôi khi CSS vẫn chưa đủ:

Chuyển đổi vòng kết nối. Bản minh hoạ tối thiểu. Nguồn.

Nếu chỉ có CSS, bạn sẽ không thể thực hiện một số phần của quá trình chuyển đổi này:

  • Ảnh động bắt đầu từ vị trí nhấp.
  • Ảnh động kết thúc với vòng tròn có bán kính ở góc xa nhất. Mặc dù vậy, chúng tôi hy vọng CSS sẽ có thể làm được điều này trong tương lai.

Rất may, bạn có thể tạo hiệu ứng chuyển đổi bằng API Ảnh động trên web!

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

Ví dụ này sử dụng transition.ready, một lời hứa sẽ được giải quyết sau khi tạo thành công các phần tử giả chuyển đổi. Các thuộc tính khác của đối tượng này được đề cập trong Tài liệu tham khảo API.


Chuyển đổi dưới dạng nâng cao

View Transition API (API Chuyển đổi khung hiển thị) được thiết kế để "bao gồm" một thay đổi DOM và tạo hiệu ứng chuyển đổi cho thay đổi đó. Tuy nhiên, bạn nên xem quá trình chuyển đổi là tính năng nâng cao, như trong trường hợp ứng dụng của bạn không được chuyển sang trạng thái "lỗi" nếu bạn thay đổi DOM thành công nhưng quá trình chuyển đổi không thành công. Tốt nhất là quá trình chuyển đổi không thành công, nhưng nếu có thì cũng không nên phá vỡ phần còn lại của trải nghiệm người dùng.

Để coi hiệu ứng chuyển đổi là tính năng nâng cao, hãy cẩn thận để không sử dụng các lời hứa chuyển đổi theo cách có thể khiến ứng dụng của bạn gửi nếu quá trình chuyển đổi không thành công.

Không nên
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

Vấn đề với ví dụ này là switchView() sẽ từ chối nếu hiệu ứng chuyển đổi không thể đạt đến trạng thái ready, nhưng điều đó không có nghĩa là không chuyển đổi được chế độ xem. DOM có thể đã cập nhật thành công, nhưng có các view-transition-name trùng lặp, vì vậy quá trình chuyển đổi đã bị bỏ qua.

Thay vào đó:

Nên
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

Ví dụ này sử dụng transition.updateCallbackDone để chờ cập nhật DOM và từ chối nếu không cập nhật được. switchView không còn từ chối nếu quá trình chuyển đổi không thành công, nó sẽ giải quyết khi quá trình cập nhật DOM hoàn tất và từ chối nếu quá trình chuyển đổi không thành công.

Nếu bạn muốn switchView giải quyết khi khung hiển thị mới đã "được giải quyết", chẳng hạn như bất kỳ quá trình chuyển đổi ảnh động nào đã hoàn tất hoặc bỏ qua đến cuối, hãy thay thế transition.updateCallbackDone bằng transition.finished.


Không phải là đoạn mã polyfill, nhưng...

Đây không phải là một tính năng dễ để chèn lấp. Tuy nhiên, chức năng trợ giúp này giúp mọi việc trở nên dễ dàng hơn trong các trình duyệt không hỗ trợ chuyển đổi chế độ xem:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

Và nó có thể được sử dụng như sau:

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

Trong các trình duyệt không hỗ trợ chuyển đổi khung hiển thị, updateDOM sẽ vẫn được gọi nhưng sẽ không có hiệu ứng chuyển đổi dạng ảnh động.

Bạn cũng có thể cung cấp một số classNames để thêm vào <html> trong quá trình chuyển đổi, giúp bạn dễ dàng thay đổi hiệu ứng chuyển đổi tuỳ thuộc vào loại hình điều hướng.

Bạn cũng có thể chuyển true đến skipTransition nếu không muốn thêm ảnh động, ngay cả trong các trình duyệt hỗ trợ hiệu ứng chuyển đổi khung hiển thị. Điều này rất hữu ích nếu trang web của bạn có lựa chọn ưu tiên của người dùng là tắt hiệu ứng chuyển đổi.


Làm việc với khung

Nếu bạn đang làm việc với một thư viện hoặc khung giúp loại bỏ các thay đổi của DOM, điều khó khăn là biết thời điểm thay đổi DOM hoàn tất. Sau đây là một loạt ví dụ bằng cách sử dụng trình trợ giúp ở trên trong các khung khác nhau.

  • Phản hồi: Khoá ở đây là flushSync, áp dụng đồng bộ một nhóm các thay đổi về trạng thái. Vâng, có một cảnh báo lớn về việc sử dụng API đó, nhưng Dan Abramov đảm bảo với tôi rằng API đó phù hợp trong trường hợp này. Như thường lệ với mã React và mã không đồng bộ, khi sử dụng nhiều hứa hẹn do startViewTransition trả về, hãy chú ý đến việc mã của bạn đang chạy với trạng thái chính xác.
  • Vue.js – khoá ở đây là nextTick, sẽ đáp ứng sau khi DOM được cập nhật.
  • Svelte – rất giống với Vue, nhưng phương thức để chờ thay đổi tiếp theo là tick.
  • Lit – chìa khoá ở đây là lời hứa this.updateComplete trong các thành phần, thực hiện sau khi DOM được cập nhật.
  • Angular – khoá ở đây là applicationRef.tick, sẽ xoá các thay đổi DOM đang chờ xử lý. Kể từ Angular phiên bản 17, bạn có thể sử dụng withViewTransitions đi kèm với @angular/router.

Tài liệu tham khảo API

const viewTransition = document.startViewTransition(update)

Bắt đầu một ViewTransition mới.

update là một hàm được gọi sau khi ghi trạng thái hiện tại của tài liệu.

Sau đó, khi lời hứa được updateCallback trả về thực hiện, quá trình chuyển đổi sẽ bắt đầu trong khung tiếp theo. Nếu updateCallback từ chối lời hứa trả về, thì quá trình chuyển đổi sẽ bị huỷ bỏ.

const viewTransition = document.startViewTransition({ update, types })

Bắt đầu một ViewTransition mới với các loại được chỉ định

update được gọi sau khi ghi trạng thái hiện tại của tài liệu.

types đặt các loại hoạt động cho hiệu ứng chuyển đổi khi chụp hoặc thực hiện hiệu ứng chuyển đổi. Ban đầu trống. Xem viewTransition.types phía dưới để biết thêm thông tin.

Thành viên thực thể của ViewTransition:

viewTransition.updateCallbackDone

Lời hứa sẽ thực hiện khi lời hứa được updateCallback trả về hoàn thành hoặc bị từ chối khi từ chối.

View Transition API (API Chuyển đổi khung hiển thị) gói một thay đổi DOM và tạo một hiệu ứng chuyển đổi. Tuy nhiên, đôi khi bạn không quan tâm đến sự thành công hay thất bại của ảnh động chuyển đổi, bạn chỉ muốn biết liệu thay đổi DOM có xảy ra hay không và khi nào. updateCallbackDone là dành cho trường hợp sử dụng đó.

viewTransition.ready

Lời hứa sẽ thực hiện được sau khi các phần tử giả cho hiệu ứng chuyển đổi được tạo và ảnh động sắp bắt đầu.

Từ chối nếu không thể bắt đầu quá trình chuyển đổi. Điều này có thể do cấu hình sai, chẳng hạn như view-transition-name trùng lặp hoặc nếu updateCallback trả về một lời hứa bị từ chối.

Điều này rất hữu ích khi tạo ảnh động cho các phần tử giả chuyển đổi bằng JavaScript.

viewTransition.finished

Lời hứa sẽ được thực hiện sau khi trạng thái kết thúc hiện hoàn toàn và tương tác được với người dùng.

Hàm này chỉ từ chối nếu updateCallback trả về một lời hứa bị từ chối, vì điều này cho biết trạng thái kết thúc chưa được tạo.

Ngược lại, nếu quá trình chuyển đổi không bắt đầu hoặc bị bỏ qua trong quá trình chuyển đổi, thì trạng thái kết thúc vẫn đạt được, do đó finished sẽ đáp ứng.

viewTransition.types

Một đối tượng giống Set chứa các loại chuyển đổi chế độ xem đang hoạt động. Để thao tác với các mục nhập, hãy dùng phương thức thực thể của lớp đó clear(), add()delete().

Để phản hồi một loại cụ thể trong CSS, hãy dùng bộ chọn lớp giả :active-view-transition-type(type) trên gốc chuyển đổi.

Các kiểu sẽ tự động được dọn dẹp khi quá trình chuyển đổi khung hiển thị kết thúc.

viewTransition.skipTransition()

Bỏ qua phần ảnh động của hiệu ứng chuyển đổi.

Điều này sẽ không bỏ qua việc gọi updateCallback, vì thay đổi DOM riêng biệt với quá trình chuyển đổi.


Kiểu mặc định và tham chiếu chuyển đổi

::view-transition
Phần tử giả gốc lấp đầy khung nhìn và chứa mỗi ::view-transition-group.
::view-transition-group

Bạn đã chọn đúng vị trí.

Chuyển đổi widthheight giữa trạng thái "trước" và "sau".

Chuyển đổi transform giữa bốn khung nhìn "trước" và "sau".

::view-transition-image-pair

Bạn đã được chuẩn bị sẵn sàng để lấp đầy nhóm.

isolation: isolate để giới hạn ảnh hưởng của mix-blend-mode đối với thành phần hiển thị cũ và mới.

::view-transition-new::view-transition-old

Được đặt tuyệt đối ở trên cùng bên trái của trình bao bọc.

Lấp đầy 100% chiều rộng nhóm, nhưng có chiều cao tự động, vì vậy sẽ duy trì tỷ lệ khung hình thay vì lấp đầy nhóm.

mix-blend-mode: plus-lighter để cho phép chuyển đổi mờ dần.

Chế độ xem cũ chuyển từ opacity: 1 sang opacity: 0. Khung hiển thị mới sẽ chuyển từ opacity: 0 sang opacity: 1.


Ý kiến phản hồi

Chúng tôi luôn trân trọng ý kiến phản hồi của nhà phát triển. Để làm như vậy, hãy gửi vấn đề cho Nhóm hoạt động CSS trên GitHub kèm theo nội dung đề xuất và câu hỏi. Thêm tiền tố [css-view-transitions] vào vấn đề của bạn.

Nếu bạn gặp lỗi thì hãy gửi lỗi Chromium.