Коротка відповідь. У React 19.3 компонент <ViewTransition> став stable: загортаєш ним елемент, оновлюєш стан через startTransition – і React разом із браузерним View Transition API сам анімує появу, зникнення чи переміщення елемента. Ключова ідея, яку варто зрозуміти до першого рядка коду: анімуються тільки non-urgent оновлення. Для ховер-ефектів і мікровзаємодій звичайний CSS transition як був, так і лишається правильним інструментом.
Що саме стало stable у 19.3
Реліз React 19.3 перевів зі статусу experimental у stable дві речі, які нас тут цікавлять: <ViewTransition> і Fragment Refs (про другі – окрема розмова). View Transitions поки що працюють лише в DOM – підтримка React Native, за словами команди, у процесі.
Якщо ти вже бачив браузерний View Transition API (document.startViewTransition), ментальна модель знайома: браузер робить «знімок» старого стану, застосовує зміни, робить знімок нового – і анімує між ними. React-обгортка додає головне: інтеграцію з життєвим циклом компонентів. Тобі не треба ловити момент «DOM уже змінився, але кадр ще не намальований» – React знає це сам.
Ментальна модель: чотири типи анімацій
import { ViewTransition } from 'react';
{isShowing && (
<ViewTransition>
<Card />
</ViewTransition>
)}React сам обирає тип анімації залежно від того, що сталося з деревом:
- enter –
<ViewTransition>з'явився; - exit – зник;
- update – діти змінили вміст або стилі;
- share – іменований
<ViewTransition>зник в одному місці й з'явився в іншому (той самий «переліт» картки у деталі, заради якого раніше тягнули цілу бібліотеку).
І найважливіше правило: анімація запускається лише коли оновлення позначене як Transition. Тобто:
- стан оновили всередині
startTransition; - показався вміст після
<Suspense>; - значення прийшло з
useDeferredValue.
Термінові оновлення – набір тексту, клік, який має відгукнутись миттєво, – рендеряться одразу і не анімуються ніколи. Це не обмеження, а найкраще дизайн-рішення цього API: неможливо випадково зробити інтерфейс, де інпут «пливе» слідом за пальцями.
Приклад: список → деталі без втрати контексту
Класичний кейс: клік по картці статті відкриває повний перегляд, і картка «перелітає» у hero нової в'юхи. Іменований <ViewTransition> в обох місцях:
import { ViewTransition, Suspense } from 'react';
import { startTransition, useState } from 'react';
function Blog() {
const [openSlug, setOpenSlug] = useState<string | null>(null);
function openArticle(slug: string) {
// Без startTransition анімації НЕ буде – оновлення буде терміновим
startTransition(() => setOpenSlug(slug));
}
if (openSlug) {
return (
<ViewTransition name={`card-${openSlug}`}>
<ArticleDetail slug={openSlug} />
</ViewTransition>
);
}
return articles.map((article) => (
<ViewTransition key={article.slug} name={`card-${article.slug}`}>
<ArticleCard article={article} onOpen={() => openArticle(article.slug)} />
</ViewTransition>
));
}Однакове name в обох піддеревах – і React запускає share-анімацію: браузер сам інтерполює позицію й розмір. Жодного вимірювання координат, жодного FLIP-коду руками.
Налаштовується це двома способами: view-transition-класами через CSS або подіями onEnter / onExit / onUpdate / onShare, у які React передає керування Web Animations API – для випадків, коли CSS недостатньо.
addTransitionType: «вперед» і «назад» – різні історії
Одна й та сама зміна стану може мати різні причини. Слайдер: наступний слайд в'їжджає справа, попередній – зліва. Стан один (setCurrentSlide), анімації різні. Для цього 19.3 додає addTransitionType:
import { startTransition, addTransitionType } from 'react';
function nextSlide() {
startTransition(() => {
addTransitionType('next');
setCurrentSlide((c) => c + 1);
});
}
function previousSlide() {
startTransition(() => {
addTransitionType('previous');
setCurrentSlide((c) => c - 1);
});
}А <ViewTransition> мапить тип на анімацію:
<ViewTransition
enter={{ next: 'from-right', previous: 'from-left' }}
exit={{ next: 'to-left', previous: 'to-right' }}
>
<Slide />
</ViewTransition>Бонус: ці типи прокидаються і в браузер як view transition types, тож у CSS можна таргетуватись через :active-view-transition-type(...).
Suspense і streaming: що анімується, а що ні
View Transitions дружать зі <Suspense> із коробки – перехід fallback → вміст запускає update-анімацію:
<ViewTransition update="auto" default="none">
<Suspense fallback={<Skeleton />}>
<Comments />
</Suspense>
</ViewTransition>Зверни увагу на патерн із релізу, він вартий запам'ятовування: default="none" означає, що поява fallback-а не анімується – скелетон показується миттєво, бо користувач має одразу бачити, що щось відбувається. А ось заміна скелетона реальним вмістом – анімується. Повільний лоадер, який ще й красиво в'їжджає, – це подвійне покарання користувача, і React тут за замовчуванням підштовхує до правильного UX.
Той самий механізм працює для зображень і шрифтів: загорнувши <img> у <ViewTransition> зі <Suspense>, отримуєш координоване завантаження без характерного блимання.
Доступність: prefers-reduced-motion – не опція
Браузерний View Transition API сам по собі не вимикає анімації для людей із налаштуванням reduced motion – це відповідальність автора. Мінімум, який має бути в кожному проєкті з View Transitions:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}Зміни стану відбудуться, вміст оновиться – просто без руху. Для частини людей анімовані переміщення елементів – це не «вау», а запаморочення. Сайт, на якому ти це читаєш, працює за тим самим принципом: усі motion-компоненти мають статичний fallback.
Типові помилки
- Оновлення без
startTransition– і «нічого не працює». Найперший камінь, об який спіткнуться всі: анімуються лише transition-оновлення. ЗвичайнийsetState– миттєвий рендер без анімації. Це фіча. <ViewTransition>навколо всього підряд. Кожна анімація – це знімки й композитинг. Анімуй смислові переходи (навігація, поява/зникнення великих блоків), а не кожен чекбокс.- Share-анімація «не спрацьовує». Перевір, що
nameунікальне на сторінці й однакове в обох піддеревах, і що старий елемент справді зникає тоді ж, коли з'являється новий, в одному transition-оновленні. - Анімований fallback. Якщо скелетон в'їжджає пів секунди, користувач пів секунди дивиться на порожнечу.
default="none"для Suspense-обгорток – розумний дефолт. - Забутий reduced-motion. Див. вище – це три рядки CSS, немає виправдань.
Коли звичайний CSS transition кращий
Чесна межа застосування, без фанатизму:
- Ховери, фокуси, мікровзаємодії –
transition: ...на псевдокласах. View Transitions тут навіть технічно не допоможуть: це не оновлення React-стану. - Анімація одного елемента на місці (розгортання акордеона, зміна кольору) – CSS досі простіший, передбачуваніший і не потребує transition-оновлення.
- Складна фізика, драг, жести – це територія Web Animations API чи спеціалізованих бібліотек; View Transitions анімують переходи між станами, а не безперервні взаємодії.
View Transitions виграють там, де елементи з'являються, зникають або переміщуються між частинами дерева: навігація між в'юхами, фільтрація списків, модалки, той самий «переліт» картки. Тобто рівно там, де раніше CSS було мало, а бібліотека була overkill.
Практичне завдання
Візьми будь-який свій список із фільтром (або зроби мінімальний: 10 карток + кнопки категорій). Загорни кожну картку в <ViewTransition key={id}>, перемикання фільтра зроби через startTransition – і подивись, як картки анімовано переставляються без жодного рядка анімаційного коду. Потім додай reduced-motion-блок із розділу вище і перевір через емуляцію в DevTools. Два кроки – і в тебе є еталонний приклад для роботи.
Якщо готуєшся до співбесід – View Transitions уже цілком можуть спливти в розмові про React-співбесіду як маркер «людина стежить за платформою». А про те, коли анімації перетворюються на проблему продуктивності, є детальний розбір у статті про оптимізацію React без карго-культу. Для Next.js-проєктів переходи між сторінками – окрема велика тема, початок – у гайді по Next.js.