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

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


Переход по умолчанию: Cross-fade.

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

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

  • 125
  • 125
  • Икс
  • Икс

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

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) .

Запись демо-версии Cards . Используя view-transition-class он применяет одну и ту же animation-timing-function ко всем картам, кроме добавленных или удаленных.

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

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

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

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

Для получения более подробной информации ознакомьтесь с (Просмотр переходов: обработка изменений соотношения сторон) (https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/).


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

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

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

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

/* 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 для элемента только для перехода представления с этим типом.

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

Вот и все!

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

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


Анимация с помощью 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 .
  • Горит — ключевым моментом здесь является обещание 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 перехода просмотра оборачивает изменение DOM и создает переход. Однако иногда вас не волнует успех или неудача анимации перехода, вы просто хотите знать, произойдет ли изменение DOM и когда это произойдет. updateCallbackDone предназначен для этого варианта использования.

viewTransition.ready

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

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

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

viewTransition.finished

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

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

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

viewTransition.types

Объект , подобный Set , который содержит типы перехода активного представления. Чтобы манипулировать записями, используйте его методы экземпляраclear 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 .