React 19.3 landed on September 9, 2026, and it is one of the more practical minor releases in a while. Two features that many of us have been trying out in canary builds, <ViewTransition> and Fragment refs, are now stable, and React DOM gains a small but very useful browser() API for client-only rendering. Here is how I would start using each of them in a real Next.js or Vite app.
Upgrading
npm install react@19.3 react-dom@19.3
If you are on a framework like Next.js, check that your framework version supports the React release before bumping it on its own.
Animations with <ViewTransition>
<ViewTransition> wraps the browser's View Transition API in a declarative component. React figures out why an element is animating and gives you four triggers:
- enter: the boundary was added to the tree
- exit: the boundary was removed
- update: its children changed content or style
- share: a named boundary moved from one place to another
The simplest version needs no configuration at all:
import { ViewTransition } from 'react'
export function Panel({ open }: { open: boolean }) {
return open ? (
<ViewTransition>
<SettingsPanel />
</ViewTransition>
) : null
}
View transitions run when the update is part of a Transition (for example inside startTransition), so wrap state changes that should animate.
Directional animations with addTransitionType
Real UIs need different animations depending on the cause of a change. A carousel should slide left when going forward and right when going back. addTransitionType tags the Transition, and <ViewTransition> can map each type to a CSS class:
import { ViewTransition, addTransitionType, startTransition, useState } from 'react'
export function Slides({ slides }: { slides: string[] }) {
const [index, setIndex] = useState(0)
const go = (dir: 'next' | 'previous') =>
startTransition(() => {
addTransitionType(dir)
setIndex((i) => (dir === 'next' ? i + 1 : i - 1))
})
return (
<>
<ViewTransition
key={index}
enter={{ next: 'slide-from-right', previous: 'slide-from-left' }}
exit={{ next: 'slide-to-left', previous: 'slide-to-right' }}
>
<Slide content={slides[index]} />
</ViewTransition>
<button onClick={() => go('previous')}>Back</button>
<button onClick={() => go('next')}>Next</button>
</>
)
}
The classes themselves are plain CSS that target the ::view-transition-old() / ::view-transition-new() pseudo-elements, so you keep full control over timing and easing. For imperative control, the onEnter, onExit, onUpdate and onShare props let you drive animations with the Web Animations API.
Animating Suspense reveals
Wrapping a <Suspense> boundary animates the swap from fallback to content. The React team recommends a conservative default so you only animate the reveal, not every update:
<ViewTransition update="auto" default="none">
<Suspense fallback={<CardSkeleton />}>
<ProductCard id={id} />
</Suspense>
</ViewTransition>
Note that <ViewTransition> is DOM-only for now; React Native support is still on the way.
Fragment Refs: behavior without wrapper divs
Before 19.3, adding focus handling or an IntersectionObserver to a list usually meant an extra <div>, which could break flex/grid layouts. Now a <Fragment> can take a ref, and you get a FragmentInstance that acts on the Fragment's first-level DOM children:
import { Fragment, useEffect, useRef } from 'react'
export function RevealOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef<any>(null)
useEffect(() => {
const fragment = ref.current
const observer = new IntersectionObserver((entries) => {
entries.forEach((e) => e.target.classList.toggle('visible', e.isIntersecting))
})
fragment.observeUsing(observer)
return () => fragment.unobserveUsing(observer)
}, [])
return <Fragment ref={ref}>{children}</Fragment>
}
The instance also exposes addEventListener / removeEventListener, focus(), focusLast(), blur(), getClientRects() and scrollIntoView(). The focus helpers are great for modals and menus: call focus() on open and keyboard users land on the first focusable child with no wrapper.
browser(): opting out of SSR cleanly
Some components only make sense on the client: anything reading the user's time zone, localStorage, or window size. The usual workaround was a useEffect + mounted flag, which causes an extra render and a flash. React DOM's new browser() suspends on the server, so the nearest <Suspense> fallback is streamed in the HTML and the real component renders on the client:
import { Suspense, use } from 'react'
import { browser } from 'react-dom'
function LocalTime() {
use(browser())
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
return <span>{new Date().toLocaleTimeString()} ({tz})</span>
}
export function Footer() {
return (
<Suspense fallback={<span>--:--</span>}>
<LocalTime />
</Suspense>
)
}
Because it works through use, you can call it conditionally, for example only when no server-provided initial data exists. That makes it a neat fit for client-side data hooks.
Other changes worth knowing
- Context in Server Components: a Server Component can render a context from a
'use client'module directly (<UserContext value={user}>) instead of needing a separate client Provider wrapper. - Trusted Types: React no longer coerces values to strings, so
TrustedHTMLand friends pass through to the browser when arequire-trusted-types-for 'script'CSP is enforced. - Independent Transitions: a slow Transition no longer blocks unrelated ones.
- Strict Mode now double-invokes Effects during hydration too, so hidden cleanup bugs may surface after upgrading. That is a good thing.
Key Takeaways
<ViewTransition>and Fragment refs are stable in React 19.3; you can use them without canary builds.- Use
addTransitionTypeto give animations direction instead of branching in CSS. - Fragment refs remove the "wrapper div just for a ref" pattern.
use(browser())replaces theuseEffect+mountedflag for client-only UI.- Expect a few new Strict Mode warnings during hydration after the upgrade.
