Only transitions animate

A plain setState swaps instantly. startTransition lets it animate.

React docsreact.dev
By default, setState updates immediately and does not activate <ViewTransition>, only updates wrapped in a Transition, <Suspense>, or useDeferredValue activate ViewTransition.
ViewTransition · CaveatsRead docs ↗

From vercel-react-view-transitions, SKILL.md.

Same toggle, two updates

Press Show details, then hide them.

Plain setStateBad
setOpen(o => !o)
startTransitionGood
startTransition(() => setOpen(o => !o))
import { startTransition, useState, ViewTransition } from 'react';

function Order() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => startTransition(() => setOpen(o => !o))}>
        Details
      </button>
      {open && (
        <ViewTransition enter="slide-up" exit="fade-out">
          <Details />
        </ViewTransition>
      )}
    </>
  );
}

Deferred values animate too

Type in the filter.

Filter by searchBad
people.filter(match(search))
useDeferredValueGood
people.filter(match(deferred))
const [search, setSearch] = useState('');
const deferred = useDeferredValue(search);
const visible = people.filter(p => p.name.includes(deferred));

<input value={search} onChange={e => setSearch(e.target.value)} />
{visible.map(p => (
  <ViewTransition key={p.id}><Row person={p} /></ViewTransition>
))}

Why it matters, pros and cons

Why it matters

React calls startViewTransition only for Transitions, Suspense reveals and deferred values. A plain update commits synchronously, so there is nothing to animate between.

Pros

  • One wrapper, no animation library.
  • React starts the browser transition; never call it yourself.
  • Unsupported browsers just swap instantly.

Cons

  • flushSync skips the animation.
  • Fast updates batch: A→B→C→D plays as B→D.
  • Outside Next.js it needs react@canary.