Опубликовано: 17 августа 2021 г., Последнее обновление: 25 сентября 2024 г.
Переход между представлениями выполняется для одного документа, что называется переходом между представлениями в пределах одного документа . Это обычно происходит в одностраничных приложениях (SPA), где для обновления DOM используется JavaScript. Переходы между представлениями в пределах одного документа поддерживаются в Chrome, начиная с версии Chrome 111.
Чтобы инициировать переход представления в том же документе, вызовите 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());
}
При вызове браузер автоматически делает снимки всех элементов, для которых объявлено CSS-свойство view-transition-name
.
Затем он выполняет переданный обратный вызов, который обновляет 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
Допустим, у вас есть переход между представлениями с кучей карточек, но также и с заголовком на странице. Чтобы анимировать все карточки, кроме заголовка, вам нужно написать селектор, который будет работать с каждой отдельной карточкой.
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 селекторов. Добавляете новый элемент? Тогда нужно ещё и расширить селектор, который применяет стили анимации. Не очень масштабируемо.
view-transition-class
можно использовать в псевдоэлементах view transition для применения того же правила стиля.
#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 DevTools отлично подходит для отладки переходов.
Используя панель «Анимация» , вы можете приостановить следующую анимацию, а затем прокручивать её вперёд и назад. При этом псевдоэлементы перехода отображаются на панели «Элементы» .
Переходные элементы не обязательно должны быть одним и тем же элементом 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.
Более подробную информацию см. в статье «Переходы между видами: обработка изменений соотношения сторон».
Используйте медиазапросы для изменения переходов для различных состояний устройства
Вы можете захотеть использовать разные переходы на мобильных устройствах и компьютерах, как в этом примере, где на мобильном устройстве выполняется полный слайд сбоку, а на компьютере — более плавный:
Этого можно добиться с помощью обычных медиа-запросов:
/* 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;
}
}
Однако предпочтение «уменьшенного движения» не означает, что пользователь не хочет никакого движения . Вместо предыдущего фрагмента можно выбрать более плавную анимацию, которая, тем не менее, отражает взаимосвязь между элементами и поток данных.
Обработка нескольких стилей перехода между представлениями с помощью типов перехода между представлениями
Иногда переход от одного представления к другому должен быть специально разработан. Например, при переходе на следующую или предыдущую страницу в последовательности страниц может потребоваться сдвинуть содержимое в разных направлениях в зависимости от того, переходите ли вы на более высокую или более низкую страницу в последовательности.
Для этого можно использовать типы переходов между представлениями, которые позволяют назначить один или несколько типов для активного перехода между представлениями. Например, при переходе на страницу выше в последовательности страниц используйте тип 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-transition-name
для элемента только для view transition с этим типом.
/* 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 {
…
}
Обработка нескольких стилей перехода между представлениями с помощью имени класса в корне перехода между представлениями
Иногда переход от одного конкретного типа представления к другому должен быть специально разработан. Или навигация «назад» должна отличаться от навигации «вперёд».
До появления типов переходов для решения таких ситуаций требовалось временно задать имя класса для корневого элемента перехода. При вызове document.startViewTransition
этим корневым элементом перехода становится элемент <html>
, доступ к которому осуществляется через document.documentElement
в 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');
}
Для удаления классов после завершения перехода в этом примере используется 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) использует маршрутизатор, вы можете настроить механизм обновления маршрутизатора так, чтобы он обновлял содержимое с помощью перехода между представлениями.
В следующем фрагменте кода, взятом из этой демонстрационной версии постраничной навигации, обработчик перехвата 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);
});
}
});
});
Некоторые браузеры, но не все, предоставляют собственные переходы, когда пользователь выполняет жест смахивания для навигации. В этом случае вам не следует запускать собственные переходы между представлениями, так как это может привести к некомфортному или запутанному пользовательскому опыту. Пользователь увидит два последовательных перехода: один, предоставленный браузером, и другой, предоставленный вами.
Поэтому рекомендуется предотвратить запуск перехода между представлениями, если браузер предоставил собственный визуальный переход. Для этого проверьте значение свойства hasUAVisualTransition
экземпляра NavigateEvent
. Свойство принимает значение true
, если браузер предоставил визуальный переход. Это свойство hasUIVisualTransition
также существует у экземпляров PopStateEvent
.
В предыдущем фрагменте кода проверка, определяющая необходимость перехода между представлениями, учитывает это свойство. Если переходы между представлениями в пределах одного документа не поддерживаются или браузер уже предоставил собственный переход, переход между представлениями пропускается.
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
В следующей записи пользователь смахивает, чтобы вернуться на предыдущую страницу. В записи слева проверка флага hasUAVisualTransition
не выполняется. В записи справа проверка выполняется, поэтому ручной переход между представлениями пропускается, поскольку браузер предоставил визуальный переход.
hasUAVisualTransition
(слева) и по ширине (справа)Анимация с помощью JavaScript
До сих пор все переходы определялись с помощью CSS, но иногда CSS недостаточно:
Некоторые части этого перехода невозможно реализовать только с помощью CSS:
- Анимация начинается с места щелчка.
- Анимация завершается тем, что окружность имеет радиус до самого дальнего угла. Хотя, надеюсь, в будущем это станет возможным благодаря CSS.
К счастью, вы можете создавать переходы с помощью 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 .
Переходы как улучшение
API View Transition предназначен для «обёртывания» изменения 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)', } ); }
Проблема в этом примере заключается в том, что switchView()
отклонит переход, если он не может достичь состояния ready
, но это не означает, что представление не переключилось. 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
.
Не полифилл, но…
Эту функцию непросто реализовать с помощью полифилла. Однако эта вспомогательная функция значительно упрощает задачу в браузерах, не поддерживающих переходы между представлениями:
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>
во время перехода, что упрощает изменение перехода в зависимости от типа навигации .
Вы также можете передать true
для skipTransition
, если не хотите использовать анимацию, даже в браузерах, поддерживающих переходы между представлениями. Это полезно, если на вашем сайте есть пользовательская настройка, позволяющая отключать переходы.
Работа с фреймворками
Если вы работаете с библиотекой или фреймворком, абстрагирующими изменения DOM, сложно определить, когда изменение DOM завершено. Вот несколько примеров использования вспомогательного метода, описанного выше , в различных фреймворках.
- React — ключевой момент здесь —
flushSync
, который синхронно применяет набор изменений состояния. Да, есть серьёзное предупреждение об использовании этого API, но Дэн Абрамов уверяет меня, что в данном случае это уместно. Как обычно в React и асинхронном коде, при использовании различных промисов, возвращаемыхstartViewTransition
, убедитесь, что ваш код выполняется с корректным состоянием. - Vue.js — ключевым здесь является
nextTick
, который выполняется после обновления DOM. - Svelte — очень похож на Vue, но метод ожидания следующего изменения —
tick
. - Lit — ключевым моментом здесь является обещание
this.updateComplete
внутри компонентов, которое выполняется после обновления DOM. - Angular — ключевой момент здесь —
applicationRef.tick
, который сбрасывает ожидающие изменения DOM. Начиная с версии Angular 17, можно использоватьwithViewTransitions
из@angular/router
.
Ссылка на 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
, или отклоняется, если оно отклоняется.API View Transition оборачивает изменение 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()
Пропустите анимационную часть перехода.
Это не приведет к пропуску вызова
updateCallback
, поскольку изменение DOM происходит отдельно от перехода.
Стиль по умолчанию и ссылка на переход
-
::view-transition
- Корневой псевдоэлемент, который заполняет область просмотра и содержит каждую
::view-transition-group
. -
::view-transition-group
Абсолютно позиционируем.
Переходы
width
иheight
между состояниями «до» и «после».Переходы
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
.
Обратная связь
Мы всегда рады отзывам разработчиков. Для этого отправьте запрос в рабочую группу CSS на GitHub с предложениями и вопросами. Добавьте к запросу префикс [css-view-transitions]
.
Если вы обнаружите ошибку, сообщите об ошибке Chromium .