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

Khi chuyển đổi chế độ xem chạy trên một tài liệu, nó được gọi là chuyển đổi chế độ xem 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) trong đó JavaScript được sử dụng để cập nhật DOM. Hiệu ứng chuyển đổi chế độ xem cùng tài liệu được hỗ trợ trong Chrome kể từ Chrome 111.

Để kích hoạt hiệu ứng chuyển đổi chế độ xem cùng một 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 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 để cập nhật DOM, sau đó sẽ 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 sức mạnh của ảnh động CSS. Các cặp ảnh chụp nhanh từ trạng thái cũ và mới chuyển đổi mượt mà từ vị trí và kích thước cũ sang vị trí mới, trong khi nội dung chuyển đổi mờ dần. Nếu muốn, bạn có thể sử dụng CSS để tuỳ chỉnh ảnh động.


Chế độ chuyển đổi mặc định: Mờ dần

Chuyển đổi khung hiển thị mặc định là làm mờ chéo, do đó đây là phần giới thiệu hữu ích 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));
}

Khi updateTheDOMSomehow thay đổi DOM sang trạng thái mới. Bạn có thể thực hiện theo bất kỳ cách nào bạn muốn. Ví dụ: bạn có thể thêm hoặc xoá phần tử, đổi tên lớp hoặc thay đổi kiểu.

Và thế là các trang dần mất đi:

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

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


Cách hoạt động của các 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 này sẽ ghi lại trạng thái hiện tại của trang. Quy trình này bao gồm cả việc chụp ảnh nhanh.

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

Sau khi ghi lại 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ủ, phía 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 khung hiển thị cũ, còn ::view-transition-new(root) là ảnh chụp màn hình trực tiếp của khung hiển thị mới. Cả hai đều hiển thị dưới dạng CSS "nội dung đã thay thế" (như <img>).

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

Tất cả ảnh động đều được thực hiện bằng cách sử dụng ảnh động CSS, vì vậy bạn có thể tùy 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ị bằng CSS và vì các ảnh động được xác định bằng CSS nên 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 thay đổi đó, hiệu ứng mờ dần hiện rất chậm:

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

Ok, vẫn chưa ấn tượng. Thay vào đó, mã sau đây sẽ triển khai chuyển đổi trục 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 chung. Bản minh hoạ tối thiểu. Nguồn.

Chuyển đổi nhiều phần tử

Trong bản minh hoạ trước, toàn bộ trang đều 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 nội dung trên trang, nhưng có vẻ không phù hợp lắm với tiêu đề vì tiêu đề trượt ra chỉ để trượt trở lại.

Để tránh điều này, bạn có thể trích xuất tiêu đề từ phần còn lại của trang để nó có thể được 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 duy nhất từng phần tử trong quá trình chuyển đổi.

Và kết quả của điều đó là:

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 ở đúng vị trí và mờ dần.

Nội dung 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 các đối tượng này một cách độc lập với CSS và dựa trên các quá trình chuyển đổi khác nhau. Mặc dù trong trường hợp này, main-header vẫn còn hiệu ứng chuyển đổi mặc định, là chuyển đổi mờ dần.

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

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

Điều đó cho đến bây giờ 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 sự 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ử là kích thước của văn bản, thay vì kéo dài đến chiều rộng còn lại. Nếu không thực hiện điều này, mũi tên quay lại sẽ giảm kích thước của phần tử 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, vẫn giữ nguyên các chế độ mặc định:

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

Giờ đây, phần văn bản tiêu đề thực hiện một đường trượt vừa ý để tạo khoảng trống cho nút quay lại.


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

Hỗ trợ trình duyệt

  • 125
  • 125
  • x
  • x

Giả sử bạn có hiệu ứng chuyển đổi chế độ xem bằng một loạt thẻ và một 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 đế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ử? Đó là 20 bộ chọn bạn cần viết. Thêm phần tử mới? Sau đó, bạn cũng cần tăng bộ chọn áp dụng kiểu ảnh động. Không thể mở rộng chính xác.

Bạn có thể 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ề các thẻ sau đây tận dụng đoạn mã CSS trước đó. Tất cả các thẻ (bao gồm cả những 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 sử dụng view-transition-class, phương thức này sẽ áp dụng cùng animation-timing-function cho tất cả các thẻ, ngoại trừ các thẻ được 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 cho nhà phát triển là lựa chọn tuyệt vời để gỡ lỗi hiệu ứng chuyển đổi.

Khi sử dụng bảng điều khiển Animations, bạn có thể tạm dừng ảnh động tiếp theo rồi tua qua 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 khung hiển thị bằng Công cụ của Chrome cho nhà phát triển.

Các phần tử chuyển đổi không cần phải giống một phần tử DOM

Cho đến nay, 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 phần đầu. Về mặt lý thuyết, đây là các phần tử giống nhau trước và sau khi DOM thay đổi, nhưng bạn có thể tạo các chuyển đổi khi không phải như vậy.

Ví dụ: Bạn có thể thêm view-transition-name vào video chính cho nội dung nhúng:

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

Sau đó, khi người dùng nhấp vào hình thu nhỏ, bạn có thể cung cấp cùng một view-transition-name, chỉ trong suốt 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 thành phần khác. Bản minh hoạ tối thiểu. Nguồn.

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

Mã thực 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ì mã này cũng xử lý hiệu ứng chuyển đổi trở lại trang hình thu nhỏ. Xem nguồn để biết cách triển khai đầy đủ.


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 sẽ khác nhau tuỳ thuộc vào việc thanh bên đang vào, thoát hay hiện ở cả hai trạng thái. Dòng đó đi vào bằng cách trượt từ bên phải và mờ dần, thoát ra bằng cách trượt sang phải và mờ dần, đồng thời nó vẫn ở đúng vị trí khi xuất hiện ở cả hai trạng thái.

Để tạo các chuyển đổi nhập và thoát cụ thể, bạn có thể sử dụng lớp giả :only-child để nhắm mục tiêu 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ể nào cho thời điểm 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 truyền đến .startViewTransition() có thể trả về một hứa hẹn, 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, vì vậy bạn nên giảm thiểu độ trễ ở đây. 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 hoàn toàn có tính tương tác, thay vì thực hiện các tìm nạp đó 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ờ linh hoạt:

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 tình trạng 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ó

Trong trường hợp hình thu nhỏ chuyển sang một hình ảnh lớn hơn:

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

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

Một cách để xử lý vấn đề này là chờ tải hình ảnh đầ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ó khả năng tương tác và một vòng quay có thể xuất hiện để cho người dùng biết rằng trang đang tải. Nhưng trong trường hợp này, có một cách tốt 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 bị mờ đi mà chỉ hiển thị bên dưới hình ảnh đầy đủ. Điều này có nghĩa là nếu chế độ xem 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 trong thời gian riêng.

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

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

Điểm thuận tiện là tất cả các hiệu ứng chuyển đổi cho đến thời điểm này đều thực hiện cho các phần tử có cùng tỷ lệ khung hình, nhưng không phải lúc nào cũng đúng. Nếu hình thu nhỏ là 1:1 và hình ảnh chính là 16:9 thì sao?

Một thành phần chuyển đổi sang một thành phần khác với tỷ lệ khung hình thay đổi. 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 đến kích thước sau. Chế độ xem cũ và mới có chiều rộng là 100% của nhóm và chiều cao tự động, nghĩa là chúng giữ nguyên tỷ lệ khung hình bất kể quy mô của nhóm.

Đây là giá trị mặc định tốt, nhưng đó không phải là giá trị được mong muốn 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;
}

Tức là hình thu nhỏ vẫn ở chính giữa thành phần khi chiều rộng mở rộng, nhưng toàn bộ hình ảnh sẽ "không bị cắt" khi chuyển tỷ lệ từ 1:1 sang 16:9.

Để biết thêm thông tin chi tiết, hãy xem bài viết Chuyển đổi chế độ xem: Xử lý các thay đổi về tỷ lệ khung hình


Dùng truy vấn đa phương tiệ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 đổi khác nhau trên thiết bị di động và máy tính, chẳng hạn như ví dụ này là một trang trình bày đầy đủ ở một bên trên thiết bị di động, nhưng là một trang trình bày tinh tế hơn trên máy tính:

Chuyển đổi một thành phần sang 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 các 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 những phần tử mà bạn chỉ định view-transition-name tuỳ thuộc vào các truy vấn đa phương tiện phù hợp.


Thể hiện cảm xúc với lựa chọn ưu tiên "chuyển động rút gọn"

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

Bạn có thể chọn không cho phép những người dùng này chuyển đổi:

@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 "chuyển động rút gọn" không có nghĩa là người dùng muốn không có chuyển động. Thay vì đoạn mã trước đó, bạn có thể chọn ả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 kiểu chuyển đổi khung hiển thị

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

Bản ghi bản minh hoạ phân trang. Cách này sử dụng các hiệu ứng chuyển cảnh khác nhau tuỳ thuộc vào trang bạn sẽ chuyển đến.

Để 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 đổi sang một trang cao hơn trong trình tự phân trang, hãy sử dụng loại forwards còn khi chuyển đến một 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 hiệu ứng chuyển đổi và bạn có thể tuỳ chỉnh mỗi loại 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 khung hiển thị cùng tài liệu, bạn chuyển types vào phương thức startViewTransition. Để cho phép điều 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 này.

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 đến vào bộ chọn. Điều này cho phép bạn tách biệt các kiểu của nhiều lượt chuyển đổi khung hiển thị mà không cần khai báo một lượt chuyển đổi khung hiển thị ảnh hưởng đến khai báo của một lượt chuyển đổi khác.

Vì các loại 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 bỏ thiết lập) view-transition-name trên một phần tử chỉ dành cho chuyển đổi chế độ xem với 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, nội dung trang trượt tiến hoặc lùi dựa trên số trang mà bạn đang truy cập. Các kiểu này được xác định khi lượt nhấp mà chúng được chuyển vào document.startViewTransition.

Để nhắm đến mọi lượt chuyển đổi chế độ xem đang hoạt động, bất kể là loại nào, bạn đều 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ị bằng tên lớp trên gốc của hiệu ứng chuyển đổi khung hiển thị

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

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

Trước khi các loại chuyển đổi, cách xử lý các 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. Đây là một lời hứa sẽ phân giải 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 hiệu ứng 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ư các truy vấn nội dung đa phương tiện, bạn cũng có thể sử dụng các lớp này để thay đổi việc phần tử nào nhận được view-transition-name.


Chạy hiệu ứng chuyển đổi mà không làm treo các ảnh động khác

Hãy tham khảo bản minh hoạ nà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 như vậy. Ở đây, tốc độ 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ẻ như bị treo rồi phiên bản đang phát của video rõ dần. Nguyên nhân là do ::view-transition-old(video) là ảnh chụp màn hình của thành phần hiển thị cũ, trong khi ::view-transition-new(video) là ảnh trực tiếp của thành phần hiển thị mới.

Bạn có thể sửa lỗi này, nhưng trước tiên, hãy tự hỏi xem có đáng sửa hay không. Nếu bạn không thấy "sự cố" khi 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 lỗi này, đừng hiện ::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 quá trình chuyển đổi đã được xác định bằng cách sử dụng CSS, nhưng đôi khi CSS là không đủ:

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

Quá trình chuyển đổi này sẽ không thể đạt được nếu chỉ dùng CSS:

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

Rất may là 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ẽ phân giải sau khi các phần tử giả chuyển đổi được tạo thành công. 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 được thiết kế để "gói" một thay đổi DOM và tạo hiệu ứng chuyển đổi cho sự thay đổi đó. Tuy nhiên, quá trình chuyển đổi phải được coi là một sự nâng cao, tức là ứng dụng của bạn không được chuyển sang trạng thái "lỗi" nếu thay đổi DOM thành công nhưng quá trình chuyển đổi không thành công. Lý tưởng nhất là quá trình chuyển đổi không thành công. Tuy nhiên, nếu có, quá trình chuyển đổi sẽ không ảnh hưởng đến trải nghiệm người dùng còn lại.

Để coi các lượt chuyển đổi là tính năng nâng cao, hãy chú ý không sử dụng các hứa hẹn chuyển đổi theo cách có thể khiến ứng dụng gửi đ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 đề trong 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 thành phần hiển thị. DOM có thể đã được cập nhật thành công, nhưng có view-transition-name trùng lặp, vì vậy 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 thành công. switchView không còn từ chối nếu quá trình chuyển đổi không thành công, nó được phân giải khi quá trình cập nhật DOM hoàn tất và từ chối nếu quá trình cập nhật không thành công.

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


Đây không phải là một đoạn mã polyfill, nhưng...

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

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à có thể 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ợ hiệu ứng chuyển đổi khung hiển thị, updateDOM vẫn sẽ đượ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ỳ theo hình thức điều hướng.

Bạn cũng có thể truyền true đến skipTransition nếu không muốn có ảnh động, ngay cả trong các trình duyệt hỗ trợ 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.


Cách dùng khung

Nếu bạn đang làm việc với một thư viện hoặc khung có loại bỏ các thay đổi của DOM, thì phần khó khăn là phải biết khi nào thay đổi DOM hoàn tất. Sau đây là một số ví dụ (sử dụng trình trợ giúp ở trên) trong nhiều khung nội dung.

  • React (Phản ứng) – khoá ở đây là flushSync để áp dụng một loạt các thay đổi trạng thái một cách đồng bộ. Đúng, có một cảnh báo quan trọng 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à không đồng bộ, khi sử dụng nhiều hứa hẹn do startViewTransition trả về, hãy đảm bảo rằng mã của bạn đang chạy ở trạng thái chính xác.
  • Vue.js – khoá ở đây là nextTick. Mã này sẽ thực hiện 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, sẽ thực hiện sau khi DOM được cập nhật.
  • Angular – khoá ở đây là applicationRef.tick dùng để xoá các thay đổi DOM đang chờ xử lý. Kể từ phiên bản Angular 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 chụp trạng thái hiện tại của tài liệu.

Sau đó, khi thực hiện lời hứa do updateCallback trả về, quá trình chuyển đổi sẽ bắt đầu trong khung tiếp theo. Nếu lời hứa mà updateCallback trả về bị từ chối, 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 theo các kiểu đã chỉ định

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

types đặt các loại đang 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, cửa sổ này trống. Xem thêm viewTransition.types dưới đây để biết thêm thông tin.

Thành phần của thực thể của ViewTransition:

viewTransition.updateCallbackDone

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

View Transition API bao bọc 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 hoạt ảnh chuyển đổi, bạn chỉ muốn biết liệu thay đổi DOM xảy ra khi nào và khi nào. updateCallbackDone dành cho trường hợp sử dụng đó.

viewTransition.ready

Lời hứa sẽ thực hiện 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.

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

Việc 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 thực hiện sau khi trạng thái kết thúc hiển thị hoàn toàn và có tính tương tác với người dùng.

Chính sách này chỉ từ chối nếu updateCallback trả về 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 sẽ đạt đến, vì vậy finished sẽ thực hiện.

viewTransition.types

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

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

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

viewTransition.skipTransition()

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

Thao tác này sẽ không bỏ qua việc gọi updateCallback, vì thay đổi đối với DOM tách biệt với hiệu ứng chuyển đổi.


Tài liệu tham khảo về hiệu ứng chuyển đổi và kiểu mặc định

::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

Hoàn toàn đúng.

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-không gian "trước" và "sau".

::view-transition-image-pair

Hoàn toàn có đủ khả năng để đưa vào nhóm.

isolation: isolate để giới hạn ảnh hưởng của mix-blend-mode đối với các 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 của nhóm, nhưng có chiều cao tự động, nên sẽ duy trì tỷ lệ khung hình thay vì lấp đầy nhóm.

mix-blend-mode: plus-lighter để cho phép hiệu ứng mờ dần thực sự.

Chế độ xem cũ sẽ chuyển đổi từ opacity: 1 sang opacity: 0. Chế độ xem mới sẽ chuyển đổi 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. Để thực hiện việc này, hãy gửi vấn đề cho Nhóm hoạt động CSS trên GitHub kèm theo các đề xuất và câu hỏi. Thêm tiền tố [css-view-transitions] cho vấn đề của bạn.

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