Переходы между представлениями одного и того же документа для одностраничных приложений

Опубликовано: 17 августа 2021 г., Последнее обновление: 25 сентября 2024 г.

Когда переход между представлениями происходит в рамках одного документа, это называется переходом между представлениями в рамках одного документа . Обычно это происходит в одностраничных приложениях (SPA), где для обновления DOM используется JavaScript. Переходы между представлениями в рамках одного документа поддерживаются в 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

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox: 144.
  • Сафари: 18.2.

Source

Допустим, у вас есть анимация перехода между элементами, включающая множество карточек, а также заголовок на странице. Чтобы анимировать все карточки, кроме заголовка, вам нужно написать селектор, который будет нацелен на каждую отдельную карточку.

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 можно использовать в псевдоэлементах перехода между представлениями для применения того же правила стиля.

#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 отлично подходит для отладки этих переходов.

С помощью панели «Анимация» вы можете приостановить следующую анимацию, а затем прокручивать ее вперед и назад. При этом псевдоэлементы перехода можно найти на панели «Элементы» .

Отладка переходов между окнами с помощью инструментов разработчика 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.

Для получения более подробной информации ознакомьтесь со статьей «Переходы между представлениями: обработка изменений соотношения сторон».


Используйте медиазапросы для изменения переходов в зависимости от состояния устройства.

Возможно, вам захочется использовать разные переходы на мобильных устройствах и компьютерах, например, как в этом примере, где на мобильных устройствах происходит полное выдвижение экрана сбоку, а на компьютерах — более плавное:

Переход от одного элемента к другому. Минимальная демонстрация . Источник .

Этого можно достичь с помощью обычных медиа-запросов:

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

Однако предпочтение «умеренной анимации» не означает, что пользователь хочет полного отсутствия анимации . Вместо приведенного выше фрагмента можно выбрать более плавную анимацию, которая, тем не менее, будет отображать взаимосвязь между элементами и поток данных.


Обработка нескольких стилей перехода между представлениями с помощью типов перехода между представлениями.

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox: 144.
  • Сафари: 18.

Source

Иногда переход от одного конкретного представления к другому должен иметь специально настроенный эффект. Например, при переходе на следующую или предыдущую страницу в последовательности пагинации может потребоваться разное направление сдвига содержимого в зависимости от того, переходите ли вы на страницу выше или ниже в последовательности.

Видеозапись демонстрации пагинации . В зависимости от страницы используются разные переходы.

Для этого можно использовать типы переходов между представлениями, которые позволяют назначать один или несколько типов активному переходу между представлениями. Например, при переходе на страницу выше в последовательности пагинации используйте тип 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 для элемента только для перехода представления с этим типом.

/* 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) использует маршрутизатор, вы можете настроить механизм обновления маршрутизатора так, чтобы он обновлял контент с помощью анимации перехода между представлениями.

В приведенном ниже фрагменте кода, взятом из демонстрации пагинации, обработчик перехвата 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);
            });
        }
    });
});

Некоторые, но не все браузеры предоставляют собственные переходы при использовании жеста свайпа для навигации. В этом случае вам не следует запускать собственный переход между окнами, так как это приведет к плохому или запутанному пользовательскому опыту. Пользователь увидит два перехода — один предоставленный браузером, а другой — вами — которые будут выполняться последовательно.

Поэтому рекомендуется предотвратить начало перехода между представлениями, если браузер предоставил собственный визуальный переход. Для этого проверьте значение свойства 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) {
    return unsupported('skipTransition was set to true');
  }

  if (!document.startViewTransition) {
    return unsupported('Same-Document View Transitions are not supported in this browser');
  }

  if (!ViewTransition || !("types" in ViewTransition.prototype)) {
    return unsupported('View Transitions with types are not supported in this browser');
  }

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

  return transition;
}

И его можно использовать следующим образом:

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 .
  • Ключевым моментом здесь является промис 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 .