Реалізація 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 робочих дні.







