Реализация CSS View Transitions API для переходов между страницами
View Transitions API позволяет создавать плавные анимированные переходы между состояниями DOM — или между страницами в MPA/SPA — с помощью нативного браузерного механизма. Браузер делает скриншот текущего состояния, применяет новое, затем анимирует переход. По умолчанию используется crossfade, но через CSS-псевдоэлементы можно реализовать любую анимацию.
Поддержка браузеров
Chrome 111+, Edge 111+, Safari 18+. Firefox — с флагом, без стабильной поддержки на 2025 год. Обязательна проверка через document.startViewTransition перед использованием.
Базовое использование (SPA)
// utils/view-transition.ts
export async function navigateWithTransition(
updateDOM: () => void | Promise<void>
): Promise<void> {
// Fallback для браузеров без поддержки
if (!document.startViewTransition) {
await updateDOM()
return
}
const transition = document.startViewTransition(async () => {
await updateDOM()
})
try {
await transition.finished
} catch (e) {
// Переход был прерван (новый переход начался)
if (!(e instanceof DOMException && e.name === 'AbortError')) {
throw e
}
}
}
Интеграция с React Router / Next.js
React Router v6
// router/transition-router.tsx
import { useNavigate } from 'react-router-dom'
import { navigateWithTransition } from '../utils/view-transition'
export function useTransitionNavigate() {
const navigate = useNavigate()
return (to: string, options?: { replace?: boolean }) => {
navigateWithTransition(() => {
navigate(to, options)
})
}
}
Next.js App Router
Next.js 14+ поддерживает View Transitions через unstable_viewTransition в next/link:
// app/components/TransitionLink.tsx
'use client'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
export function TransitionLink({
href,
children,
className,
}: {
href: string
children: React.ReactNode
className?: string
}) {
const router = useRouter()
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
if (!document.startViewTransition) {
router.push(href)
return
}
document.startViewTransition(() => {
router.push(href)
})
}
return (
<a href={href} onClick={handleClick} className={className}>
{children}
</a>
)
}
CSS: кастомные анимации переходов
/* styles/view-transitions.css */
/* Псевдоэлементы по умолчанию */
::view-transition-old(root) {
animation: fade-out 0.2s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-out;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
/* Слайд для навигации */
@keyframes slide-from-right {
from { transform: translateX(40px); opacity: 0; }
}
@keyframes slide-to-left {
to { transform: translateX(-40px); opacity: 0; }
}
/* Применяем к конкретному направлению (атрибут на html) */
html[data-transition="forward"] {
&::view-transition-old(root) {
animation: slide-to-left 0.25s ease-in both;
}
&::view-transition-new(root) {
animation: slide-from-right 0.25s ease-out both;
}
}
html[data-transition="backward"] {
&::view-transition-old(root) {
animation: 0.25s ease-in both reverse slide-from-right;
}
&::view-transition-new(root) {
animation: 0.25s ease-out both reverse slide-to-left;
}
}
Установка атрибута при навигации:
// Определяем направление перед стартом перехода
function navigate(to: string, history: string[]) {
const isBack = history[history.length - 2] === to
document.documentElement.dataset.transition = isBack ? 'backward' : 'forward'
document.startViewTransition(() => {
// Обновление DOM/маршрута
router.push(to)
})
}
Именованные view-transition-name: общие элементы
Именованные переходы позволяют плавно "переносить" элемент между страницами (shared element transition):
/* Страница списка */
.product-card-image {
view-transition-name: product-image; /* уникальное имя */
}
/* Страница детального просмотра */
.product-detail-image {
view-transition-name: product-image; /* то же имя */
}
/* Браузер сам анимирует переход от одного к другому */
::view-transition-group(product-image) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
Для динамических имён (список товаров):
// components/ProductCard.tsx
export function ProductCard({ product }: { product: Product }) {
return (
<div>
<img
src={product.image}
alt={product.name}
style={{
// view-transition-name должно быть уникальным в DOM
viewTransitionName: `product-image-${product.id}`,
}}
/>
</div>
)
}
// pages/product/[id]/page.tsx
export default function ProductDetail({ params }: { params: { id: string } }) {
return (
<img
src={product.image}
style={{ viewTransitionName: `product-image-${params.id}` }}
/>
)
}
Управление паузой и готовностью
async function transitionWithControl(update: () => void) {
const transition = document.startViewTransition(update)
// Ждём пока старый контент скопирован
await transition.ready
// Здесь можно запустить дополнительные Web Animations
document.documentElement.animate(
{ opacity: [1, 0.8, 1] },
{ duration: 300, pseudoElement: '::view-transition-new(root)' }
)
await transition.finished
}
Отключение для prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
}
Типичные сроки
Базовый crossfade между страницами Next.js/React Router — 4–6 часов. Направленные слайд-переходы + shared element transitions для списка/детали — 2–3 рабочих дня. Полная система с управлением направлением, fallback, тестами и поддержкой Safari — 3–4 рабочих дня.







