View Transitions API: плавные переходы между страницами
View Transitions API — анимации переходов между страницами и элементами, cross-document transitions, ::view-transition-old/new, snapshot и кастомизация переходов в CSS.
Что такое View Transitions
View Transitions API позволяет создать плавную анимацию при переходе между страницами или смене состояния — без JavaScript-библиотек. Браузер делает «снимок» текущего состояния, применяет новое, и анимирует переход между ними.
Простейший переход (SPA)
Для одностраничных приложений — вызовите startViewTransition при смене контента:
document.startViewTransition(() => {
updateContent()
})
Этого хватит, чтобы браузер сделал плавное затухание (cross-fade) между старым и новым состоянием.
Включение для MPA (многостраничных сайтов)
Для переходов между разными HTML-страницами добавьте meta-тег:
<meta name="view-transition" content="same-origin">
И опционально CSS-правило:
@view-transition {
navigation: auto;
}
Теперь обычные переходы по ссылкам <a href="about.html"> будут анимироваться через cross-fade.
Псевдоэлементы перехода
Браузер создаёт снимки старого и нового состояний. Ими можно управлять через псевдоэлементы:
::view-transition-old(root) {
animation: fade-out 0.3s ease;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
По умолчанию — cross-fade 250ms. Можно менять длительность и кривую:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
Именованные переходы для отдельных элементов
Чтобы анимировать не всю страницу, а конкретный элемент — задайте view-transition-name:
.hero-image {
view-transition-name: hero;
}
.page-title {
view-transition-name: title;
}
Теперь эти элементы будут иметь собственные переходы:
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 0.4s;
}
::view-transition-old(title),
::view-transition-new(title) {
animation-duration: 0.3s;
}
Имя уникально — на странице не может быть двух элементов с одинаковым view-transition-name. Если нужно анимировать элементы списка, используйте JS для установки уникальных имён:
document.querySelectorAll('.card').forEach((card, i) => {
card.style.viewTransitionName = `card-${i}`
})
Exclude: исключение элементов из перехода
Не все элементы должны участвовать в анимации — хедер, футер, сайдбар:
header,
footer,
.sidebar {
view-transition-name: none;
}
Элементы с none просто появляются мгновенно, без transition.
Анимация morph (Shared Element Transition)
Если на обеих страницах есть элемент с одинаковым view-transition-name, браузер морфирует его — плавно меняет позицию, размер и форму:
.card-image {
view-transition-name: product-image;
}
При переходе со страницы каталога на карточку товара изображение «перелетит» из списка в нужную позицию. Браузер сам рассчитает разницу координат и размеров.
Кастомизация с JS
Полный контроль через startViewTransition:
const transition = document.startViewTransition(async () => {
await updateDOM()
})
transition.ready.then(() => {
document.documentElement.animate(
[
{ clipPath: 'circle(0% at 50% 50%)' },
{ clipPath: 'circle(100% at 50% 50%)' }
],
{
duration: 500,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)'
}
)
})
Раскрытие кругом от центра — популярный эффект для модальных окон и переходов.
Раскрытие от точки клика
button.addEventListener('click', (e) => {
const x = e.clientX
const y = e.clientY
const endRadius = Math.hypot(
window.innerWidth - x,
window.innerHeight - y
)
const transition = document.startViewTransition(() => {
updateContent()
})
transition.ready.then(() => {
document.documentElement.animate(
[
{ clipPath: `circle(0px at ${x}px ${y}px)` },
{ clipPath: `${endRadius}px at ${x}px ${y}px` }
],
{
duration: 400,
easing: 'ease-out',
pseudoElement: '::view-transition-new(root)'
}
)
})
})
Nuxt / Vue
В Nuxt-проекте view transitions включаются через конфиг:
export default defineNuxtConfig({
app: {
head: {
meta: [
{ name: 'view-transition', content: 'same-origin' }
]
}
}
})
Или программно в плагине:
export default defineNuxtPlugin(() => {
useRouter().afterEach(() => {
if (!document.startViewTransition) return
document.startViewTransition(async () => {
await nextTick()
})
})
})
Поддержка
- Chrome 111+, Edge 111+ — полная поддержка
- Firefox 126+ — полная поддержка
- Safari 18+ — частичная (SPA-переходы)
- MPA-переходы (между страницами) — Chrome/Edge, Firefox
Для проверки:
if (document.startViewTransition) {
document.startViewTransition(() => updateDOM())
} else {
updateDOM()
}
Практические паттерны
Cross-fade страниц
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: 0.25s ease both fade-and-scale-down;
}
::view-transition-new(root) {
animation: 0.25s ease both fade-and-scale-up;
}
@keyframes fade-and-scale-down {
to {
opacity: 0;
transform: scale(0.95);
}
}
@keyframes fade-and-scale-up {
from {
opacity: 0;
transform: scale(1.05);
}
}
Перелёт логотипа
.logo {
view-transition-name: logo;
}
::view-transition-old(logo),
::view-transition-new(logo) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
Слайд галереи
.gallery-image {
view-transition-name: gallery-main;
}
::view-transition-old(gallery-main) {
animation: slide-out-left 0.3s ease forwards;
}
::view-transition-new(gallery-main) {
animation: slide-in-right 0.3s ease forwards;
}
@keyframes slide-out-left {
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(100%); opacity: 0; }
}
Итог
document.startViewTransition()— запуск перехода для SPA<meta name="view-transition">— переходы между страницами (MPA)view-transition-name— именованные переходы для отдельных элементов- Morph-эффект при одинаковом имени на двух страницах
::view-transition-old/new— кастомизация анимации через CSS- Проверяйте поддержку через
if (document.startViewTransition)