Проблема
На сайтах часто есть несколько представлений объекта, например список товаров и страница с подробными сведениями о каждом из них. Переключение между ними часто кажется неудобным. Когда пользователь нажимает на уменьшенное изображение товара, чтобы посмотреть подробные сведения о нем, это изображение исчезает, а в другой части экрана мгновенно появляется новое, более крупное. Из-за этого пользователям сложнее отслеживать связи между элементами.
Решение
View Transitions API позволяет указать пары элементов, которые существуют в разных состояниях до и после перехода. При запуске перехода с помощью document.startViewTransition() в одностраничном приложении браузер идентифицирует общие элементы по их уникальному view-transition-name. Затем он автоматически рассчитывает разницу в их положении, размере и стиле и плавно анимирует их из старого состояния в новое. Этот переход происходит на верхнем уровне, даже над элементами с высокими значениями z-index.
Руководство по внедрению
Шаг 1. Оберните изменения состояния в startViewTransition
Для одностраничных приложений или простых изменений состояния оберните логику, которая обновляет DOM, в document.startViewTransition. Браузер делает снимок текущего состояния, выполняет обновление, а затем делает снимок нового состояния.
function navigate(view) {
// MANDATORY: Wrap the update in startViewTransition
document.startViewTransition(() => updateDOM(view));
}
Шаг 2. Назначьте имена общих переходов
Используйте свойство CSS view-transition-name, чтобы указать браузеру, какие элементы нужно преобразовать. Название может быть любым (кроме none). ОБЯЗАТЕЛЬНОЕ ТРЕБОВАНИЕ. До и после элемента с заданным значением view-transition-name должно быть не более одного элемента. Если есть два или более элементов с заданным view-transition-name, DOM будет обновлен до нового состояния немедленно, без перехода.
Вы можете использовать несколько атрибутов view-transition-name, чтобы преобразовать несколько пар элементов. Например, вы можете настроить отдельные переходы для изображения и названия товара.
В списке несколько элементов, поэтому вы не можете присвоить им одинаковое значение view-transition-name. В одностраничном приложении эту проблему можно решить двумя способами.
Динамическая страница сведений. Назначьте каждому элементу на странице списка уникальное значение
view-transition-name, а затем динамически применяйте это название к соответствующему элементу на странице сведений, когда элемент списка выбран, как показано ниже./* In the list view, give each */ #product-1 { view-transition-name: p1 } #product-2 { view-transition-name: p2 } #product-3 { view-transition-name: p3 }function updateDOM(clickedTransitionName){ const hero = document.getElementById("hero"); hero.style.viewTransitionName = clickedTransitionName; }Элемент динамического списка. Назначьте элементу на странице сведений
view-transition-nameи примените это название к элементу на странице списка, когда он выбран. Удалитеview-transition-nameиз элемента на странице списка при возврате на страницу списка.
Элемент #hero на странице сведений и выбранный элемент .thumbnail на странице списка имеют общий view-transition-name.
#hero{
view-transition-name: hero;
}
.thumbnail.selected {
view-transition-name: hero;
}
Когда пользователь нажимает на значок, мы должны подготовить список, назначив view-transition-name с помощью селектора класса .selected и внеся изменения в DOM перед началом перехода.
Затем вы можете вызвать document.startViewTransition() и применить изменения, чтобы перейти от подробного представления к списку.
После возврата к списку необходимо удалить классы переходов, чтобы при следующей навигации не возникло ошибок. Вы можете выполнить эту очистку после того, как будет выполнено обещание finished перехода.
// Function called when a thumbnail is clicked
function goFromListToDetail(e){
e.currentTarget.classList.add("selected");
const hero = document.getElementById("hero");
const bgColor = getComputedStyle(e.currentTarget).backgroundColor;
hero.style.background = bgColor;
// Trigger the transition, checking for support
if (!document.startViewTransition) {
document.body.classList.add("detail");
// MANDATORY Accessibility Routing: Route focus to the newly revealed heading to announce context and preserve logical tab flow
document.getElementById("detail-heading")?.focus();
return; // MANDATORY: End function execution if view transitions are not supported.
}
const transition = document.startViewTransition(() => {
document.body.classList.add("detail");
});
// MANDATORY Accessibility Routing: Route focus after the view transition resolves
transition.finished.finally(() => {
document.getElementById("detail-heading")?.focus();
});
}
// Function called when navigating from detail back to list view
function goFromDetailToList() {
if (!document.startViewTransition) {
document.body.classList.remove("detail");
document.getElementById("list-heading")?.focus();
return;
}
const transition = document.startViewTransition(() => {
document.body.classList.remove("detail");
});
// Clean up the list view and route focus
transition.finished.finally(() => {
// Route focus back to list view
document.getElementById("list-heading")?.focus();
// Remove selected classList to remove view-transition-names
document.querySelectorAll(".selected").forEach(
(element) => {
element.classList.remove("selected");
},
);
});
}
Выбор метода зависит от конкретного случая. Для динамического элемента списка требуется меньше повторяющегося кода CSS, но больше ручной очистки JavaScript.
Шаг 3. Исправьте растянутое соотношение сторон
По умолчанию браузер перекрестно затухает старые и новые снимки в группе, которая растягивается, чтобы соответствовать обоим. Если вы переводите текст, установите ширину текстового элемента равной fit-content как в старом, так и в новом представлении, чтобы соотношение сторон переведенного элемента было стабильным.
#list-page .title {
width: fit-content;
}
#detail-page #title {
width: fit-content;
}
Если вы переходите на элементы, которые меняют соотношение сторон, вам может потребоваться установить высоту старых и новых псевдоэлементов на 100% от псевдоэлемента ::view-transition-pair().
::view-transition-old(hero),
::view-transition-new(hero){
height: 100%;
}
Псевдоэлементы – это снимки активных элементов, поэтому вы также можете использовать объявления object-fit и object-position, чтобы лучше контролировать эффект перехода.
Рекомендации
- НЕ указывайте слишком много переходов. Используйте общие элементы только для основного контента, за которым пользователь активно следит (например, для главных изображений и заголовков).
- УДАЛЯЙТЕ временные значения
view-transition-nameпосле завершения перехода, чтобы избежать побочных эффектов при будущих переходах. - НЕ используйте переходы для элементов с активной анимацией. Переходы между представлениями работают со снимками, поэтому во время перехода анимация будет приостановлена.
- Учитывайте предпочтения пользователей, которые хотят видеть меньше анимации, с помощью медиазапроса
prefers-reduced-motion. - ОБЯЗАТЕЛЬНО. Маршрутизация специальных возможностей. Переходы между представлениями динамически изменяют макеты страниц, но не управляют программным фокусом. Если фокус остается на элементе, который скрыт или удален во время перехода, фокус теряется, в результате чего пользователи клавиатуры и технологий специальных возможностей остаются без контекста. Программно перенесите фокус на обновленный заголовок страницы или контейнер представления (с помощью
tabindex="-1") сразу после обновления DOM или когда будет выполнено обещаниеfinishedперехода представления.
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Резервные стратегии
View Transitions API разработан для прогрессивного улучшения. Браузеры, которые его не поддерживают, просто обновят DOM без анимации.
function navigate(){
if (!document.startViewTransition) {
// Fallback: Just update the DOM
updateDOM();
} else {
document.startViewTransition(() => updateDOM());
}
}