限定範圍的元件轉場效果

使用 document.startViewTransition() 啟動檢視畫面轉換時,系統會將 ::view-transition 疊加層附加至 :root 元素,這會封鎖頁面其餘部分的指標事件、防止同時轉換,並遮蓋 position: fixed 標頭或開啟的 [popover] 元素 (即使使用巢狀檢視畫面轉換群組來防止溢位出血)。

在元件根層級呼叫 element.startViewTransition(),可將轉場效果限定在該 DOM 子樹狀結構,進而解決這個問題:::view-transition 虛擬元素樹狀結構會直接插入元素,保留外部堆疊內容和頁面互動性,同時允許多個元件同時轉場。

導入作業

1. 觸發範圍轉換

請在元件的根容器元素上呼叫 element.startViewTransition(),而非 document.startViewTransition()。您呼叫 startViewTransition() 的元素會成為轉場效果的範圍 (範圍元素):系統只會擷取其子樹狀結構中的元素,在更新回呼期間只會停止轉譯範圍,且產生的 ::view-transition 疊加層會與範圍元素的大小、形狀和堆疊內容相符。

2. 命名參與元素

將 view-transition-name 指派給範圍內應建立動畫的子項元素。您可以在動態清單項目上使用 view-transition-name: match-element,自動從每個元素的 DOM 節點身分衍生轉換名稱,或指派明確名稱 (只需在該範圍內不重複)。

名稱在各個範圍內會相互隔離,避免 DOM 不同部分的元件發生衝突。不過,view-transition-name 可能會與巢狀範圍檢視畫面轉場效果中的名稱衝突。如要避免這種情況,請在子項範圍元素中加入 view-transition-scope: all。

3. 自我參與和溢位剪輯

範圍元素會自動參與自己的檢視畫面轉換,因為使用者代理程式樣式表會對其套用 view-transition-name: root 和 view-transition-group: contain。這樣一來,當 DOM 變更影響範圍的幾何圖形時,範圍就會與子項一起流暢地以動畫呈現自己的幾何圖形,做為檢視畫面轉換的一部分。

當範圍裁剪溢位 (overflow: hidden、scroll 或 clip) 時,::view-transition-group-children(root) 會自動套用 overflow: clip,防止轉換中的子項溢出元件邊界。

4. 並行和巢狀轉場

獨立的元件子樹可以同時執行 element.startViewTransition(),不會互相略過。有效範圍會自動在 UA 樣式表中接收 view-transition-scope: all,因此內部 view-transition-name 值不會洩漏至外部轉場效果。在重複元件容器的 CSS 中套用 view-transition-scope: all,防止外部轉場效果擷取內部名稱,即使元件處於閒置狀態也一樣。

如果外部轉場效果需要在內部轉場效果啟用時移動元件,請將 view-transition-name 指派給包裝內部範圍的元素。接著,外部轉場效果只會擷取包裝函式並加上動畫,而不是內部範圍元素,讓內部轉場效果執行完畢。

範例

範圍元素本身 (.card 或 .card ul) 不需要 CSS 即可建立範圍,範圍是透過在元素上呼叫 startViewTransition() 而非 document 來決定 (如果元件會巢狀置於外部檢視畫面轉換中,請參閱「執行外部轉換」一節,瞭解 view-transition-scope: all)。

/* 1. Name participating child items */
.card ul li {
  view-transition-name: match-element;
}

/* 2. OPTIONAL: Customize transition timing (300ms is an example duration) */
::view-transition-group(*) {
  animation-duration: 300ms;
  animation-timing-function: ease-in-out;
}

/* 3. MANDATORY: Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
// 1. Trigger a transition scoped to a single component subtree
function runScopedTransition(scopeEl, updateDOM) {
  // MANDATORY: Call startViewTransition on the component element, not document
  const transition = scopeEl.startViewTransition(() => {
    updateDOM();
  });

  // Re-triggering startViewTransition() on the same element skips the active transition
  // and rejects `ready`. Catch the rejection to avoid unhandled promise rejection errors.
  transition.ready.catch(() => {});
}

// 2. Reorder items by moving existing DOM nodes (required for `match-element`)
// Scope to `listEl` since only the list order changes, keeping controls outside `listEl` interactive
function reorderItems(listEl, orderedIds) {
  const existing = new Map(
    Array.from(listEl.children, (li) => [li.dataset.id, li]),
  );

  runScopedTransition(listEl, () => {
    for (const id of orderedIds) {
      const li = existing.get(id);
      if (li) listEl.appendChild(li);
    }
  });
}

// 3. Remove an item while preserving exit animation and keyboard focus
// Scope to the outer `cardEl` so the card itself also transitions its height change
function removeItem(cardEl, itemEl) {
  const hadFocus = itemEl.contains(document.activeElement);
  const nextFocusTarget =
    itemEl.nextElementSibling?.querySelector('button') ??
    itemEl.previousElementSibling?.querySelector('button') ??
    cardEl.querySelector('button');

  runScopedTransition(cardEl, () => {
    itemEl.remove();
    if (hadFocus) {
      nextFocusTarget?.focus();
    }
  });
}

執行外部轉場

在預期範圍外啟動檢視畫面轉場效果時 (例如從父項元件,或從頁面層級轉場效果的文件),外部轉場效果會擷取範圍元素及其內的所有內容,並套用動畫。

為避免這種情況,請將範圍元素包裝在具有專屬 view-transition-name 的獨立包裝函式元素中,並將 view-transition-scope: all 套用至內部範圍元素。這樣一來,包裝函式本身就能由外部轉場效果擷取並建立動畫,同時防止內部範圍的內容遭到擷取。

<div class="page" id="page">
  <div class="card-wrapper">
    <section class="card" id="left">
      <h2>Card A</h2>
      <ul id="left-list">
        <li>…</li>
      </ul>
    </section>
  </div>
  <div class="card-wrapper">
    <section class="card" id="right">
      <h2>Card B</h2>
      <ul id="right-list">
        <li>…</li>
      </ul>
    </section>
  </div>
</div>
document.querySelector('#page').startViewTransition(() => {
  // do your DOM updates here...
});
/* Apply a view-transition-name on the wrapper */
.card-wrapper {
  view-transition-name: match-element;
}

/* Prevent the contents of the card being captured by an outer transition */
.card {
  view-transition-scope: all;
}

.card ul li {
  view-transition-name: match-element;
}

或者,您也可以允許外部轉場效果擷取子項。如要保留剪輯效果,請改用巢狀檢視畫面轉換群組,在包裝函式邊界剪輯擷取的子項。不過,在這種情況下,您必須手動將 clip 套用至產生的 ::view-transition-group-children() 虛擬元素。

.card-wrapper {
  overflow: clip;
  view-transition-name: match-element; /* Capture me as part of the outer VT */
  view-transition-group: contain; /* Nest children that also have a view-transition-name */
  view-transition-class: card; /* For targeting purposes */
}

/* Manually copy back the clip onto the ::view-transition-group-children pseudo */
::view-transition-group-children(.card) {
  overflow: clip;
}

.card ul li {
  view-transition-name: match-element;
}

限制與無障礙功能

  • 必要:在元件根元素上呼叫 startViewTransition(),而不是 document,這樣外部 position: fixed 元素和 [popover] 疊加層就會保持在頂層,頁面的其餘部分則可繼續互動。
  • 必要:使用 view-transition-name: match-element 時,請保留並移動現有的 DOM 節點 (而非使用 innerHTML 重新建立),因為 match-element 會追蹤 DOM 節點身分。
  • 必要:請在 ::view-transition-group(*)、::view-transition-old(*) 和 ::view-transition-new(*) 上設定 animation: none !important,使用 @media (prefers-reduced-motion: reduce) 遵循使用者減少動態效果的偏好設定。
  • 強制:在轉換期間移除焦點元素時,請將焦點移至更新回呼中的相鄰項目或控制項,以保留鍵盤焦點。
  • 建議做法:如果範圍內擷取的元素會裁剪溢出的內容,且包含巢狀擷取元素,請使用巢狀檢視畫面轉換群組 (::view-transition-group-children() 上的 view-transition-group: contain 和 overflow: clip)。
  • 建議做法:如果快速互動可能會在同一個範圍元素上重新觸發 element.startViewTransition(),請附加 transition.ready.catch(() => {})。
  • 建議做法:如果外部轉場效果需要在內部轉場效果執行時,為元件的位置加上動畫效果,請將內部範圍元素包裝在獨立的命名包裝函式元素中。
  • 請勿:在裁剪溢位 (overflow: hidden、scroll 或 clip) 或變更自身幾何圖形的範圍元素上設定 view-transition-name: none,因為選擇不自行參與會停用自動溢位裁剪功能。

備用策略

如果基準目標不支援元素範圍檢視轉場效果,請立即執行 DOM 更新回呼,不使用動畫,以確保功能正常運作。雖然您可以改用 document.startViewTransition(),但不建議用於獨立元件更新,因為文件範圍的轉場效果會阻擋並行元件轉場效果,並遮蓋頂層的 position: fixed 和 [popover] 元素。

// Feature detect specifically on Element.prototype (not document.startViewTransition)
const supportsScopedTransition = 'startViewTransition' in Element.prototype;

function runWithTransition(scopeEl, updateDOM) {
  if (!supportsScopedTransition) {
    // Fallback: apply the DOM update immediately without animation
    updateDOM();
    return;
  }

  const transition = scopeEl.startViewTransition(() => {
    updateDOM();
  });

  transition.ready.catch(() => {});
}

如果確實需要使用 document.startViewTransition() 做為備用方案,請務必保留容器的裁剪效果,詳情請參閱「執行外部轉場效果」一節。請注意,這個備用方法會破壞同時執行多個檢視畫面轉場效果的能力,並會遮蓋頂層的 position: fixed 和 [popover] 元素。