Плавные и простые переходы с помощью View Transitions API

Поддержка браузера

  • 111
  • 111
  • Икс
  • Икс

Источник

API View Transition позволяет легко изменить DOM за один шаг, создавая при этом анимированный переход между двумя состояниями. Он доступен в Chrome 111+.

Переходы, созданные с помощью View Transition API. Попробуйте демо-сайт — требуется Chrome 111+.

Зачем нам нужна эта функция?

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

Но в Интернете уже есть инструменты анимации, такие как CSS-переходы , CSS-анимации и API веб-анимации , так зачем нам что-то новое для перемещения объектов?

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

Даже что-то вроде простого перекрестного затухания предполагает одновременное присутствие обоих состояний. Это создает проблемы с удобством использования, такие как обработка дополнительного взаимодействия на исходящем элементе. Кроме того, для пользователей вспомогательных устройств существует период, когда состояние «до» и «после» одновременно находятся в DOM, и объекты могут перемещаться по дереву так, что это визуально нормально, но может легко привести к неправильному положению чтения и фокусу. Потерянный.

Обработка изменений состояний особенно сложна, если два состояния различаются по положению прокрутки. А если элемент перемещается из одного контейнера в другой, вы можете столкнуться с трудностями из overflow: hidden и других форм отсечения, а это означает, что вам придется реструктурировать свой CSS, чтобы получить желаемый эффект.

Это не невозможно, это просто очень сложно .

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

Кроме того, хотя текущая реализация ориентирована на одностраничные приложения (SPA), эта функция будет расширена, чтобы обеспечить переходы между полными загрузками страниц, что в настоящее время невозможно.

Статус стандартизации

Эта функция разрабатывается в рамках рабочей группы W3C CSS в качестве проекта спецификации .

Как только мы будем довольны дизайном API, мы начнем процессы и проверки, необходимые для выпуска этой функции в стабильную версию.

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

Самый простой переход: перекрестное затухание.

Переход просмотра по умолчанию представляет собой плавное затухание, поэтому он служит хорошим введением в 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)
   └─ …

Но опять же, просто используя настройки по умолчанию:

Скользящий текст заголовка. Минимальная демонстрация . Источник .

Теперь текст заголовка немного сдвигается, освобождая место для кнопки «Назад».

Отладка переходов

Поскольку переходы просмотра построены на основе анимации CSS, панель «Анимации» в Chrome DevTools отлично подходит для отладки переходов.

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

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

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

Изменение перехода в зависимости от типа навигации

Иногда переход от одного конкретного типа страниц к другому должен иметь специально адаптированный переход. Или навигация «назад» должна отличаться от навигации «вперед».

Различные переходы при движении «назад». Минимальная демонстрация . Источник .

Лучший способ справиться с этими случаями — установить имя класса для <html> , также известного как элемент документа:

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

Вот и все!

Видео переход, медленнее. Минимальная демонстрация . Источник .

Теперь видео воспроизводится на протяжении всего перехода.

Анимация с помощью 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 перехода просмотра предназначен для «обертывания» изменения 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,
  classNames = [],
  updateDOM,
}) {
  if (skipTransition || !document.startViewTransition) {
    const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});

    return {
      ready: Promise.reject(Error('View transitions unsupported')),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
    };
  }

  document.documentElement.classList.add(...classNames);

  const transition = document.startViewTransition(updateDOM);

  transition.finished.finally(() =>
    document.documentElement.classList.remove(...classNames)
  );

  return transition;
}

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

function spaNavigate(data) {
  const classNames = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

В браузерах, которые не поддерживают переходы просмотра, updateDOM все равно будет вызываться, но анимированного перехода не будет.

Вы также можете указать некоторые classNames для добавления в <html> во время перехода, что упрощает изменение перехода в зависимости от типа навигации .

Вы также можете передать true в skipTransition если вам не нужна анимация, даже в браузерах, поддерживающих View Transitions. Это полезно, если на вашем сайте пользователь предпочитает отключать переходы.

Работа с фреймворками

Если вы работаете с библиотекой или платформой, которая абстрагирует изменения 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(updateCallback)

Запустите новый ViewTransition .

updateCallback вызывается после фиксации текущего состояния документа.

Затем, когда обещание, возвращенное updateCallback выполняется, переход начинается в следующем кадре. Если обещание, возвращенное updateCallback , отклоняется, переход прекращается.

Члены экземпляра ViewTransition :

viewTransition.updateCallbackDone

Обещание, которое выполняется, когда обещание, возвращенное updateCallback выполняется, или отклоняется, когда оно отклоняется.

API перехода просмотра оборачивает изменение DOM и создает переход. Однако иногда вас не волнует успех/неуспех анимации перехода, вы просто хотите знать, произойдет ли изменение DOM и когда это произойдет. updateCallbackDone предназначен для этого варианта использования.

viewTransition.ready

Обещание, которое выполняется, когда псевдоэлементы для перехода созданы и анимация вот-вот начнется.

Он отклоняет, если переход не может начаться. Это может произойти из-за неправильной конфигурации, например, из-за дублирования view-transition-name или из-за того, что updateCallback возвращает отклоненное обещание.

Это полезно для анимации псевдоэлементов перехода с помощью JavaScript .

viewTransition.finished

Обещание, которое выполняется, как только конечное состояние становится полностью видимым и интерактивным для пользователя.

Он отклоняется только в том случае, если updateCallback возвращает отклоненное обещание, поскольку это указывает на то, что конечное состояние не было создано.

В противном случае, если переход не начинается или пропускается во время перехода, конечное состояние все равно достигается, поэтому finished выполняется.

viewTransition.skipTransition()

Пропустите анимационную часть перехода.

При этом вызов updateCallback не будет пропущен, поскольку изменение DOM не связано с переходом.

Стиль по умолчанию и ссылка на переход

::view-transition
Корневой псевдоэлемент, который заполняет область просмотра и содержит каждую ::view-transition-group .
::view-transition-group

Абсолютно позиционирован.

width и height перехода между состояниями «до» и «после».

Переходы transform четырехугольник пространства окна просмотра «до» и «после».

::view-transition-image-pair

Абсолютно готов пополнить группу.

Имеет isolation: isolate , чтобы ограничить влияние режима наложения plus-lighter на старый и новый виды.

::view-transition-new и ::view-transition-old

Абсолютно расположен в верхнем левом углу оболочки.

Заполняет 100 % ширины группы, но имеет автоматическую высоту, поэтому сохраняет соотношение сторон, а не заполняет группу.

Имеет mix-blend-mode: plus-lighter чтобы обеспечить истинное плавное затухание.

Старый вид переходит от opacity: 1 к opacity: 0 . Новое представление переходит от opacity: 0 к opacity: 1 .

Обратная связь

На этом этапе очень важна обратная связь с разработчиками, поэтому сообщайте о проблемах на GitHub с предложениями и вопросами.