Новый View Transitions API, что можно перевести как "интерфейс переходов отображения", предлагает легкий способ анимирования перехода между двумя состояниями DOM — даже между загрузками страниц. Это прогрессивное улучшение, которое можно реализовать уже сегодня.


Переходы и анимации CSS революционизировали веб-эффекты за последнее десятилетие, но не все так просто. Представьте список элементов, например, 10 изображений с заголовками, который мы хотим преобразовать в новый список элементов через плавное затухание (cross-fade). Классический подход заключается в следующем:


  • сохраняем старые элементы (узлы) DOM
  • создаем новые узлы, добавляем их на страницу и позиционируем нужным образом
  • плавно скрываем старые узлы и отображаем новые
  • (опционально) заменяем старые узлы новыми

До недавнего времени у нас не было возможности простого обновления DOM. При использовании View Transitions API происходит следующее:


  • делается снимок (snapshot) текущего состояния страницы
  • выполняется обновление DOM (добавляются/удаляются элементы)
  • делается снимок нового состояния страницы
  • переход между этими состояниями анимируется с помощью дефолтного плавного затухания или с помощью кастомной анимации

Таким образом, наш код отвечает только за обновление DOM.





Данный интерфейс является экспериментальным и пока поддерживается только браузерами на основе Chromium и в SPA (одностраничные приложения).


ViewTransition API for navigations (для навигаций, переходов) также доступен в Chrome 115+ и позволяет анимировать загрузки отдельных страниц, из которых, например, состоит типичный сайт WordPress. Это даже не требует наличия JavaScript.


О намерениях Mozilla и Apple по реализации рассматриваемого интерфейса в Firefox и Safari ничего не известно. Однако браузер, который не поддерживает переходы отображения, просто пропустит их (переход будет мгновенным).


Новое — хорошо забытое старое


Разработчики определенного возраста могут испытать дежавю. Microsoft добавила переходы элементов и целых страниц в IE 4.0 (релиз которого состоялся в 1997 году) с дальнейшими обновлениями в IE 5.5 (в 2000). Мы могли добавлять вдохновленные PowerPoint эффекты с помощью тега meta:


<meta http-equiv="Page-Enter" content="progid:DXImageTransform.Microsoft.Iris(Motion='in', IrisStyle='circle')">
<meta http-equiv="Page-Exit" content="progid:DXImageTransform.Microsoft.Iris(Motion='out', IrisStyle='circle')">

Данная техника не получила широкого распространения. Не очень понятно, почему появление альтернативы заняло четверть века.


Создание переходов в пределах одной страницы


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



HTML содержит 2 элемента article с идентификаторами article1 и article2 для блоков с содержимым:


<header>
  <div>

    <h1>View Transition example 1</h1>

    <nav>
      <ul>
        <li><a href="#article1">Article 1</a></li>
        <li><a href="#article2">Article 2</a></li>
      </ul>
    </nav>

  </div>
</header>

<main>
  <div id="articleroot">

    <article id="article1">

      <h2>Article 1 content</h2>

      <figure>
        <img src="https://picsum.photos/seed/2/800/500" width="800" height="500" alt="random" />
        <figcaption>photo credit: picsum.photos</figcaption>
      </figure>

      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam nulla tortor, facilisis vel mauris in, luctus semper turpis. Aliquam lobortis dolor in lacus convallis feugiat. Nulla aliquet ante laoreet enim maximus mollis.</p>

      <p>Donec luctus rhoncus ligula, quis porta massa pharetra eu. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam luctus ante finibus massa blandit, vel gravida mi egestas. Nunc ipsum tellus, luctus vel finibus in, venenatis at lectus.</p>

      <p>Aenean vestibulum turpis nisl, at suscipit nisi pellentesque vitae. Etiam maximus nulla vitae eleifend efficitur. Curabitur gravida orci vitae mauris sollicitudin ultricies nec eu lacus. Sed tellus purus, rhoncus ullamcorper ex ut, gravida mollis felis.</p>

    </article>

    <article id="article2">

      <h2>Article 2 content</h2>

      <figure>
        <img src="https://picsum.photos/seed/4/800/500" width="800" height="500" alt="random" />
        <figcaption>photo credit: picsum.photos</figcaption>
      </figure>

      <p>Ut pretium ac orci nec dictum. Suspendisse finibus lorem tincidunt, vehicula risus sit amet, rutrum ante. Morbi ac ante tellus. Nam id turpis in diam viverra eleifend. Phasellus auctor vitae diam et vehicula. Phasellus id dolor eu nibh commodo lacinia ut at enim.</p>

      <p>Vestibulum aliquam quis mauris sit amet elementum. Ut luctus tempus turpis, scelerisque suscipit risus tristique non. Maecenas sodales id nisi vitae vehicula.</p>

    </article>

  </div>
</main>

Функция switchArticle обрабатывает все обновления DOM. Отображение статьи контролируется атрибутом hidden. При загрузке страницы активная статья определяется по location.hash (хеш-части URL — все, что после символа #). При отсутствии хеша, активной становится первая статья.


// Получаем все статьи
const article = document.getElementsByTagName('article');

// Отображаем активную статью при загрузке страницы
switchArticle();

function switchArticle(e) {

  const hash = e?.target?.hash?.slice(1) || location?.hash?.slice(1);

  Array.from(article).forEach((a, i) => {

    if (a.id === hash || (!hash && !i)) {
      a.removeAttribute('hidden');
    }
    else {
      a.setAttribute('hidden', '');
    }

  });

}

Регистрируем обрабочик кликов, вызывающий switchArticle() при клике по ссылке #hash:


document.body.addEventListener('click', e => {

  if (!e?.target?.hash) return;

  switchArticle(e);

});

Обновляем обработчик, передавая switchArticle() в качестве коллбэка document.startViewTransition() с проверкой доступности последнего:


document.body.addEventListener('click', e => {

  if (!e?.target?.hash) return;

  if (document.startViewTransition) {

    // Запускаем переход отображения
    document.startViewTransition(() => switchArticle(e));

  }
  else {

    // Переходы отображения недоступны
    switchArticle(e);
  }

});

document.startViewTransition() делает снимок начального состояния, запускает switchArticle(), делает снимок нового состояния и создает дефолтный полусекундный переход между состояниями.


Для стилизации старого и нового состояния предназначены следующие селекторы CSS:


::view-transition-old(root) {
  /* анимирование выхода (старого состояния) */
}

::view-transition-new(root) {
  /* анимирование входа (нового состояния) */
}

В приведенном выше примере мы увеличиваем продолжительность анимации до 1 секунды, чтобы сделать ее более заметной:


::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 1s;
}

::view-transition-group(root) позволяет применять эффекты сразу к обоим состояниям, хотя вряд ли мы будем применять одинаковую анимацию в большинстве случаев.


Асинхронные обновления DOM


Коллбэк, передаваемый document.startViewTransition(), может возвращать промис, что делает возможными асинхронные обновления DOM. Например:


document.startViewTransition(async () => {

  const response = await fetch('/some-data');
  const json = await response.json();
  doDOMUpdates(json);
  await sendAnalyticsEvent();

});

Это замораживает (freeze) страницу до разрешения промиса, что может негативно повлиять на пользовательский опыт. Более эффективным подходом является вызов большей части кода за пределами метода startViewTransition:


const response = await fetch('/some-data');
const json = await response.json();

document.startViewTransition(() => doDOMUpdates(json));

await sendAnalyticsEvent();

Создание более сложных переходов


Пример более сложного перехода:



В CSS определяются анимации transition-out и transition-in с затуханием и вращением (прим. пер.: обратите внимание на использование еще одной новой фичи — индивидуальных свойств трансформации):


::view-transition-old(root) {
  animation: 1s transition-out 0s ease;
}

::view-transition-new(root) {
  animation: 1s transition-in 0s ease;
}

@keyframes transition-out {
  from {
    opacity: 1;
    translate: 0;
    rotate: 0;
  }
  to {
    opacity: 0;
    translate: -3rem -5rem;
    rotate: -10deg;
  }
}

@keyframes transition-in {
  from {
    opacity: 0;
    translate: 3rem 5rem;
    rotate: -10deg;
  }
  to {
    opacity: 1;
    translate: 0;
    rotate: 0;
  }
}

Анимации применяются ко всей странице, включая элемент header, что выглядит немного странно. Свойство view-transition-name позволяет применять (или отключать) анимации к отдельным элементам:


header {
  view-transition-name: header;
}

Теперь шапка может анимироваться отдельно:


::view-transition-old(header) {
}

::view-transition-new(header) {
}

Мы не хотим анимировать шапку, поэтому у нас нет необходимости определять для нее анимации. Селекторы ::view-transition-old(root) и ::view-transition-new(root) теперь применяются ко всем элементам, за исключением <header>:



Эффекты определяются с помощью CSS, что позволяет использовать инструменты разработчика, такие как панель "Анимация" (animation) для более детального анализа и отладки анимаций.


Использование Web Animations API


Несмотря на то, что для большинства эффектов достаточно CSS, Web Animations API предоставляет более гранулированный контроль над таймингом и эффектами анимации с помощью JavaScript.


document.startViewTransition() возвращает объект, запускающий промис ready, который разрешается, когда становятся доступными псевдоэлементы перехода (обратите внимание на свойство pseudoElement второго параметра animate()):


const transition = document.startViewTransition( doDOMupdate );

transition.ready.then( () => {

  document.documentElement.animate(
    [
      { rotate: '0deg' },
      { rotate: '360deg' },
    ],
    {
      duration: 1000,
      easing: 'ease',
      pseudoElement: '::view-transition-new(root)',
    }
  );

});

Создание переход в MPA


ViewTransition API for navigations на данный момент доступен только в Chrome 115 под специальным флагом.


Для применения возможностей, предоставляемых этим интерфейсом, достаточно добавить в раздел head страницы такой тег:


<meta name="view-transition" content="same-origin" />

Для стилизации состояний также используются селекторы CSS ::view-transition-old и ::view-transition-new, а для глубокой кастомизации анимации можно применять Web Animations API.


Отключение анимаций


Для некоторых людей с особенностями восприятия анимированные переходы могут оказаться нежелательными. Соответствующее предпочтение пользователя можно определить с помощью медиа-запроса prefers-reduced-motion:


@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Заключение


View Transitions API упрощает анимирование переходов состояний элементов на странице и самих страниц. Раньше такие переходы требовали наличия JavaScript и очень осторожного обращения с навигацией браузера, такой как обработка нажатия кнопки "Назад".


Данный интерфейс является новым. Сложно сказать, останется ли он неизменным, станет ли стандартом и если станет, то когда, когда будет реализован в Firefox и Safari. Однако в качестве прогрессивного улучшения мы можем применять его уже сегодня.


Ссылки для дальнейшего изучения:


Комментарии (1)


  1. Spaceoddity
    04.07.2023 07:23

    Не знаю как там дела в Реакте, но у Vue давно уже есть обработка переходов. Разумеется кроссбраузерная. И активно используемая.

    Вообще очень похоже, что эту механику Гугл просто целиком позаимствовал у Вью, сравните:

    https://ru.vuejs.org/v2/guide/transitions.html

    Что могу сказать из опыта использования - на самом деле частенько лениво возиться с тонкой настройкой анимации через <transition>. Уж тем более в качестве "прогрессивного улучшения". Но бывают случаи, когда вот "прям то что доктор прописал" - например, частенько встречающийся "баг", когда при загрузке страницы все анимируемые элементы приходят в дефолтное состояние как раз через анимацию.