Коротка відповідь. У 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.

Типові помилки

  1. Оновлення без startTransition – і «нічого не працює». Найперший камінь, об який спіткнуться всі: анімуються лише transition-оновлення. Звичайний setState – миттєвий рендер без анімації. Це фіча.
  2. <ViewTransition> навколо всього підряд. Кожна анімація – це знімки й композитинг. Анімуй смислові переходи (навігація, поява/зникнення великих блоків), а не кожен чекбокс.
  3. Share-анімація «не спрацьовує». Перевір, що name унікальне на сторінці й однакове в обох піддеревах, і що старий елемент справді зникає тоді ж, коли з'являється новий, в одному transition-оновленні.
  4. Анімований fallback. Якщо скелетон в'їжджає пів секунди, користувач пів секунди дивиться на порожнечу. default="none" для Suspense-обгорток – розумний дефолт.
  5. Забутий 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.