Всем привет. Я Артем Курочкин, frontend разработчик компании DD Planet.

Сегодня я расскажу об одном из ключевых нововведений в React, представленных на React Conf 2025. Прошу любить и жаловать ViewTransition - нативная поддержка view transition api в экосистеме реакта.

Что это значит для React-разработчиков и как нам всем это поможет, мы и разберем в этой статье.

Что за зверь такой View Transition API

The View Transition API provides a mechanism for easily creating animated transitions between different website views. This includes animating between DOM states in a single-page app (SPA), and animating the navigation between documents in a multi-page app (MPA).

Если не углубляться в подробности, и сказать простым языком, данное API позволяет нам делать красивые анимации просто подключив это апи. Больше не нужно проводить часы вытирая слёзы, покадрово выверяя анимацию отрисованную дизайнером! Правда ведь? Ну, к этому мы вернемся в практической части.

Что касается всего это чуда и можно ли это тащить в прод.

Can I Use
Can I Use

И да, и нет. Все зависит от ваших требований. Хром вот поддерживает с 23 года, в то время как Firefox только в прошлом месяце включили флаг поддержки по умолчанию. Но тем не менее, это baseline 2025, поэтому еще годик-два и все будет, а изучить стоит уже сейчас.

И да, дела с MPA похуже, но мы все же говорим в контексте React разработки.

Ну и в целом, забегая вперед, стейбл релиз в React произойдет лишь в 19.3, а пока мы посмотрим на практике, как это все работает в canary ветке.

Как с этим работать в React

Рассмотрим самый базовый пример как все это завести, чтобы было красиво.

В React добавили компонент ViewTransition который является оберткой для ванильного ViewTransition интерфейса. Главное правило, без которого ничего не будет, — в нем обязательно должен лежать DOM-элемент.

<ViewTransition>
	<div className="item">...</div>
</ViewTransition>

Ну все, погнали в прод? Ну, нет, чтобы анимация случилась, необходимо обернуть функцию, обновляющую DOM, в метод startTransition.

const handleUpdateDOM = () => 
	startTransition(() => 
		updateDOMSomethig()
)

И вот теперь все полетит. Но есть исключение: когда мы добавляем ноду ViewTransition, воспроизводится enter-анимация, а при удалении - exit.

function Child() {  

	return (  
		<ViewTransition>  
			<div>Hi</div>  
		</ViewTransition>  
	);  
}  

  

function Parent() {  
	const [show, setShow] = useState();  

	if (show) {  
		return <Child />;  
	}  

	return null;  
}

Но при мапе ViewTransition, все же нужно вызывать startTransition.

А теперь давайте рассмотрим реальный пример, для этого я написал простую тудушку.

Вот код который реализует это.

//App.jsx
...
import { startTransition, useState } from "react"  
import { Todo } from "./components/Todo"  
  
function App() {  
   const [todos, setTodos] = useState([  
      {...},  
   ])  
  
   const [isList, setIsList] = useState(false)  
  
   const addTodo = () => 
      startTransition(() => {
	      ...addTodoLogic
		}
      )
  
   const updateTodos = (updatedTodo) => {  
		...updateTodoLogic
   }  
  
   const deleteTodo = (id) =>  
      startTransition(() => {  
         ...deleteTodoLogic  
    })
  
   const handleView = () => 
      startTransition(() => {  
         ...handleViewLogic
    })
  
   return (  
      <div className={"wrapper"}>  
         <div className={"buttons"}>  
            <button onClick={addTodo}>Add</button>  
            <button onClick={handleView}>  
               {isList ? "Toggle Grid View" : "Toggle List View"}  
            </button>  
         </div>  
         <div className={clsx("container", { ["list"]: isList })}>  
            {todos.map((item) => (  
               <Todo  
                  key={item.id}  
                  item={item}  
                  update={updateTodos}  
                  onDelete={deleteTodo}  
               />  
            ))}  
         </div>  
      </div>  
   )}  
  
export default App
//Todo.jsx
import { startTransition, ViewTransition } from "react"  
  
import styles from "./todo.module.css"  
  
export const Todo = ({ item, update, onDelete }) => {  
   const handleTodo = (value) =>  
      startTransition(() => {  
		...todoNameUpdateLogic 
      })  
   const handleComment = (value) => {  
		...todoCommentUpdateLogic
   }  
  
   const handleComplete = (value) =>  
      startTransition(() => {  
         ...todoCompleteLogic
      })  
      
   const handleDelete = () => onDelete(item.id)  
  
   return (  
      <ViewTransition>  
         <div className={styles.wrapper}>  
            <div className={styles.row}>  
               <input  
                  value={item.todo}  
                  onChange={(event) => handleTodo(event.currentTarget.value)}  
               />  
               <input
		            type='checkbox'  
			        checked={item.complete}  
	                onChange={(event) =>  
	                    handleComplete(event.currentTarget.checked)  
	                }
                />  
            </div>  
            <textarea
	            value={item.comment}  
	            onChange={(event) => handleComment(event.currentTarget.value)}  
            />  
            <button onClick={handleDelete}>Delete</button>  
         </div>  
      </ViewTransition>  
   )}

В целом ничего сложного: один компонент обертка, и вызов метода на запуск transition, а на выходе достойный результат.

Главное не заигрывайтесь с анимациями и будьте внимательны, не вся верстка будет идеально работать.

Например, я ради эксперимента решил посмотреть, что будет с инпутом. Ответ - ничего хорошего. Даже если поставить время анимации 1мс, инпут будет терять интерактивность сильно дольше.

А что касается не идеальной работы, вот такие бывают артефакты :) (хотя, может, кому-то понравится)

Но это все цветочки, так или иначе это все можно было реализовать, написав кучку кода. А теперь перейдем к ягодкам.

Киллер фича

Барабанная дробь
Теперь можно анимировать переходы между страницами (да, стейт роутера тоже прекрасно анимируется). На этом у меня все, можно уходить (шутка).

А если серьезно, то задача такого рода (а никогда не знаешь, чего ждать от дизайнеров), ну, если раньше была не нереализуема, то точно на грани добра и зла. То теперь, пожалуйста, задача решается в одну строчку кода.

Весь код, чтобы достичь такого результата:

createRoot(document.getElementById("root")).render(  
   <StrictMode>
		<BrowserRouter>  
	         <ViewTransition>  
	            <Routes>  
	               <Route path='/' element={<App />} />  
	               <Route path='memes' element={<Memes />} />  
	               <Route path='lorem' element={<Lorem />} />  
	            </Routes>  
	         </ViewTransition>  
	    </BrowserRouter>  
   </StrictMode>,  
)

Да, вы все правильно поняли, просто обернуть роутер в ViewTransition.

Так или иначе, кому-то будет мало простой cross-fade анимации. Неужели это все, что можно выжать из данного инструментария? Ответ: конечно нет!

Кастомизация

Вся кастомизация сводится к работе с CSS (да, все же его пописать придется, если хочется чего-то большего)

View Transition API вводит новые псевдо-элементы:

Pseudo-elements
Pseudo-elements

А у компонента ViewTransition есть пропсы, которые позволяют задать класс для общения с этими псевдо-элементами.

React.dev
React.dev

P.S. Там еще колбеки есть на enter, exit, update и share, но я не придумал им практического применения. Можете поделиться своими идеями в комментариях.

Ну так вот, вернемся к практике. Я немного поигрался с кастомизацией анимации тудушек.

Вот все телодвижения, что я совершил для этого:

return (  
	<ViewTransition enter='slide-in' exit='slide-out' update='update'>
		...
	</ViewTransition>
)
::view-transition-new(.slide-in){  
    animation: slide-in cubic-bezier(.83,.15,0,.98) 0.5s forwards;  
}  
  
::view-transition-old(.slide-out){  
    animation: slide-out cubic-bezier(.83,.15,0,.98) 0.5s  forwards ;  
}  
  
::view-transition-group(.update){  
    animation-duration: 1s;  
    animation-timing-function: cubic-bezier(.83,.15,0,.98);  
}  
  
@keyframes slide-in {  
    from {  
        transform: translateX(-100%);  
        opacity: 0;  
    }  
    to {  
        transform: translateX(0);  
        opacity: 1;  
    }  
}  
  
@keyframes slide-out {  
    from {  
        transform: translateX(0);  
        opacity: 1;  
    }  
    to {  
        transform: translateX(100%);  
        opacity: 0;  
    }  
}

И отвечая на вопрос, который мог возникнуть у многих, кто сталкивался с анимациями: да, нас услышали - больше возится с тем, чтобы удалить элемент из DOMа только по завершению анимации не нужно.

Никаких больше CSSTransition и тому подобных либ, или что еще хуже, самим прописывать setTimeout-ы.

Если технически углубится, элементы теперь уничтожаются сразу, но появляется псевдо-элемент view-transition, в котором анимация и воспроизводится. C чем связан и не приятный бонус: к сожалению, пока воспроизводится анимация, взаимодействие с интерфейсом блокируется. Так что нужно соблюдать тонкую грань между плавностью/красотой и user friendly.

И добавлю, что для enter и exit не имеет смысла прописывать какие либо псевдо-элементы кроме new и old соответственно.

И если немного "пошаманить с бубном", можно получить красивые переходы между страницами.

<ViewTransition default='slide'>  
   ...
</ViewTransition>
::view-transition-new(.slide){  
    animation: slide-in-rout cubic-bezier(.83,.15,0,.98) 1.5s forwards;  
}  
  
::view-transition-old(.slide){  
    animation: slide-out-rout cubic-bezier(.83,.15,0,.98) 1.5s  forwards ;  
}  
  
@keyframes slide-in-rout {  
    from {  
        transform: translate(-100%, -50%) rotate(-90deg) scale(2);  
        filter: blur(50px);  
        opacity: 0;  
    }  
    to {  
        transform: translate(0, 0) rotate(0deg) scale(1);  
        filter: blur(0);  
        opacity: 1;  
    }  
}  
  
@keyframes slide-out-rout {  
    from {  
        transform: translate(0,0) rotate(0deg) scale(1);  
        filter: blur(0);  
        opacity: 1;  
    }  
    to {  
        transform: translate(100%, 50%) rotate(90deg) scale(.2);  
        filter: blur(50px);  
        opacity: 0;  
    }  
}

Но и это не все, что мы можем накрутить. Можно выбирать разные анимации, в зависимости от действий юзера, так как в пропсы можно передавать не только строку, но и "словари". Чтобы переключить тип анимации, нужно использовать метод addTransitionType.

А вот и реализация

<ViewTransition  
   default={{  
      "navigation-left": "slide-left",  
      "navigation-right": "slide-right",  
   }}>  
   ...
</ViewTransition>

...
<button  
   onClick={() =>  
      startTransition(() => {  
         addTransitionType("navigation-left")  
         navigate("/")  
      })   }>  
   {"<="} Go to todos{" "}  
</button>  
<button  
   onClick={() =>  
      startTransition(() => {  
         addTransitionType("navigation-right")  
         navigate("/memes")  
      })   }>  
   Go to memes {"=>"}  
</button>
...

::view-transition-new(.slide-left){  
    animation: slide-in-left cubic-bezier(.83,.15,0,.98) 1.5s forwards;  
}  
  
::view-transition-old(.slide-left){  
    animation: slide-out-left cubic-bezier(.83,.15,0,.98) 1.5s  forwards ;  
}  
  
@keyframes slide-in-left {  
    from {  
        transform: translate(-200%, -50%) scale(2);  
    }  
    to {  
        transform: translate(0, 0)  scale(1);  
    }  
}  
  
@keyframes slide-out-left {  
    from {  
        transform: translate(0,0) scale(1);  
    }  
    to {  
        transform: translate(200%, 50%) scale(.2);  
    }  
}  
  
::view-transition-new(.slide-right){  
    animation: slide-in-right cubic-bezier(.83,.15,0,.98) 1.5s forwards;  
}  
  
::view-transition-old(.slide-right){  
    animation: slide-out-right cubic-bezier(.83,.15,0,.98) 1.5s  forwards ;  
}  
  
@keyframes slide-in-right {  
    from {  
        transform: translate(200%, 50%) scale(2);  
    }  
    to {  
        transform: translate(0, 0)  scale(1);  
    }  
}  
  
@keyframes slide-out-right {  
    from {  
        transform: translate(0,0) scale(1);  
    }  
    to {  
        transform: translate(-200%, -50%) scale(.2);  
    }  
}

Fallbacks и оптимизация

Не нашел этому место в основном рассказе, но так же можно сделать красивые переходы между фолбеком и загрузившимся контентом.

Можно использовать для этого update или enter/exit. Реализуется это так:

//update

<ViewTransition>  
	<Suspense fallback={<A />}>  
		<B />  
	</Suspense>  
</ViewTransition>

//enter/exit
<Suspense fallback={<ViewTransition><A /></ViewTransition>}>  
	<ViewTransition><B /></ViewTransition>  
</Suspense>

И да, если у вас есть анимированный родитель, но у ребенка нужно убрать анимацию, в целях оптимизации или просто потому что, ребенка достаточно обернуть в еще один ViewTransition и в update прописать класс "none"

<ViewTransition>  
	<div className={theme}>  
		<ViewTransition update="none">  
			{children}  
		</ViewTransition>  
	</div>  
</ViewTransition>

Заключение

В целом, у меня очень большие ожидания от данного API, и я очень рад, что это завезут в свежем React. Оно открывает двери в мир, где анимации реализуются на чистом CSS (почти), так как проблема с DOM ушла. Ну а для простеньких, так и в CSS лезть не надо, все из коробки: обернул что нужно, накинул методы для запуска и красиво.

Особенно это выделяется среди непонятного Activity и откровенного гениального костыльного решения проблемы ESlint в виде useEffectEvent, которые нас так же ждут в React 19.3

Ну, а на этом у меня все. Всем добра и позитива. Пишите хороший код.

Комментарии (0)