單頁應用程式適用的相同文件檢視轉換功能

發布日期:2021 年 8 月 17 日,上次更新日期:2024 年 9 月 25 日

當檢視畫面轉場在單一文件中執行時,稱為同一個文件的檢視畫面轉場。這通常是單頁應用程式 (SPA) 的情況,因為 SPA 會使用 JavaScript 更新 DOM。自 Chrome 111 版起,Chrome 就支援相同文件檢視畫面的轉場效果。

如要觸發同一個文件檢視畫面的轉場效果,請呼叫 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());
}

在叫用時,瀏覽器會自動擷取所有宣告 view-transition-name CSS 屬性的元素快照。

接著,它會執行傳入的回呼,更新 DOM,然後擷取新狀態的快照。

這些快照會排列在偽元素樹狀結構中,並使用 CSS 動畫功能製作動畫。舊狀態和新狀態的快照組合會從舊位置和大小順利轉換至新位置,同時內容會進行交叉淡出/淡入。如有需要,您可以使用 CSS 自訂動畫。


預設轉場效果:交叉淡出

預設的檢視畫面轉場效果是交叉淡出,因此可做為 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));
}

updateTheDOMSomehow 會將 DOM 變更為新狀態。您可以按照自己的喜好進行操作。例如,您可以新增或移除元素、變更類別名稱或樣式。

這樣一來,頁面就會交錯淡出淡入:

預設交叉淡出效果。最簡單的示範來源

好吧,交叉淡出效果不怎麼吸引人。好消息是,您可以自訂轉場效果,但首先必須瞭解這項基本交叉淡出效果的運作方式。


轉場效果的運作方式

讓我們更新先前的程式碼範例。

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

呼叫 .startViewTransition() 時,API 會擷取網頁目前的狀態。包括擷取快照。

完成後,系統會呼叫傳遞至 .startViewTransition() 的回呼。這就是 DOM 變更的地方。接著,API 會擷取網頁的新狀態。

擷取新狀態後,API 會建構類似以下的偽元素樹狀結構:

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

::view-transition 會顯示在疊加層中,位於網頁上的所有其他內容上方。如果您想為轉場效果設定背景顏色,這個方法就很實用。

::view-transition-old(root) 是舊檢視畫面的螢幕截圖,::view-transition-new(root) 則是新檢視畫面的即時表示法。兩者都會以 CSS 的「已取代內容」(例如 <img>) 呈現。

舊檢視畫面會從 opacity: 1 動畫轉換至 opacity: 0,而新檢視畫面則會從 opacity: 0 動畫轉換至 opacity: 1,藉此建立交叉淡出效果。

所有動畫都是使用 CSS 動畫執行,因此可以使用 CSS 自訂動畫。

自訂轉場效果

所有檢視畫面轉場副元素都可以指定 CSS,而且由於動畫是使用 CSS 定義,因此您可以使用現有的 CSS 動畫屬性修改動畫。例如:

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

經過這項變更後,現在淡出速度非常慢:

長時間交錯淡出。最簡單的示範來源

好的,這仍不夠令人信服。以下程式碼會實作 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;
}

結果如下:

共用軸轉場效果。最簡單的示範來源

轉場多個元素

在先前的示範中,整個頁面都會參與共用軸轉場效果。這適用於大部分的網頁,但似乎不適合標題,因為標題會滑出後又滑回。

為避免這種情況,您可以從頁面其餘部分擷取頁首,以便個別為其製作動畫。方法是將 view-transition-name 指派給元素。

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

view-transition-name 的值可以是任何您想要的值 (除了 none,因為這表示沒有轉場名稱)。用於唯一識別轉換中的元素。

結果如下:

共用軸轉場效果,搭配固定標頭。最簡單的示範來源

標頭現在會維持原位並進行交叉淡出。

這個 CSS 宣告會導致虛擬元素樹狀結構變更:

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

目前有兩個轉場群組。一個用於標題,另一個用於其餘內容。您可以使用 CSS 獨立指定這些元素,並指定不同的轉場效果。不過,在本例中,main-header 會保留預設轉場效果,也就是交叉淡出/淡入效果。

好吧,預設轉場效果不只是交叉淡出,::view-transition-group 也會轉場:

  • 位置和轉換 (使用 transform)
  • 寬度
  • 高度

這項問題在先前並未造成影響,因為 DOM 變更的兩側標頭大小和位置相同。不過,您也可以擷取標題中的文字:

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

使用 fit-content 可讓元素的大小與文字相同,而不會延伸至剩餘的寬度。否則,返回箭頭會縮小標題文字元素的大小,而不會在兩個頁面中顯示相同大小。

因此,我們現在有三個部分可供操作:

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

不過,我們還是採用預設值:

滑動式標題文字。最簡單的示範來源

標題文字現在會滑動至適當位置,為返回按鈕騰出空間。


使用 view-transition-class 以相同方式為多個擬似元素設定動畫

瀏覽器支援

  • Chrome:125。
  • Edge:125。
  • Firefox:不支援。
  • Safari 技術預覽:支援。

假設您有一個包含多個資訊卡的檢視轉場,但頁面上也有標題。如要為標題以外的所有資訊卡製作動畫,您必須編寫選取器,指定每張資訊卡。

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);
}

有 20 個元素嗎?也就是說,您需要編寫 20 個選取器。新增元素?接著,您也需要擴充套用動畫樣式的 selector。不太具擴充性。

view-transition-class 可用於檢視畫面轉場虛擬元素,以套用相同的樣式規則。

#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);
}

以下卡片範例會運用先前的 CSS 程式碼片段。所有資訊卡 (包括新加入的資訊卡) 都會使用相同的時間,並透過單一選取器 html::view-transition-group(.card) 套用。

卡片示範錄影。使用 view-transition-class 時,系統會將相同的 animation-timing-function 套用至所有卡片,但新增或移除的卡片除外。

轉場偵錯

由於檢視畫面轉場效果是建立在 CSS 動畫之上,因此 Chrome 開發人員工具中的「Animations」面板非常適合用於偵錯轉場效果。

您可以使用「Animations」面板暫停下一個動畫,然後前後拖曳動畫。在這個期間,您可以在「Elements」面板中找到轉場虛擬元素。

使用 Chrome 開發人員工具偵錯檢查檢視畫面轉場效果。

轉場元素不必是相同的 DOM 元素

到目前為止,我們已使用 view-transition-name 為標頭和標頭中的文字建立個別的轉場元素。在概念上,DOM 變更前後的元素是相同的,但您可以建立不符合此情況的轉場效果。

舉例來說,主要影片嵌入項目可以使用 view-transition-name

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

接著,當使用者點選縮圖時,系統會在轉場期間為縮圖提供相同的 view-transition-name

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

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

結果如下:

一個元素轉換為另一個元素。最小示範來源

縮圖現在會轉換成主要圖片。雖然這些元素在概念上 (和字面上) 不同,但轉換 API 會將其視為相同,因為它們共用相同的 view-transition-name

這項轉場效果的實際程式碼比前述範例複雜一些,因為它也處理返回縮圖頁面的轉場效果。如需完整實作方式,請參閱原始碼


自訂進入和離開轉場效果

請參考以下範例:

進入和離開側欄。最簡單的示範來源

側欄是轉場效果的一部分:

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

但與前一個範例的標頭不同,側欄不會顯示在所有頁面上。如果兩個狀態都有側欄,轉場虛擬元素會如下所示:

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

不過,如果側欄只位於新頁面,則不會有 ::view-transition-old(sidebar) 擬似元素。由於側欄沒有「舊」圖片,因此圖片組合只會顯示 ::view-transition-new(sidebar)。同樣地,如果側欄只出現在舊版網頁上,圖片組合就只會有 ::view-transition-old(sidebar)

在前面的示範中,側欄的轉場效果會因進入、離開或同時處於這兩種狀態而有所不同。進入時會從右側滑入並淡入,退出時會滑向右側並淡出,且在兩種狀態下都會停留在原處。

如要建立特定的進入和退出轉場效果,您可以使用 :only-child 擬似類別,在圖片組中找出唯一子項,然後指定舊或新擬似元素:

/* 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;
}

在這種情況下,由於預設值已足夠,因此在兩種狀態下顯示側欄時,沒有特定的轉場效果。

非同步 DOM 更新,以及等待內容

傳遞至 .startViewTransition() 的回呼可以傳回承諾,允許非同步 DOM 更新,並等待重要內容準備就緒。

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

承諾完成後,系統才會開始轉換。在此期間,網頁會處於凍結狀態,因此應盡量減少延遲時間。具體來說,網路擷取應在呼叫 .startViewTransition() 之前完成,且網頁仍處於可完全互動狀態,而非在 .startViewTransition() 回呼中執行。

如果您決定等待圖片或字型準備就緒,請務必使用積極的逾時值:

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)]);
});

不過,在某些情況下,建議您完全避免延遲,並使用現有的內容。


充分運用現有內容

如果縮圖轉換為較大的圖片:

縮圖轉換為較大的圖片。試用示範網站

預設的轉場效果是交叉淡出,也就是說,縮圖可能會與尚未載入的完整圖片交叉淡出。

處理這項問題的其中一種方法,就是等到整個圖片載入完成後再開始轉場。理想情況下,應在呼叫 .startViewTransition() 之前完成這項操作,以便讓網頁保持互動性,並顯示旋轉圖示,向使用者顯示內容正在載入。但在這種情況下,有更好的方法:

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

縮圖現在不會淡出,而是會顯示在完整圖片下方。也就是說,如果新檢視畫面尚未載入,則在整個轉場期間都會顯示縮圖。這表示轉場效果可以立即開始,而完整圖片則可在自己的時間載入。

如果新檢視畫面具有透明度,這項最佳化就無法運作,但在本例中,我們知道該檢視畫面沒有透明度,因此可以進行這項最佳化。

處理顯示比例變更

方便起見,到目前為止,所有轉場效果都會套用至長寬比相同的元素,但這並非一成不變的情況。如果縮圖為 1:1,而主要圖片為 16:9 呢?

一個元素轉換為另一個元素,並變更顯示比例。最簡單的示範來源

在預設轉場效果中,群組會從「前」大小變更為「後」大小。舊版和新版檢視畫面都會以群組的 100% 寬度和自動高度顯示,也就是說,無論群組大小為何,檢視畫面都會維持原始的顯示比例。

這是不錯的預設值,但在這種情況下並非所需。舉例來說:

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

也就是說,當寬度擴大時,縮圖會停留在元素中央,但完整圖片會從 1:1 轉換為 16:9 時「取消裁剪」。

如需詳細資訊,請參閱「View 轉場:處理顯示比例變更


使用媒體查詢變更不同裝置狀態的轉場效果

您可能會在行動裝置和電腦上使用不同的轉場效果,例如這個範例在行動裝置上會從側邊執行完整滑動效果,但在電腦上則會執行較不明顯的滑動效果:

一個元素轉換為另一個元素。最簡單的示範來源

您可以使用一般媒體查詢來達成這項目標:

/* 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;
  }
}

您也可以根據媒體查詢的配對結果,變更要指派 view-transition-name 的元素。


回應「減少動態效果」偏好設定

使用者可以透過作業系統表示偏好減少動畫效果,系統會將偏好設定公開在 CSS 中

您可以選擇為下列使用者停用所有轉換:

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

不過,設定為「減少動畫」並不代表使用者想要「沒有動畫」。您可以選擇更精緻的動畫,取代上述程式碼片段,但仍要能表達元素之間的關係和資料流程。


使用檢視畫面轉場類型處理多種檢視畫面轉場樣式

瀏覽器支援

  • Chrome:125。
  • Edge:125。
  • Firefox:不支援。
  • Safari:18 歲。

有時候,從一個特定檢視畫面切換到另一個檢視畫面時,應使用專屬轉場效果。舉例來說,當您在分頁序列中前往下一頁或上一頁時,請根據您是要前往序列中較高或較低的頁面,考慮以不同方向滑動內容。

分頁示範的錄製影片。系統會根據您前往的頁面使用不同的轉場效果。

為此,您可以使用檢視畫面轉場類型,為有效的檢視畫面轉場指派一或多個類型。舉例來說,如果要在分頁序列中轉換至較高階的頁面,請使用 forwards 類型,如果要轉換至較低階的頁面,請使用 backwards 類型。這些類型只會在擷取或執行轉場時啟用,而且每種類型都可以透過 CSS 自訂,以便使用不同的動畫。

如要在同一個文件的檢視畫面轉場中使用類型,請將 types 傳遞至 startViewTransition 方法。為此,document.startViewTransition 也接受物件:update 是用於更新 DOM 的回呼函式,而 types 則是含有類型的陣列。

const direction = determineBackwardsOrForwards();

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

如要回應這些類型,請使用 :active-view-transition-type() 選取器。將要指定的 type 傳遞至選擇器。這樣一來,您就能將多個 View 轉場的樣式分開,不會互相干擾。

由於類型只會在擷取或執行轉場時套用,因此您可以使用選取器,針對具有該類型的檢視畫面轉場,在元素上設定或取消設定 view-transition-name

/* 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;
  }
}

在以下分頁示範中,網頁內容會根據您前往的頁碼向前或向後滑動。系統會在點擊時判斷類型,並將其傳遞至 document.startViewTransition

如要指定任何有效的檢視畫面轉場效果,無論其類型為何,您可以改用 :active-view-transition 擬群組類別選取器。

html:active-view-transition {
    …
}

在檢視畫面轉換根目錄中使用類別名稱,處理多種檢視畫面轉換樣式

有時,從某種特定類型的檢視畫面轉換至另一種檢視畫面時,應使用專屬的轉換效果。或者,'back'導覽應與 'forward'導覽不同。

返回時的轉場效果不同。最簡單的示範來源

轉場類型推出前,處理這些情況的方式是暫時在轉場根目錄上設定類別名稱。呼叫 document.startViewTransition 時,此轉場根源為 <html> 元素,可透過 JavaScript 中的 document.documentElement 存取:

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');
}

為了在轉換完成後移除類別,這個範例使用 transition.finished,這是在轉換達到結束狀態後解析的承諾。如要瞭解這個物件的其他屬性,請參閱 API 參考資料

您現在可以在 CSS 中使用該類別名稱來變更轉場效果:

/* '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;
}

與媒體查詢一樣,這些類別的存在狀態也可以用來變更哪些元素會取得 view-transition-name


在不凍結其他動畫的情況下執行轉場效果

請觀看這部影片,瞭解如何轉換位置:

影片轉場效果。最簡單的示範來源

你是否發現任何問題?如果你沒有收到,也請放心。以下是放慢的影片:

影片轉場效果,速度較慢。最簡單的示範來源

在轉場期間,影片似乎會暫停,然後播放的影片版本會淡入。這是因為 ::view-transition-old(video) 是舊版檢視畫面的螢幕截圖,而 ::view-transition-new(video) 則是新版檢視畫面的即時圖片。

你可以修正這個問題,但請先問問自己是否值得修正。如果在轉場播放正常速度時沒有看到「問題」,我不會費心變更。

如果您真的想修正這個問題,請不要顯示 ::view-transition-old(video);直接切換至 ::view-transition-new(video)。您可以覆寫預設樣式和動畫來執行這項操作:

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

這樣就大功告成了!

影片轉場效果,速度較慢。最簡單的示範來源

影片現在會在轉場期間播放。


與 Navigation API (和其他架構) 整合

視圖轉場效果會以可與其他架構或程式庫整合的方式指定。舉例來說,如果單頁應用程式 (SPA) 使用路由器,您可以調整路由器的更新機制,以便使用檢視畫面轉場更新內容。

在以下程式碼片段中,我們從這個分頁範例中擷取了程式碼片段,並調整 Navigation API 的攔截處理常式,以便在支援檢視畫面轉場時呼叫 document.startViewTransition

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

當使用者執行滑動手勢進行導覽時,部分瀏覽器會提供轉場效果,但並非所有瀏覽器都會這麼做。在這種情況下,請勿觸發自己的檢視畫面轉場,否則可能會導致使用者體驗不佳或造成混淆。使用者會看到兩個轉場效果,一個由瀏覽器提供,另一個則由您提供,兩者會依序執行。

因此,建議在瀏覽器提供自己的視覺轉場效果時,避免啟動檢視畫面轉場效果。如要達成這項目標,請檢查 NavigateEvent 例項的 hasUAVisualTransition 屬性值。當瀏覽器提供視覺轉場效果時,屬性會設為 true。這個 hasUIVisualTransition 屬性也存在於 PopStateEvent 例項中。

在前述程式碼片段中,檢查是否要執行檢視畫面轉場效果的檢查會考量這個屬性。如果系統不支援同一份文件的檢視畫面轉場效果,或是瀏覽器已提供自己的轉場效果,則會略過檢視畫面轉場效果。

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

在以下錄影中,使用者滑動螢幕,返回先前的頁面。左側擷取畫面未檢查 hasUAVisualTransition 標記。右側的錄製畫面確實包含檢查,因此瀏覽器提供視覺轉場效果,因此略過手動檢視轉場。

比較相同網站,左側未勾選 hasUAVisualTransition,右側勾選 hasUAVisualTransition

使用 JavaScript 製作動畫

到目前為止,所有轉場效果都是使用 CSS 定義,但有時 CSS 並不足以完成工作:

圓形轉場效果。最簡單的示範來源

這個轉場效果的部分內容無法單靠 CSS 實現:

  • 動畫會從點擊位置開始播放。
  • 動畫結束時,圓的半徑會延伸到最遠的角落。不過,我們希望日後 CSS 可以支援這項功能。

好消息!您可以使用 Web Animation API 建立轉場效果!

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)',
      }
    );
  });
}

這個範例使用 transition.ready,這是在成功建立轉場效果的疑似元素後解析的承諾。如要瞭解這個物件的其他屬性,請參閱 API 參考資料


轉場效果做為強化功能

View Transition API 的設計目的是「包裝」DOM 變更,並為其建立轉場效果。不過,轉場應視為強化功能,也就是說,如果 DOM 變更成功,但轉場失敗,應用程式不應進入「錯誤」狀態。理想情況下,轉場應不會失敗,但如果失敗,也不會影響其他使用者體驗。

為了將轉場視為強化功能,請注意不要以會在轉場失敗時導致應用程式擲回的方式使用轉場承諾。

錯誤做法
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)',
    }
  );
}

這個範例的問題在於,如果轉換無法達到 ready 狀態,switchView() 就會拒絕,但這並不表示檢視畫面無法切換。DOM 可能已成功更新,但有重複的 view-transition-name,因此略過轉場。

請改採以下做法:

正確做法
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
  }
}

這個範例會使用 transition.updateCallbackDone 等待 DOM 更新,並在更新失敗時拒絕。switchView 不再在轉場失敗時拒絕,而是在 DOM 更新完成時解析,並在失敗時拒絕。

如果您希望 switchView 在新檢視畫面「穩定」時解析,也就是任何動畫轉場已完成或略過至結尾,請將 transition.updateCallbackDone 替換為 transition.finished


不是 polyfill,但…

這並非容易以 polyfill 實作的功能。不過,如果瀏覽器不支援檢視畫面轉場效果,這個輔助函式可讓您更輕鬆地處理:

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');
  }
}

並且可以像這樣使用:

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

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

  // …
}

在不支援檢視畫面轉場效果的瀏覽器中,系統仍會呼叫 updateDOM,但不會顯示動畫轉場效果。

您也可以在轉場期間提供一些 classNames 來新增至 <html>,這樣就能更輕鬆地根據導覽類型變更轉場效果

即使在支援檢視畫面轉場的瀏覽器中,您也可以傳遞 trueskipTransition,以便停用動畫。如果您的網站有使用者偏好設定,可用來停用轉場效果,這項功能就很實用。


使用架構

如果您使用的是可抽象化 DOM 變更的程式庫或架構,則要知道 DOM 變更何時完成,就會比較困難。以下是一系列範例,在各種架構中使用上述輔助程式

  • React:此處的關鍵是 flushSync,可同步套用一組狀態變更。是的,使用該 API 會顯示重大警告,但 Dan Abramov 向我保證,在這種情況下使用該 API 是適當的。如同 React 和非同步程式碼,使用 startViewTransition 傳回的各種承諾時,請注意程式碼是否以正確的狀態運作。
  • Vue.js:此處的關鍵是 nextTick,在 DOM 更新後就會執行。
  • Svelte:與 Vue 非常相似,但等待下一次變更的方法是 tick
  • Lit:此處的重點是元件中的 this.updateComplete 應許,在 DOM 更新後會完成。
  • Angular:此處的關鍵是 applicationRef.tick,可清除待處理的 DOM 變更。自 Angular 17 版起,您可以使用 @angular/router 隨附的 withViewTransitions

API 參考資料

const viewTransition = document.startViewTransition(update)

開始新的 ViewTransition

update 是擷取文件目前狀態後會呼叫的函式。

接著,當 updateCallback 傳回的承諾完成時,轉換作業就會在下一個影格開始。如果 updateCallback 傳回的承諾遭到拒絕,轉場動作就會遭到放棄。

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

使用指定類型啟動新的 ViewTransition

擷取文件的目前狀態後,系統會呼叫 update

types 會在擷取或執行轉場時,設定轉場的有效類型。這個集合一開始是空的。詳情請參閱下方的 viewTransition.types

ViewTransition 的例項成員:

viewTransition.updateCallbackDone

updateCallback 傳回的承諾完成時,此承諾會完成;當該承諾遭到拒絕時,則會遭到拒絕。

View Transition API 會包裝 DOM 變更並建立轉場效果。不過,有時您可能不關心轉場動畫是否成功,只想知道 DOM 是否會發生變更,以及何時發生變更。updateCallbackDone 就是用於此用途。

viewTransition.ready

當建立轉場的虛擬元素,且動畫即將開始時,就會執行的承諾。

如果轉場無法開始,就會遭到拒絕。這可能是因為設定錯誤 (例如重複的 view-transition-name),或是 updateCallback 傳回已遭拒的承諾。

這對於使用 JavaScript 為轉場偽元素製作動畫非常有用。

viewTransition.finished

當結束狀態完全顯示且可供使用者互動時,就會執行的承諾。

只有在 updateCallback 傳回已遭拒的承諾時才會拒絕,因為這表示未建立結束狀態。

否則,如果轉換無法開始,或在轉換期間略過,仍會達到結束狀態,因此 finished 會完成。

viewTransition.types

類似 Set 的物件,可保留有效的檢視畫面轉場類型。如要操作項目,請使用其例項方法 clear()add()delete()

如要回應 CSS 中的特定類型,請在轉場根目錄上使用 :active-view-transition-type(type) 擬類別選取器。

檢視畫面轉場完成後,系統會自動清理類型。

viewTransition.skipTransition()

略過轉場動畫部分。

由於 DOM 變更與轉場無關,因此不會略過呼叫 updateCallback


預設樣式和轉場參考資料

::view-transition
填滿檢視區塊並包含每個 ::view-transition-group 的根層級擬似元素。
::view-transition-group

絕對定位。

在「before」和「after」狀態之間,轉換 widthheight

在「前」和「後」的視區空間四邊形之間轉換 transform

::view-transition-image-pair

絕對定位,可填滿群組。

具有 isolation: isolate,可限制 mix-blend-mode 對舊版和新版檢視畫面的影響。

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

絕對定位至包裝函式左上方。

會填滿 100% 的群組寬度,但高度為自動,因此會維持其顯示比例,而非填滿群組。

具有 mix-blend-mode: plus-lighter,可允許真正的交叉淡出/淡入效果。

舊版檢視畫面會從 opacity: 1 轉換為 opacity: 0。新檢視畫面會從 opacity: 0 轉換為 opacity: 1


意見回饋

我們非常重視開發人員的意見回饋。如要這樣做,請在 GitHub 上向 CSS 工作小組回報問題,並提供建議和問題。在問題名稱前方加上 [css-view-transitions]

如果遇到錯誤,請改為回報 Chromium 錯誤