При запуске перехода между представлениями с помощью 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 в дочерний элемент scope.
3. Самостоятельное участие и обрезка переполнения
Элемент области автоматически участвует в собственном переходе представления, поскольку к нему применяется таблица стилей агента пользователя view-transition-name: root и view-transition-group: contain. Таким образом, когда на геометрию области влияет изменение, которое вы вносите в DOM, она плавно анимирует свою собственную геометрию вместе со своими дочерними элементами в рамках перехода представления.
Когда область обрезает свое переполнение (overflow: hidden, scroll или clip), ::view-transition-group-children(root) автоматически применяет overflow: clip, предотвращая выход дочерних элементов за пределы компонента.
4. Параллельные и вложенные переходы
Отдельные поддеревья компонентов могут работать параллельно, не пропуская друг друга.element.startViewTransition() Активные области автоматически получают view-transition-scope: all в таблице стилей UA, поэтому внутренние значения view-transition-name не просачиваются во внешние переходы. Примените view-transition-scope: all в CSS к контейнерам повторяющихся компонентов, чтобы внешние переходы не захватывали внутренние имена, даже когда компонент неактивен.
Если внешний переход должен переместить компонент, пока активен его внутренний переход, назначьте view-transition-name элементу, который оборачивает внутреннюю область видимости. Затем внешний переход захватывает и анимирует только оболочку, а не элемент внутренней области, позволяя внутреннему переходу завершиться.
Пример
Для создания области видимости не требуется CSS в самом элементе области (.card или .card ul). Область определяется путем вызова 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();
}
});
}
Выполнение внешнего перехода
Если переход между представлениями начинается за пределами области видимости (например, из родительского компонента или из документа для перехода на уровне страницы), внешний переход захватит и анимирует элемент области видимости и все, что находится внутри него.
Чтобы этого не произошло, поместите элемент scope в отдельный элемент wrapper с собственным view-transition-name и примените view-transition-scope: all к внутреннему элементу scope. Это позволяет захватывать и анимировать саму оболочку с помощью внешнего перехода, предотвращая захват содержимого внутренней области.
<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]оставались на верхнем слое, а остальная часть страницы – интерактивной. - ОБЯЗАТЕЛЬНО. Сохраняйте и перемещайте существующие узлы DOM (а не создавайте их заново с помощью
innerHTML) при использованииview-transition-name: match-element, посколькуmatch-elementотслеживает идентификаторы узлов DOM. - ОБЯЗАТЕЛЬНО. Учитывайте предпочтения пользователей в отношении уменьшения движения, используя
@media (prefers-reduced-motion: reduce)и задаваяanimation: none !importantдля::view-transition-group(*),::view-transition-old(*)и::view-transition-new(*). - ОБЯЗАТЕЛЬНО. Сохраняйте фокус клавиатуры при удалении элемента в фокусе во время перехода, перемещая фокус на соседний элемент или элемент управления внутри обратного вызова обновления.
- РЕКОМЕНДУЕТСЯ. Используйте вложенные группы переходов между представлениями (
view-transition-group: containиoverflow: clipв::view-transition-group-children()), если захваченный элемент в области видимости обрезает собственное переполнение и содержит вложенные захваченные элементы. - РЕКОМЕНДУЕТСЯ прикреплять
transition.ready.catch(() => {}), когда быстрые взаимодействия могут повторно активироватьelement.startViewTransition()для того же элемента области действия. - РЕКОМЕНДУЕТСЯ помещать внутренний элемент области видимости в отдельный именованный элемент-оболочку, если внешний переход должен анимировать положение компонента во время выполнения внутреннего перехода.
- НЕЛЬЗЯ устанавливать
view-transition-name: noneдля элемента области видимости, который обрезает свое переполнение (overflow: hidden,scrollилиclip) или изменяет собственную геометрию, поскольку отказ от собственного участия отключает автоматическую обрезку переполнения.
Резервные стратегии
Если ваша целевая платформа Baseline не поддерживает переходы между представлениями на уровне элементов, обеспечьте корректное снижение производительности, немедленно запустив обратный вызов обновления 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] в верхнем слое.