使用 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] 元素。