[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-react-19-3-viewtransition-not-animating":3},{"content":4,"lastModified":5},"\u003Cfigure>\n  \u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1547658719-da2b51169166?auto=format&fit=crop&w=1200&q=80\" alt=\"Desktop monitor with a code editor open beside a rendered web page during a frontend animation debugging session\" loading=\"lazy\" \u002F>\n\u003C\u002Ffigure>\n\n\u003Cp>I bumped React to 19.3 on a Tuesday afternoon expecting it to be a chore. No breaking changes, ViewTransition and Fragment refs out of canary, five minutes of work. Then I spent the afternoon learning that a plain \u003Ccode>setState\u003C\u002Fcode> will never animate anything.\u003C\u002Fp>\n\n\u003Cp>The component reads well and it is easy to use wrong, which is the worst combination for a UI API. Everything below is what I hit, in the order I hit it.\u003C\u002Fp>\n\n\u003Ch2>The gate is startTransition\u003C\u002Fh2>\n\n\u003Cp>I wrapped a panel in \u003Ccode>&lt;ViewTransition&gt;\u003C\u002Fcode>, wired up a button, clicked it, and got a hard cut. No warning, no console message, no difference from the code I had before.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">\u002F\u002F nothing happens\n&lt;button onClick={() =&gt; setOpen(!open)}&gt;Toggle&lt;\u002Fbutton&gt;\n\n\u002F\u002F this one animates\n&lt;button onClick={() =&gt; startTransition(() =&gt; setOpen(!open))}&gt;Toggle&lt;\u002Fbutton&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>A boundary only activates for updates React already treats as non-urgent. Three things qualify: a state update inside \u003Ccode>startTransition\u003C\u002Fcode>, a Suspense boundary revealing its content, and a value coming from \u003Ccode>useDeferredValue\u003C\u002Fcode>. A click handler setting state is urgent, commits right away, and skips the animation on purpose.\u003C\u002Fp>\n\n\u003Cp>I was annoyed for ten minutes, then decided the docs are right. Typing in a search field should never cross-fade. The updates where motion earns its place were deferrable anyway, and tying the two together kills a category of bug where an animation fights the input it was meant to polish.\u003C\u002Fp>\n\n\u003Ch2>The boundary has to be the thing being inserted\u003C\u002Fh2>\n\n\u003Cp>React looks at how the tree changed and picks one of four animations: enter when the boundary is added, exit when it is removed, update when its children change, share when a named boundary disappears in one subtree while the same name shows up in another.\u003C\u002Fp>\n\n\u003Cp>I wanted a slide and got a cross-fade, because the boundary never unmounted. Only its contents were changing. A key fixed it:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;ViewTransition key={tab}&gt;\n  {tab === 'inbox' ? &lt;Inbox \u002F&gt; : &lt;Sent \u002F&gt;}\n&lt;\u002FViewTransition&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Changing the key unmounts one boundary and mounts another, so React sees an exit and an enter. Without it the boundary stays mounted and you get an update, which is a cross-fade in place. Both are legitimate, they just mean different things.\u003C\u002Fp>\n\n\u003Cp>Then the same class of bug bit me one level deeper:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">\u002F\u002F cross-fade, no slide\n&lt;div className=\"toast-wrap\"&gt;\n  &lt;ViewTransition&gt;Saved&lt;\u002FViewTransition&gt;\n&lt;\u002Fdiv&gt;\n\n\u002F\u002F slides in\n&lt;ViewTransition&gt;\n  &lt;div className=\"toast-wrap\"&gt;Saved&lt;\u002Fdiv&gt;\n&lt;\u002FViewTransition&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Enter and exit fire when the boundary is the first thing inserted in that transition. Nest it inside a wrapper and the wrapper becomes the inserted node, so the boundary inside has nothing to enter. Same markup, same CSS, different result.\u003C\u002Fp>\n\n\u003Ch2>Stop naming things\u003C\u002Fh2>\n\n\u003Cp>This is the real difference from the \u003Ca href=\"\u002Fposts\u002Fview-transitions-api-replace-animation-library\u002F\">raw browser API\u003C\u002Fa>, where I had to set \u003Ccode>view-transition-name\u003C\u002Fcode> on both sides myself and clear it afterwards. In 19.3 you pass the \u003Ccode>name\u003C\u002Fcode> prop only for shared elements, the thumbnail-that-becomes-a-hero case, and React generates a unique name for every other boundary.\u003C\u002Fp>\n\n\u003Cp>That detail matters more than it sounds. The name has to be unique across the entire app, so a \u003Ccode>name=\"card\"\u003C\u002Fcode> copied into three list components is a bug waiting to surface, and a duplicate drops the whole transition rather than just the second card.\u003C\u002Fp>\n\n\u003Cp>One migration note for anyone who shipped the old workaround: React calls \u003Ccode>startViewTransition\u003C\u002Fcode> itself and will interrupt anything else on the page trying to do the same. The \u003Ccode>flushSync\u003C\u002Fcode> wrapper I had been dragging around now fights React for the same transition, so it went out with the upgrade.\u003C\u002Fp>\n\n\u003Ch2>Direction, and the object form of the props\u003C\u002Fh2>\n\n\u003Cp>A carousel is the obvious next problem. Forward and back both land on the same state value, so React has no way to know which way you moved. \u003Ccode>addTransitionType\u003C\u002Fcode> is that channel:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-jsx\">function next() {\n  startTransition(() =&gt; {\n    addTransitionType('forward')\n    setSlide(n =&gt; n + 1)\n  })\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Then \u003Ccode>enter\u003C\u002Fcode>, \u003Ccode>exit\u003C\u002Fcode>, \u003Ccode>update\u003C\u002Fcode>, \u003Ccode>share\u003C\u002Fcode> and \u003Ccode>default\u003C\u002Fcode> each take a class name string, \u003Ccode>\"auto\"\u003C\u002Fcode> for the browser default, \u003Ccode>\"none\"\u003C\u002Fcode> to switch an animation off, or an object keyed by type with a \u003Ccode>default\u003C\u002Fcode> fallback. That object form is the cleanest part of the release in practice.\u003C\u002Fp>\n\n\u003Cp>React also puts each type on the element as a browser view transition type, so \u003Ccode>:active-view-transition-type(forward)\u003C\u002Fcode> works in plain CSS if you prefer the decision to live there.\u003C\u002Fp>\n\n\u003Ch2>What looks broken mid-flight\u003C\u002Fh2>\n\n\u003Cp>A boundary animates a captured image of the region, not the live nodes. That is what makes the animation cheap, and it is also why a spinner inside a transitioning region looks frozen during the fade. It is a screenshot. Anything with parts that move on their own needs a nested boundary of its own.\u003C\u002Fp>\n\n\u003Cp>Overlapping updates batch as well. Start A to B, then land updates toward C and D while the first is still running, and the next animation goes from B to D instead of replaying the queue. New fonts hold a transition up for up to 500ms, and an image inside a boundary waits for the image. That one fixed a hero image that used to pop in half-loaded, so no complaints from me.\u003C\u002Fp>\n\n\u003Ch2>Before you ship\u003C\u002Fh2>\n\n\u003Cp>React does not turn animations off for people who asked for less motion. The docs say to add the media query yourself, and I would treat that as a requirement rather than a suggestion. It is also DOM only right now, so a shared component library should not assume the behavior survives on React Native.\u003C\u002Fp>\n\n\u003Cp>I would skip it for UI that is cached and appears instantly. Animating something that was already on screen is a delay dressed up as polish. What I kept is narrow: a panel that slides in from the side, a list row that expands into a detail view, and a settings tab that stopped jumping. A chunk of animation code gone, none of it doing anything the browser could not do.\u003C\u002Fp>\n\n\u003Cp>The two snippets I keep retyping, the key trick and the object form of the enter and exit props, live in \u003Ca href=\"\u002Fsnippetark\u002F\" rel=\"noopener noreferrer\" target=\"_blank\">Snippet Ark\u003C\u002Fa> next to the CSS I wrote for the old API, since the two are easy to confuse a month apart.\u003C\u002Fp>\n","2026-09-23",1790289963467]