Skip to content

[Fiber] Use className on <ViewTransition> to assign view-transition-class - #31999

Merged
sebmarkbage merged 3 commits into
react:mainfrom
sebmarkbage:viewtransitionclass
Jan 8, 2025
Merged

[Fiber] Use className on <ViewTransition> to assign view-transition-class#31999
sebmarkbage merged 3 commits into
react:mainfrom
sebmarkbage:viewtransitionclass

Conversation

@sebmarkbage

@sebmarkbage sebmarkbage commented Jan 6, 2025

Copy link
Copy Markdown
Contributor

Stacked on #31975.

This is the primary way we recommend styling your View Transitions since it allows for reusable styling such as a CSS library specializing in View Transitions in a way that's composable and without naming conflicts. E.g.

<ViewTransition className="enter-slide-in exit-fade-out update-cross-fade">

This doesn't change the HTML class attribute. It's not a CSS class. Instead it assign the view-transition-class style prop of the underlying DOM node while it's transitioning.

You can also just use <div style={{viewTransitionClass: ...}}> on the DOM node but it's convenient to control the Transition completely from the outside and conceptually we're transitioning the whole fragment. You can even make Transition components that just wraps existing components. <RevealTransition><Component /></RevealTransition> this way.

Since you can also have multiple wrappers for different circumstances it allows React's heuristics to use different classes for different scenarios. We'll likely add more options like configuring different classes for different types or scenarios that can't be described by CSS alone.

CSS Modules

import transitions from './transitions.module.css';

<ViewTransition className={transitions.bounceIn}>...</ViewTransition>

CSS Modules works well with this strategy because you can have globally unique namespaces and define your transitions in the CSS modules as a library that you can import. As seen in the fixture here.

I did notice an unfortunate bug in how CSS Modules (at least in Webpack) generates class names. Sometimes the + character is used in the hash of the class name which is not valid for view-transition-class and so it breaks. I had to rename my class names until the hash yielded something different to work around it. Ideally that bug gets fixed soon.

className, rly?

className isn't exactly the most loved property name, however, I'm using className here too for consistency. Even though in this case there's no direct equivalent DOM property name. The CSS property is named viewTransitionClass, but the "viewTransition" prefix is implied by the Component it is on in this case. For most people the fact that this is actually a different namespace than other CSS classes doesn't matter. You'll most just use a CSS library anyway and conceptually you're just assigning classes the same way as className on a DOM node.

But if we ever rename the class prop then we can do that for this one as well.

@vercel

vercel Bot commented Jan 6, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 6, 2025 8:57pm

@react-sizebot

react-sizebot commented Jan 6, 2025

Copy link
Copy Markdown

Comparing: a4d122f2d192fe0b6480e669cca43c8f953aaf85...648c923a82a2c374219ff2f1d5b96cc3e082205f

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB +0.05% 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 513.86 kB 513.86 kB = 91.77 kB 91.77 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB +0.05% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.22% 544.65 kB 545.84 kB +0.18% 96.91 kB 97.08 kB
facebook-www/ReactDOM-prod.classic.js = 595.76 kB 595.76 kB = 104.85 kB 104.86 kB
facebook-www/ReactDOM-prod.modern.js = 586.19 kB 586.19 kB = 103.30 kB 103.31 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.22% 544.65 kB 545.84 kB +0.18% 96.91 kB 97.08 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +0.21% 559.38 kB 560.57 kB +0.17% 100.52 kB 100.69 kB

Generated by 🚫 dangerJS against c3c8c34

I did have to rename my classes because there's a bug where generated
CSS Module class names can have the `+` character in them which is invalid
for View Transition Class Names.
@sebmarkbage
sebmarkbage merged commit 3a5496b into react:main Jan 8, 2025
github-actions Bot pushed a commit that referenced this pull request Jan 8, 2025
…lass (#31999)

Stacked on #31975.

This is the primary way we recommend styling your View Transitions since
it allows for reusable styling such as a CSS library specializing in
View Transitions in a way that's composable and without naming
conflicts. E.g.

```js
<ViewTransition className="enter-slide-in exit-fade-out update-cross-fade">
```

This doesn't change the HTML `class` attribute. It's not a CSS class.
Instead it assign the `view-transition-class` style prop of the
underlying DOM node while it's transitioning.

You can also just use `<div style={{viewTransitionClass: ...}}>` on the
DOM node but it's convenient to control the Transition completely from
the outside and conceptually we're transitioning the whole fragment. You
can even make Transition components that just wraps existing components.
`<RevealTransition><Component /></RevealTransition>` this way.

Since you can also have multiple wrappers for different circumstances it
allows React's heuristics to use different classes for different
scenarios. We'll likely add more options like configuring different
classes for different `types` or scenarios that can't be described by
CSS alone.

## CSS Modules

```js
import transitions from './transitions.module.css';

<ViewTransition className={transitions.bounceIn}>...</ViewTransition>
```

CSS Modules works well with this strategy because you can have globally
unique namespaces and define your transitions in the CSS modules as a
library that you can import. [As seen in the fixture
here.](8b91b37#diff-b4d9854171ffdac4d2c01be92a5eff4f8e9e761e6af953094f99ca243b054a85R11)

I did notice an unfortunate bug in how CSS Modules (at least in Webpack)
generates class names. Sometimes the `+` character is used in the hash
of the class name which is not valid for `view-transition-class` and so
it breaks. I had to rename my class names until the hash yielded
something different to work around it. Ideally that bug gets fixed soon.

## className, rly?

`className` isn't exactly the most loved property name, however, I'm
using `className` here too for consistency. Even though in this case
there's no direct equivalent DOM property name. The CSS property is
named `viewTransitionClass`, but the "viewTransition" prefix is implied
by the Component it is on in this case. For most people the fact that
this is actually a different namespace than other CSS classes doesn't
matter. You'll most just use a CSS library anyway and conceptually
you're just assigning classes the same way as `className` on a DOM node.

But if we ever rename the `class` prop then we can do that for this one
as well.

DiffTrain build for [3a5496b](3a5496b)
github-actions Bot pushed a commit that referenced this pull request Jan 8, 2025
…lass (#31999)

Stacked on #31975.

This is the primary way we recommend styling your View Transitions since
it allows for reusable styling such as a CSS library specializing in
View Transitions in a way that's composable and without naming
conflicts. E.g.

```js
<ViewTransition className="enter-slide-in exit-fade-out update-cross-fade">
```

This doesn't change the HTML `class` attribute. It's not a CSS class.
Instead it assign the `view-transition-class` style prop of the
underlying DOM node while it's transitioning.

You can also just use `<div style={{viewTransitionClass: ...}}>` on the
DOM node but it's convenient to control the Transition completely from
the outside and conceptually we're transitioning the whole fragment. You
can even make Transition components that just wraps existing components.
`<RevealTransition><Component /></RevealTransition>` this way.

Since you can also have multiple wrappers for different circumstances it
allows React's heuristics to use different classes for different
scenarios. We'll likely add more options like configuring different
classes for different `types` or scenarios that can't be described by
CSS alone.

## CSS Modules

```js
import transitions from './transitions.module.css';

<ViewTransition className={transitions.bounceIn}>...</ViewTransition>
```

CSS Modules works well with this strategy because you can have globally
unique namespaces and define your transitions in the CSS modules as a
library that you can import. [As seen in the fixture
here.](8b91b37#diff-b4d9854171ffdac4d2c01be92a5eff4f8e9e761e6af953094f99ca243b054a85R11)

I did notice an unfortunate bug in how CSS Modules (at least in Webpack)
generates class names. Sometimes the `+` character is used in the hash
of the class name which is not valid for `view-transition-class` and so
it breaks. I had to rename my class names until the hash yielded
something different to work around it. Ideally that bug gets fixed soon.

## className, rly?

`className` isn't exactly the most loved property name, however, I'm
using `className` here too for consistency. Even though in this case
there's no direct equivalent DOM property name. The CSS property is
named `viewTransitionClass`, but the "viewTransition" prefix is implied
by the Component it is on in this case. For most people the fact that
this is actually a different namespace than other CSS classes doesn't
matter. You'll most just use a CSS library anyway and conceptually
you're just assigning classes the same way as `className` on a DOM node.

But if we ever rename the `class` prop then we can do that for this one
as well.

DiffTrain build for [3a5496b](3a5496b)
leochiu-a added a commit to leochiu-a/open-resume that referenced this pull request Aug 21, 2026
The rules lived at the bottom of globals.css, four hundred lines from the only
component they describe. React intends them to be a module — react/react#31999
introduced the `enter`/`exit` props with `import transitions from
'./transitions.module.css'` as the motivating usage — and the module earns more
than proximity: the two class names are hashed, so `page.tsx` has to reference
them through this file's exports instead of spelling them, and `@keyframes` are
hashed with them, which is what lets the keyframe be called `fade`.

The root rules need `:global(…)`. `root` there names the browser's own snapshot,
but CSS Modules treats it as an identifier in a place identifiers get hashed, so
written plainly they compile to `::view-transition-old(…__root)` — a group that
does not exist. Nothing errors. The rules stop matching, root falls back to the
browser's 250ms default, and the flicker they exist to prevent comes back. That
form is also what vercel/next.js#77321 landed on, because LightningCSS and
PostCSS disagree about whether to hash that selector. The pragma from the same PR
is the wrong tool here: it turns localisation off for the whole file, and the two
classes depend on it.

Reduced motion stays in globals.css. It is not about this transition — it is the
document's answer for any of them, including ones added later that would
otherwise ship without it.

The new spec measures the animation the browser actually ran, because every way
this breaks is silent: a dropped `startTransition`, a hash containing `+` (which
is not a legal view-transition-class, per that React PR), or a missing
`:global(…)` all leave a column that still swaps and a panel that still works.
Removing either `:global(…)` fails both tests — root reports 250ms on the way in
and is drawn again on the way out.
leochiu-a added a commit to leochiu-a/open-resume that referenced this pull request Aug 21, 2026
…cked

Code review found the comment in `appearance-transition.module.css` crediting
react/react#31999 with the `enter`/`exit` props. That PR introduced
`className`; the props came earlier, in #31975. Everything else it is cited for —
CSS Modules as the intended home for these classes, the `+`-in-hash hazard —
holds. The commit that introduced the error keeps it in history; the file no
longer repeats it.

Two other comments claimed more than the code did. The type declaration said it
catches a misspelled `enter`, which is true of prop names and not of their values:
`"auto" | "none" | string` reads as a constraint and collapses to `string`, so the
union is gone and the two special values are documented instead. The spec said a
skipped transition "fails with the empty array", which was a TypeError on
`ran.types` — it now waits for a transition to be recorded and asserts the count,
which is also what closes a race the review spotted: both helpers return when the
DOM commits, and the DOM commits before `ready` resolves.

`changeMode(true)`/`changeMode(false)` said nothing at the call sites, so the two
directions are named functions. The CSS module's import was bound to `modeSwap`,
naming something other than the file.

The duration was written four times across the module and the spec. It is now
`--appearance-swap-duration`, declared once, and the spec reads it rather than
carrying a copy — which found something: the production build minifies `180ms` to
`.18s`, so a test that assumed the unit compares 0.18 against animations measured
in milliseconds.

`::view-transition-new(root) { animation: none }` was missing. The recipe pairs it
with the `display: none` on the old frame, and Chrome happens to create no such
animation, so the result rested on that rather than on the rule.

Reduced motion is app-wide and shipped with nothing checking it, which for an
accessibility rule is the wrong way round — there is a test now.

`docs/interface.md` documented the theme wipe as the app's only View Transition
and is where AGENTS.md says the editor's layout belongs. It now covers the
appearance swap and the reduced-motion policy, and the theme-wipe paragraph no
longer implies it is alone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants