Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add <ViewTransition> Component #31975

Merged
merged 32 commits into from
Jan 8, 2025
Merged

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jan 4, 2025

This will provide the opt-in for using View Transitions in React.

View Transitions only trigger for async updates like startTransition, useDeferredValue, Actions or <Suspense> revealing from fallback to content. Synchronous updates provide an opt-out but also guarantee that they commit immediately which View Transitions can't.

There's no need to opt-in to View Transitions at the "cause" side like event handlers or actions. They don't know what UI will change and whether that has an animated transition described.

Conceptually the <ViewTransition> component is like a DOM fragment that transitions its children in its own isolate/snapshot. The API works by wrapping a DOM node or inner component:

import {ViewTransition} from 'react';

<ViewTransition><Component /></ViewTransition>

The default is name="auto" which will automatically assign a view-transition-name to the inner DOM node. That way you can add a View Transition to a Component without controlling its DOM nodes styling otherwise.

A difference between this and the browser's built-in view-transition-name: auto is that switching the DOM nodes within the <ViewTransition> component preserves the same name so this example cross-fades between the DOM nodes instead of causing an exit and enter:

<ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition>

This becomes especially useful with <Suspense> as this example cross-fades between Skeleton and Content:

<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>

Where as this example triggers an exit of the Skeleton and an enter of the Content:

<Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}>
  <ViewTransition><Content /></ViewTransition>
</Suspense>

Managing instances and keys becomes extra important.

You can also specify an explicit name property for example for animating the same conceptual item from one page onto another. However, best practices is to property namespace these since they can easily collide. It's also useful to add an id to it if available.

<ViewTransition name="my-shared-view">

The model in general is the same as plain view-transition-name except React manages a set of heuristics for when to apply it. A problem with the naive View Transitions model is that it overly opts in every boundary that might transition into transitioning. This is leads to unfortunate effects like things floating around when unrelated updates happen. This leads the whole document to animate which means that nothing is clickable in the meantime. It makes it not useful for smaller and more local transitions. Best practice is to add view-transition-name only right before you're about to need to animate the thing. This is tricky to manage globally on complex apps and is not compositional. Instead we let React manage when a <ViewTransition> "activates" and add/remove the view-transition-name. This is also when React calls startViewTransition behind the scenes while it mutates the DOM.

I've come up with a number of heuristics that I think will make a lot easier to coordinate this. The principle is that only if something that updates that particular boundary do we activate it. I hope that one day maybe browsers will have something like these built-in and we can remove our implementation.

A <ViewTransition> only activates if:

  • If a mounted Component renders a <ViewTransition> within it outside the first DOM node, and it is within the viewport, then that ViewTransition activates as an "enter" animation. This avoids inner "enter" animations trigger when the parent mounts.
  • If an unmounted Component had a <ViewTransition> within it outside the first DOM node, and it was within the viewport, then that ViewTransition activates as an "exit" animation. This avoids inner "exit" animations triggering when the parent unmounts.
  • If an explicitly named <ViewTransition name="..."> is deep within an unmounted tree and one with the same name appears in a mounted tree at the same time, then both are activated as a pair, but only if they're both in the viewport. This avoids these triggering "enter" or "exit" animations when going between parents that don't have a pair.
  • If an already mounted <ViewTransition> is visible and a DOM mutation, that might affect how it's painted, happens within its children but outside any nested <ViewTransition>. This allows it to "cross-fade" between its updates.
  • If an already mounted <ViewTransition> resizes or moves as the result of direct DOM nodes siblings changing or moving around. This allows insertion, deletion and reorders into a list to animate all children. It is only within one DOM node though, to avoid unrelated changes in the parent to trigger this. If an item is outside the viewport before and after, then it's skipped to avoid things flying across the screen.
  • If a <ViewTransition> boundary changes size, due to a DOM mutation within it, then the parent activates (or the root document if there are no more parents). This ensures that the container can cross-fade to avoid abrupt relayout. This can be avoided by using absolutely positioned children. When this can avoid bubbling to the root document, whatever is not animating is still responsive to clicks during the transition.

Conceptually each DOM node has its own default that activates the parent <ViewTransition> or no transition if the parent is the root. That means that if you add a DOM node like <div><ViewTransition><Component /></ViewTransition></div> this won't trigger an "enter" animation since it was the div that was added, not the ViewTransition. Instead, it might cause a cross-fade of the parent ViewTransition or no transition if it had no parent. This ensures that only explicit boundaries perform coarse animations instead of every single node which is really the benefit of the View Transitions model. This ends up working out well for simple cases like switching between two pages immediately while transitioning one floating item that appears on both pages. Because only the floating item transitions by default.

Note that it's possible to add manual view-transition-name with CSS or style={{ viewTransitionName: 'auto' }} that always transitions as long as something else has a <ViewTransition> that activates. For example a <ViewTransition> can wrap a whole page for a cross-fade but inside of it an explicit name can be added to something to ensure it animates as a move when something relates else changes its layout. Instead of just cross-fading it along with the Page which would be the default.

There's more PRs coming with some optimizations, fixes and expanded APIs. This first PR explores the above core heuristic.

Copy link

vercel bot commented Jan 4, 2025

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 7, 2025 4:09am

@react-sizebot
Copy link

react-sizebot commented Jan 4, 2025

Comparing: 379089d...5bd3d66

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 +0.12% 513.40 kB 514.03 kB +0.08% 91.72 kB 91.79 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +5.14% 518.18 kB 544.81 kB +4.71% 92.57 kB 96.93 kB
facebook-www/ReactDOM-prod.classic.js +0.14% 595.26 kB 596.09 kB +0.13% 104.77 kB 104.91 kB
facebook-www/ReactDOM-prod.modern.js +0.14% 585.52 kB 586.36 kB +0.12% 103.21 kB 103.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js +6.31% 394.01 kB 418.86 kB +5.54% 64.04 kB 67.58 kB
oss-experimental/react-art/cjs/react-art.production.js +6.27% 306.09 kB 325.28 kB +5.22% 52.31 kB 55.04 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js +5.70% 446.20 kB 471.63 kB +5.09% 71.66 kB 75.31 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +5.00% 532.91 kB 559.54 kB +4.53% 96.20 kB 100.56 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js +4.80% 573.25 kB 600.79 kB +4.37% 101.21 kB 105.64 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +4.06% 678.17 kB 705.72 kB +3.65% 107.54 kB 111.47 kB
oss-experimental/react-art/cjs/react-art.development.js +3.76% 594.90 kB 617.26 kB +3.22% 95.02 kB 98.08 kB
oss-experimental/react-is/cjs/react-is.production.js +3.46% 4.46 kB 4.61 kB +2.98% 1.08 kB 1.11 kB
oss-experimental/react-is/cjs/react-is.development.js +3.32% 5.00 kB 5.17 kB +3.03% 1.12 kB 1.16 kB
oss-experimental/react-dom/cjs/react-dom-client.development.js +3.25% 981.49 kB 1,013.41 kB +2.99% 165.08 kB 170.01 kB
oss-experimental/react-dom/cjs/react-dom-profiling.development.js +3.20% 997.93 kB 1,029.85 kB +2.93% 167.91 kB 172.83 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +3.20% 998.43 kB 1,030.35 kB +2.90% 168.82 kB 173.71 kB
oss-stable-semver/react-is/cjs/react-is.production.js +2.49% 4.46 kB 4.57 kB +2.51% 1.08 kB 1.10 kB
oss-stable/react-is/cjs/react-is.production.js +2.49% 4.46 kB 4.57 kB +2.51% 1.08 kB 1.10 kB
facebook-react-native/react-is/cjs/ReactIs-prod.js +2.49% 4.47 kB 4.58 kB +2.90% 1.07 kB 1.10 kB
facebook-react-native/react-is/cjs/ReactIs-profiling.js +2.49% 4.47 kB 4.58 kB +2.90% 1.07 kB 1.10 kB
facebook-react-native/react-is/cjs/ReactIs-dev.js +2.40% 4.95 kB 5.07 kB +3.11% 1.09 kB 1.13 kB
oss-stable-semver/react-is/cjs/react-is.development.js +2.38% 5.00 kB 5.12 kB +2.67% 1.12 kB 1.15 kB
oss-stable/react-is/cjs/react-is.development.js +2.38% 5.00 kB 5.12 kB +2.67% 1.12 kB 1.15 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-reconciler/cjs/react-reconciler.production.js +6.31% 394.01 kB 418.86 kB +5.54% 64.04 kB 67.58 kB
oss-experimental/react-art/cjs/react-art.production.js +6.27% 306.09 kB 325.28 kB +5.22% 52.31 kB 55.04 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js +5.70% 446.20 kB 471.63 kB +5.09% 71.66 kB 75.31 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +5.14% 518.18 kB 544.81 kB +4.71% 92.57 kB 96.93 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +5.00% 532.91 kB 559.54 kB +4.53% 96.20 kB 100.56 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js +4.80% 573.25 kB 600.79 kB +4.37% 101.21 kB 105.64 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +4.06% 678.17 kB 705.72 kB +3.65% 107.54 kB 111.47 kB
oss-experimental/react-art/cjs/react-art.development.js +3.76% 594.90 kB 617.26 kB +3.22% 95.02 kB 98.08 kB
oss-experimental/react-is/cjs/react-is.production.js +3.46% 4.46 kB 4.61 kB +2.98% 1.08 kB 1.11 kB
oss-experimental/react-is/cjs/react-is.development.js +3.32% 5.00 kB 5.17 kB +3.03% 1.12 kB 1.16 kB
oss-experimental/react-dom/cjs/react-dom-client.development.js +3.25% 981.49 kB 1,013.41 kB +2.99% 165.08 kB 170.01 kB
oss-experimental/react-dom/cjs/react-dom-profiling.development.js +3.20% 997.93 kB 1,029.85 kB +2.93% 167.91 kB 172.83 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +3.20% 998.43 kB 1,030.35 kB +2.90% 168.82 kB 173.71 kB
oss-stable-semver/react-is/cjs/react-is.production.js +2.49% 4.46 kB 4.57 kB +2.51% 1.08 kB 1.10 kB
oss-stable/react-is/cjs/react-is.production.js +2.49% 4.46 kB 4.57 kB +2.51% 1.08 kB 1.10 kB
facebook-react-native/react-is/cjs/ReactIs-prod.js +2.49% 4.47 kB 4.58 kB +2.90% 1.07 kB 1.10 kB
facebook-react-native/react-is/cjs/ReactIs-profiling.js +2.49% 4.47 kB 4.58 kB +2.90% 1.07 kB 1.10 kB
facebook-react-native/react-is/cjs/ReactIs-dev.js +2.40% 4.95 kB 5.07 kB +3.11% 1.09 kB 1.13 kB
oss-stable-semver/react-is/cjs/react-is.development.js +2.38% 5.00 kB 5.12 kB +2.67% 1.12 kB 1.15 kB
oss-stable/react-is/cjs/react-is.development.js +2.38% 5.00 kB 5.12 kB +2.67% 1.12 kB 1.15 kB
facebook-www/ReactIs-prod.classic.js +1.93% 5.74 kB 5.85 kB +1.93% 1.34 kB 1.37 kB
facebook-www/ReactIs-prod.modern.js +1.93% 5.74 kB 5.85 kB +1.93% 1.34 kB 1.37 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer.production.js +1.90% 35.66 kB 36.34 kB +1.43% 6.71 kB 6.81 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer.production.js +1.90% 35.68 kB 36.36 kB +1.39% 6.74 kB 6.84 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer.production.js +1.90% 35.69 kB 36.37 kB +1.41% 6.74 kB 6.84 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-persistent.production.js +1.90% 35.79 kB 36.46 kB +1.43% 6.73 kB 6.83 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-persistent.production.js +1.90% 35.81 kB 36.49 kB +1.39% 6.76 kB 6.85 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-persistent.production.js +1.90% 35.82 kB 36.50 kB +1.40% 6.76 kB 6.86 kB
facebook-www/ReactIs-dev.classic.js +1.88% 6.34 kB 6.46 kB +2.04% 1.37 kB 1.40 kB
facebook-www/ReactIs-dev.modern.js +1.88% 6.34 kB 6.46 kB +2.04% 1.37 kB 1.40 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer.development.js +1.81% 39.80 kB 40.52 kB +1.32% 7.30 kB 7.40 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer.development.js +1.81% 39.83 kB 40.55 kB +1.31% 7.33 kB 7.43 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer.development.js +1.81% 39.83 kB 40.55 kB +1.30% 7.34 kB 7.43 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-persistent.development.js +1.80% 39.94 kB 40.66 kB +1.31% 7.32 kB 7.41 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-persistent.development.js +1.80% 39.97 kB 40.69 kB +1.29% 7.35 kB 7.44 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-persistent.development.js +1.80% 39.97 kB 40.69 kB +1.31% 7.35 kB 7.45 kB
oss-experimental/react/cjs/react-jsx-dev-runtime.development.js +1.32% 11.18 kB 11.33 kB +1.28% 3.13 kB 3.17 kB
oss-experimental/react/cjs/react-jsx-runtime.development.js +1.30% 11.38 kB 11.53 kB +1.27% 3.15 kB 3.19 kB
oss-experimental/react/cjs/react-jsx-runtime.react-server.development.js +1.23% 12.00 kB 12.15 kB +1.19% 3.28 kB 3.32 kB
oss-experimental/react/cjs/react-jsx-dev-runtime.react-server.development.js +1.23% 12.01 kB 12.15 kB +1.22% 3.28 kB 3.32 kB
oss-experimental/react/cjs/react.react-server.production.js +1.07% 18.52 kB 18.72 kB +1.02% 4.92 kB 4.97 kB
oss-experimental/react/cjs/react.react-server.development.js +0.74% 35.85 kB 36.12 kB +0.65% 8.48 kB 8.54 kB
oss-experimental/react/cjs/react.production.js +0.72% 17.98 kB 18.11 kB +0.77% 4.66 kB 4.70 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.development.js +0.60% 6.80 kB 6.84 kB +0.78% 1.66 kB 1.67 kB
oss-experimental/react/cjs/react.development.js +0.57% 45.49 kB 45.75 kB +0.54% 10.39 kB 10.45 kB
facebook-www/JSXDEVRuntime-dev.classic.js +0.40% 27.97 kB 28.08 kB +0.52% 6.57 kB 6.60 kB
facebook-www/JSXDEVRuntime-dev.modern.js +0.40% 27.97 kB 28.08 kB +0.52% 6.57 kB 6.60 kB
oss-experimental/react-server/cjs/react-server.production.js +0.38% 121.88 kB 122.34 kB +0.30% 21.41 kB 21.47 kB
oss-experimental/react-server/cjs/react-server.development.js +0.32% 177.04 kB 177.61 kB +0.27% 31.20 kB 31.29 kB
facebook-www/ReactReconciler-prod.modern.js +0.25% 450.16 kB 451.27 kB +0.29% 72.37 kB 72.58 kB
facebook-www/ReactReconciler-prod.classic.js +0.24% 460.48 kB 461.59 kB +0.27% 73.93 kB 74.13 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js +0.23% 388.99 kB 389.88 kB +0.24% 63.11 kB 63.26 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js +0.23% 389.02 kB 389.91 kB +0.25% 63.13 kB 63.29 kB
oss-experimental/react-server/cjs/react-server-flight.production.js +0.23% 62.56 kB 62.70 kB +0.43% 12.44 kB 12.49 kB
facebook-www/ReactART-prod.modern.js +0.22% 357.74 kB 358.54 kB +0.27% 59.89 kB 60.05 kB
facebook-www/ReactART-prod.classic.js +0.22% 367.47 kB 368.27 kB +0.22% 61.47 kB 61.61 kB
oss-experimental/react-markup/cjs/react-markup.production.js +0.22% 207.26 kB 207.71 kB +0.19% 38.58 kB 38.66 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js +0.21% 415.14 kB 416.03 kB +0.23% 66.76 kB 66.91 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js +0.21% 415.16 kB 416.05 kB +0.23% 66.78 kB 66.94 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.browser.development.js +0.21% 104.87 kB 105.10 kB +0.28% 19.86 kB 19.92 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +0.21% 105.02 kB 105.25 kB +0.31% 20.05 kB 20.12 kB
oss-experimental/react-client/cjs/react-client-flight.development.js +0.21% 106.35 kB 106.57 kB +0.26% 19.78 kB 19.84 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +0.21% 107.21 kB 107.43 kB +0.29% 20.44 kB 20.50 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +0.21% 107.77 kB 107.99 kB +0.28% 20.58 kB 20.63 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.development.js +0.21% 108.09 kB 108.31 kB +0.38% 20.60 kB 20.67 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +0.21% 109.12 kB 109.35 kB +0.39% 20.74 kB 20.82 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.js +0.20% 218.55 kB 219.00 kB +0.16% 39.27 kB 39.34 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.edge.development.js +0.20% 109.89 kB 110.11 kB +0.38% 20.95 kB 21.03 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js +0.20% 111.76 kB 111.99 kB +0.38% 21.22 kB 21.30 kB

Generated by 🚫 dangerJS against 7f587b3

@Fausto95
Copy link

Fausto95 commented Jan 4, 2025

Wondering how this will work in RN

@li-jia-nan
Copy link

How can I set the transition to duration?

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Jan 5, 2025

Wondering how this will work in RN

The idea is that this can be implemented on the native side with a form of Shared Element Transitions. It would however likely be a new implementation (as opposed to mapping to Layout Animations or Reanimated) to ensure it lines up semantically with the web's View Transitions model so it's not slightly different on native. I think this would likely only be available on the New Architecture.

The way you style these are a bit up on the air since on web it heavily relies on CSS, but the general gist is that a View Transition class could be registered with Native and you'd specify a name class on the JS side. There will be a programmatic API that can work on RN and web.

Similarly, libraries like react-three-fiber could implement GL based transitions as long as it follows the general model and implements the required config hooks that React exposes.

The idea is that this would be a general purpose View Transition / Shared Element Animation model across platforms. That's why this Component is exposed on react and not just react-dom. That said, this is a ton of additional work, so I wouldn't hold my breath for React Native support any time soon. The more likely approach is that you'd have two forked implementations of animating wrappers between Web and Native to start.

sebmarkbage added a commit that referenced this pull request Jan 6, 2025
…but flush it eagerly if we're sync (#31987)

This is a follow up to #31930 and a prerequisite for #31975.

With View Transitions, the commit phase becomes async which means that
other work can sneak in between. We need to be resilient to that.

This PR first refactors the flushMutationEffects and flushLayoutEffects
to use module scope variables to track its arguments so we can defer
them. It shares these with how we were already doing it for
flushPendingEffects.

We also track how far along the commit phase we are so we know what we
have left to flush.

Then callers of flushPassiveEffects become flushPendingEffects. That
helper synchronously flushes any remaining phases we've yet to commit.
That ensure that things are at least consistent if that happens.

Finally, when we are using a scheduled task, we don't do any work. This
ensures that we're not flushing any work too early if we could've
deferred it. This still ensures that we always do flush it before
starting any new work on any root so new roots observe the committed
state.

There are some unfortunate effects that could happen from allowing
things to flush eagerly. Such as if a flushSync sneaks in before
startViewTransition, it'll skip the animation. If it's during a
suspensey font it'll start the transition before the font has loaded
which might be better than breaking flushSync. It'll also potentially
flush passive effects inside the startViewTransition which should
typically be ok.
github-actions bot pushed a commit that referenced this pull request Jan 6, 2025
…but flush it eagerly if we're sync (#31987)

This is a follow up to #31930 and a prerequisite for #31975.

With View Transitions, the commit phase becomes async which means that
other work can sneak in between. We need to be resilient to that.

This PR first refactors the flushMutationEffects and flushLayoutEffects
to use module scope variables to track its arguments so we can defer
them. It shares these with how we were already doing it for
flushPendingEffects.

We also track how far along the commit phase we are so we know what we
have left to flush.

Then callers of flushPassiveEffects become flushPendingEffects. That
helper synchronously flushes any remaining phases we've yet to commit.
That ensure that things are at least consistent if that happens.

Finally, when we are using a scheduled task, we don't do any work. This
ensures that we're not flushing any work too early if we could've
deferred it. This still ensures that we always do flush it before
starting any new work on any root so new roots observe the committed
state.

There are some unfortunate effects that could happen from allowing
things to flush eagerly. Such as if a flushSync sneaks in before
startViewTransition, it'll skip the animation. If it's during a
suspensey font it'll start the transition before the font has loaded
which might be better than breaking flushSync. It'll also potentially
flush passive effects inside the startViewTransition which should
typically be ok.

DiffTrain build for [defffdb](defffdb)
github-actions bot pushed a commit that referenced this pull request Jan 6, 2025
…but flush it eagerly if we're sync (#31987)

This is a follow up to #31930 and a prerequisite for #31975.

With View Transitions, the commit phase becomes async which means that
other work can sneak in between. We need to be resilient to that.

This PR first refactors the flushMutationEffects and flushLayoutEffects
to use module scope variables to track its arguments so we can defer
them. It shares these with how we were already doing it for
flushPendingEffects.

We also track how far along the commit phase we are so we know what we
have left to flush.

Then callers of flushPassiveEffects become flushPendingEffects. That
helper synchronously flushes any remaining phases we've yet to commit.
That ensure that things are at least consistent if that happens.

Finally, when we are using a scheduled task, we don't do any work. This
ensures that we're not flushing any work too early if we could've
deferred it. This still ensures that we always do flush it before
starting any new work on any root so new roots observe the committed
state.

There are some unfortunate effects that could happen from allowing
things to flush eagerly. Such as if a flushSync sneaks in before
startViewTransition, it'll skip the animation. If it's during a
suspensey font it'll start the transition before the font has loaded
which might be better than breaking flushSync. It'll also potentially
flush passive effects inside the startViewTransition which should
typically be ok.

DiffTrain build for [defffdb](defffdb)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jan 6, 2025
…but flush it eagerly if we're sync (facebook#31987)

This is a follow up to facebook#31930 and a prerequisite for facebook#31975.

With View Transitions, the commit phase becomes async which means that
other work can sneak in between. We need to be resilient to that.

This PR first refactors the flushMutationEffects and flushLayoutEffects
to use module scope variables to track its arguments so we can defer
them. It shares these with how we were already doing it for
flushPendingEffects.

We also track how far along the commit phase we are so we know what we
have left to flush.

Then callers of flushPassiveEffects become flushPendingEffects. That
helper synchronously flushes any remaining phases we've yet to commit.
That ensure that things are at least consistent if that happens.

Finally, when we are using a scheduled task, we don't do any work. This
ensures that we're not flushing any work too early if we could've
deferred it. This still ensures that we always do flush it before
starting any new work on any root so new roots observe the committed
state.

There are some unfortunate effects that could happen from allowing
things to flush eagerly. Such as if a flushSync sneaks in before
startViewTransition, it'll skip the animation. If it's during a
suspensey font it'll start the transition before the font has loaded
which might be better than breaking flushSync. It'll also potentially
flush passive effects inside the startViewTransition which should
typically be ok.

DiffTrain build for [defffdb](facebook@defffdb)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jan 6, 2025
…but flush it eagerly if we're sync (facebook#31987)

This is a follow up to facebook#31930 and a prerequisite for facebook#31975.

With View Transitions, the commit phase becomes async which means that
other work can sneak in between. We need to be resilient to that.

This PR first refactors the flushMutationEffects and flushLayoutEffects
to use module scope variables to track its arguments so we can defer
them. It shares these with how we were already doing it for
flushPendingEffects.

We also track how far along the commit phase we are so we know what we
have left to flush.

Then callers of flushPassiveEffects become flushPendingEffects. That
helper synchronously flushes any remaining phases we've yet to commit.
That ensure that things are at least consistent if that happens.

Finally, when we are using a scheduled task, we don't do any work. This
ensures that we're not flushing any work too early if we could've
deferred it. This still ensures that we always do flush it before
starting any new work on any root so new roots observe the committed
state.

There are some unfortunate effects that could happen from allowing
things to flush eagerly. Such as if a flushSync sneaks in before
startViewTransition, it'll skip the animation. If it's during a
suspensey font it'll start the transition before the font has loaded
which might be better than breaking flushSync. It'll also potentially
flush passive effects inside the startViewTransition which should
typically be ok.

DiffTrain build for [defffdb](facebook@defffdb)
sebmarkbage and others added 11 commits January 8, 2025 12:03
…eappearing

We need to know this before we visit the deletions in the snapshot phase so
we know if there is a pair.
That way we'll be able to exclude them if they are not within the viewport.

To do this we need a different mechanisms to detect insertions instead of
Placement effect which was already wrong since it includes moves.
This avoids things flying in, onto, or across the screen unexpectedly.

Unfortunately if it goes from onscreen to offscreen we have no way to cancel
the old state, so that appears as an exit even though it should've just
painted on top of its deleted parent if we had known.
Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>
We were reading from the wrong fiber.

Update the fixture to show that the document and the button remains interactive
due to the inner boundary not resizing.
@sebmarkbage sebmarkbage merged commit a4d122f into facebook:main Jan 8, 2025
186 checks passed
github-actions bot pushed a commit that referenced this pull request Jan 8, 2025
This will provide the opt-in for using [View
Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)
in React.

View Transitions only trigger for async updates like `startTransition`,
`useDeferredValue`, Actions or `<Suspense>` revealing from fallback to
content. Synchronous updates provide an opt-out but also guarantee that
they commit immediately which View Transitions can't.

There's no need to opt-in to View Transitions at the "cause" side like
event handlers or actions. They don't know what UI will change and
whether that has an animated transition described.

Conceptually the `<ViewTransition>` component is like a DOM fragment
that transitions its children in its own isolate/snapshot. The API works
by wrapping a DOM node or inner component:

```js
import {ViewTransition} from 'react';

<ViewTransition><Component /></ViewTransition>
```

The default is `name="auto"` which will automatically assign a
`view-transition-name` to the inner DOM node. That way you can add a
View Transition to a Component without controlling its DOM nodes styling
otherwise.

A difference between this and the browser's built-in
`view-transition-name: auto` is that switching the DOM nodes within the
`<ViewTransition>` component preserves the same name so this example
cross-fades between the DOM nodes instead of causing an exit and enter:

```js
<ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition>
```

This becomes especially useful with `<Suspense>` as this example
cross-fades between Skeleton and Content:

```js
<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>
```

Where as this example triggers an exit of the Skeleton and an enter of
the Content:

```js
<Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}>
  <ViewTransition><Content /></ViewTransition>
</Suspense>
```

Managing instances and keys becomes extra important.

You can also specify an explicit `name` property for example for
animating the same conceptual item from one page onto another. However,
best practices is to property namespace these since they can easily
collide. It's also useful to add an `id` to it if available.

```js
<ViewTransition name="my-shared-view">
```

The model in general is the same as plain `view-transition-name` except
React manages a set of heuristics for when to apply it. A problem with
the naive View Transitions model is that it overly opts in every
boundary that *might* transition into transitioning. This is leads to
unfortunate effects like things floating around when unrelated updates
happen. This leads the whole document to animate which means that
nothing is clickable in the meantime. It makes it not useful for smaller
and more local transitions. Best practice is to add
`view-transition-name` only right before you're about to need to animate
the thing. This is tricky to manage globally on complex apps and is not
compositional. Instead we let React manage when a `<ViewTransition>`
"activates" and add/remove the `view-transition-name`. This is also when
React calls `startViewTransition` behind the scenes while it mutates the
DOM.

I've come up with a number of heuristics that I think will make a lot
easier to coordinate this. The principle is that only if something that
updates that particular boundary do we activate it. I hope that one day
maybe browsers will have something like these built-in and we can remove
our implementation.

A `<ViewTransition>` only activates if:

- If a mounted Component renders a `<ViewTransition>` within it outside
the first DOM node, and it is within the viewport, then that
ViewTransition activates as an "enter" animation. This avoids inner
"enter" animations trigger when the parent mounts.
- If an unmounted Component had a `<ViewTransition>` within it outside
the first DOM node, and it was within the viewport, then that
ViewTransition activates as an "exit" animation. This avoids inner
"exit" animations triggering when the parent unmounts.
- If an explicitly named `<ViewTransition name="...">` is deep within an
unmounted tree and one with the same name appears in a mounted tree at
the same time, then both are activated as a pair, but only if they're
both in the viewport. This avoids these triggering "enter" or "exit"
animations when going between parents that don't have a pair.
- If an already mounted `<ViewTransition>` is visible and a DOM
mutation, that might affect how it's painted, happens within its
children but outside any nested `<ViewTransition>`. This allows it to
"cross-fade" between its updates.
- If an already mounted `<ViewTransition>` resizes or moves as the
result of direct DOM nodes siblings changing or moving around. This
allows insertion, deletion and reorders into a list to animate all
children. It is only within one DOM node though, to avoid unrelated
changes in the parent to trigger this. If an item is outside the
viewport before and after, then it's skipped to avoid things flying
across the screen.
- If a `<ViewTransition>` boundary changes size, due to a DOM mutation
within it, then the parent activates (or the root document if there are
no more parents). This ensures that the container can cross-fade to
avoid abrupt relayout. This can be avoided by using absolutely
positioned children. When this can avoid bubbling to the root document,
whatever is not animating is still responsive to clicks during the
transition.

Conceptually each DOM node has its own default that activates the parent
`<ViewTransition>` or no transition if the parent is the root. That
means that if you add a DOM node like `<div><ViewTransition><Component
/></ViewTransition></div>` this won't trigger an "enter" animation since
it was the div that was added, not the ViewTransition. Instead, it might
cause a cross-fade of the parent ViewTransition or no transition if it
had no parent. This ensures that only explicit boundaries perform coarse
animations instead of every single node which is really the benefit of
the View Transitions model. This ends up working out well for simple
cases like switching between two pages immediately while transitioning
one floating item that appears on both pages. Because only the floating
item transitions by default.

Note that it's possible to add manual `view-transition-name` with CSS or
`style={{ viewTransitionName: 'auto' }}` that always transitions as long
as something else has a `<ViewTransition>` that activates. For example a
`<ViewTransition>` can wrap a whole page for a cross-fade but inside of
it an explicit name can be added to something to ensure it animates as a
move when something relates else changes its layout. Instead of just
cross-fading it along with the Page which would be the default.

There's more PRs coming with some optimizations, fixes and expanded
APIs. This first PR explores the above core heuristic.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>

DiffTrain build for [a4d122f](a4d122f)
github-actions bot pushed a commit that referenced this pull request Jan 8, 2025
This will provide the opt-in for using [View
Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API)
in React.

View Transitions only trigger for async updates like `startTransition`,
`useDeferredValue`, Actions or `<Suspense>` revealing from fallback to
content. Synchronous updates provide an opt-out but also guarantee that
they commit immediately which View Transitions can't.

There's no need to opt-in to View Transitions at the "cause" side like
event handlers or actions. They don't know what UI will change and
whether that has an animated transition described.

Conceptually the `<ViewTransition>` component is like a DOM fragment
that transitions its children in its own isolate/snapshot. The API works
by wrapping a DOM node or inner component:

```js
import {ViewTransition} from 'react';

<ViewTransition><Component /></ViewTransition>
```

The default is `name="auto"` which will automatically assign a
`view-transition-name` to the inner DOM node. That way you can add a
View Transition to a Component without controlling its DOM nodes styling
otherwise.

A difference between this and the browser's built-in
`view-transition-name: auto` is that switching the DOM nodes within the
`<ViewTransition>` component preserves the same name so this example
cross-fades between the DOM nodes instead of causing an exit and enter:

```js
<ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition>
```

This becomes especially useful with `<Suspense>` as this example
cross-fades between Skeleton and Content:

```js
<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>
```

Where as this example triggers an exit of the Skeleton and an enter of
the Content:

```js
<Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}>
  <ViewTransition><Content /></ViewTransition>
</Suspense>
```

Managing instances and keys becomes extra important.

You can also specify an explicit `name` property for example for
animating the same conceptual item from one page onto another. However,
best practices is to property namespace these since they can easily
collide. It's also useful to add an `id` to it if available.

```js
<ViewTransition name="my-shared-view">
```

The model in general is the same as plain `view-transition-name` except
React manages a set of heuristics for when to apply it. A problem with
the naive View Transitions model is that it overly opts in every
boundary that *might* transition into transitioning. This is leads to
unfortunate effects like things floating around when unrelated updates
happen. This leads the whole document to animate which means that
nothing is clickable in the meantime. It makes it not useful for smaller
and more local transitions. Best practice is to add
`view-transition-name` only right before you're about to need to animate
the thing. This is tricky to manage globally on complex apps and is not
compositional. Instead we let React manage when a `<ViewTransition>`
"activates" and add/remove the `view-transition-name`. This is also when
React calls `startViewTransition` behind the scenes while it mutates the
DOM.

I've come up with a number of heuristics that I think will make a lot
easier to coordinate this. The principle is that only if something that
updates that particular boundary do we activate it. I hope that one day
maybe browsers will have something like these built-in and we can remove
our implementation.

A `<ViewTransition>` only activates if:

- If a mounted Component renders a `<ViewTransition>` within it outside
the first DOM node, and it is within the viewport, then that
ViewTransition activates as an "enter" animation. This avoids inner
"enter" animations trigger when the parent mounts.
- If an unmounted Component had a `<ViewTransition>` within it outside
the first DOM node, and it was within the viewport, then that
ViewTransition activates as an "exit" animation. This avoids inner
"exit" animations triggering when the parent unmounts.
- If an explicitly named `<ViewTransition name="...">` is deep within an
unmounted tree and one with the same name appears in a mounted tree at
the same time, then both are activated as a pair, but only if they're
both in the viewport. This avoids these triggering "enter" or "exit"
animations when going between parents that don't have a pair.
- If an already mounted `<ViewTransition>` is visible and a DOM
mutation, that might affect how it's painted, happens within its
children but outside any nested `<ViewTransition>`. This allows it to
"cross-fade" between its updates.
- If an already mounted `<ViewTransition>` resizes or moves as the
result of direct DOM nodes siblings changing or moving around. This
allows insertion, deletion and reorders into a list to animate all
children. It is only within one DOM node though, to avoid unrelated
changes in the parent to trigger this. If an item is outside the
viewport before and after, then it's skipped to avoid things flying
across the screen.
- If a `<ViewTransition>` boundary changes size, due to a DOM mutation
within it, then the parent activates (or the root document if there are
no more parents). This ensures that the container can cross-fade to
avoid abrupt relayout. This can be avoided by using absolutely
positioned children. When this can avoid bubbling to the root document,
whatever is not animating is still responsive to clicks during the
transition.

Conceptually each DOM node has its own default that activates the parent
`<ViewTransition>` or no transition if the parent is the root. That
means that if you add a DOM node like `<div><ViewTransition><Component
/></ViewTransition></div>` this won't trigger an "enter" animation since
it was the div that was added, not the ViewTransition. Instead, it might
cause a cross-fade of the parent ViewTransition or no transition if it
had no parent. This ensures that only explicit boundaries perform coarse
animations instead of every single node which is really the benefit of
the View Transitions model. This ends up working out well for simple
cases like switching between two pages immediately while transitioning
one floating item that appears on both pages. Because only the floating
item transitions by default.

Note that it's possible to add manual `view-transition-name` with CSS or
`style={{ viewTransitionName: 'auto' }}` that always transitions as long
as something else has a `<ViewTransition>` that activates. For example a
`<ViewTransition>` can wrap a whole page for a cross-fade but inside of
it an explicit name can be added to something to ensure it animates as a
move when something relates else changes its layout. Instead of just
cross-fading it along with the Page which would be the default.

There's more PRs coming with some optimizations, fixes and expanded
APIs. This first PR explores the above core heuristic.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>

DiffTrain build for [a4d122f](a4d122f)
sebmarkbage added 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.
sebmarkbage added a commit that referenced this pull request Jan 8, 2025
Stacked on #31975.

We're going to recommend that the primary way you style a View
Transition is using a View Transition Class (and/or Type). These are
only available in the View Transitions v2 spec. When they're not
available it's better to fallback to just not animating instead of
animating with the wrong styling rules applied.

This is already widely supported in Chrome and Safari 18.2. Safari 18.2
usage is still somewhat low but it's rolling out quickly as we speak.

A way to detect this is by just passing the object form to
`startViewTransition` which throws if it's an earlier version. The
object form is required for `types` but luckily classes rolled out at
the same time. Therefore we're only indirectly detecting class support.

This means that in practice Safari 18.0 and 18.1 won't animate. We could
try to only apply the feature detection if you're actually using classes
or types, but that would create an unfortunate ecosystem burden to try
to support names. It also leads to flaky effects when only some
animations work. Better to just disable them all.

Firefox has yet to ship anything. We'll have to look out for how the
feature detection happens there and if they roll things out in different
order but if you ship late, you deal with web compat as the ball lies.
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)
github-actions bot pushed a commit that referenced this pull request Jan 8, 2025
Stacked on #31975.

We're going to recommend that the primary way you style a View
Transition is using a View Transition Class (and/or Type). These are
only available in the View Transitions v2 spec. When they're not
available it's better to fallback to just not animating instead of
animating with the wrong styling rules applied.

This is already widely supported in Chrome and Safari 18.2. Safari 18.2
usage is still somewhat low but it's rolling out quickly as we speak.

A way to detect this is by just passing the object form to
`startViewTransition` which throws if it's an earlier version. The
object form is required for `types` but luckily classes rolled out at
the same time. Therefore we're only indirectly detecting class support.

This means that in practice Safari 18.0 and 18.1 won't animate. We could
try to only apply the feature detection if you're actually using classes
or types, but that would create an unfortunate ecosystem burden to try
to support names. It also leads to flaky effects when only some
animations work. Better to just disable them all.

Firefox has yet to ship anything. We'll have to look out for how the
feature detection happens there and if they roll things out in different
order but if you ship late, you deal with web compat as the ball lies.

DiffTrain build for [38127b2](38127b2)
github-actions bot pushed a commit that referenced this pull request Jan 8, 2025
Stacked on #31975.

We're going to recommend that the primary way you style a View
Transition is using a View Transition Class (and/or Type). These are
only available in the View Transitions v2 spec. When they're not
available it's better to fallback to just not animating instead of
animating with the wrong styling rules applied.

This is already widely supported in Chrome and Safari 18.2. Safari 18.2
usage is still somewhat low but it's rolling out quickly as we speak.

A way to detect this is by just passing the object form to
`startViewTransition` which throws if it's an earlier version. The
object form is required for `types` but luckily classes rolled out at
the same time. Therefore we're only indirectly detecting class support.

This means that in practice Safari 18.0 and 18.1 won't animate. We could
try to only apply the feature detection if you're actually using classes
or types, but that would create an unfortunate ecosystem burden to try
to support names. It also leads to flaky effects when only some
animations work. Better to just disable them all.

Firefox has yet to ship anything. We'll have to look out for how the
feature detection happens there and if they roll things out in different
order but if you ship late, you deal with web compat as the ball lies.

DiffTrain build for [38127b2](38127b2)
sebmarkbage added a commit that referenced this pull request Jan 8, 2025
…tion to finish (#32002)

Stacked on #31975.

View Transitions cannot handle interruptions in that if you start a new
one before the previous one has finished, it just stops and then
restarts. It doesn't seamlessly transition into the new transition.

This is generally considered a bad thing but I actually think it's quite
good for fire-and-forget animations (gestures is another story). There
are too many examples of bad animations in fast interactions because the
scenario wasn't predicted. Like overlapping toasts or stacked layers
that look bad. The only case interrupts tend to work well is when you do
a strict reversal of an animation like returning to the page you just
left or exiting a modal just being opened. However, we're limited by the
platform even in that regard.

I think one reason interruptions have traditionally been seen as good is
because it's hard if you have a synchronous framework to not interrupt
since your application state has already moved on. We don't have that
limitation since we can suspend commits. We can do all the work to
prepare for the next commit by rendering while the animation is going
but then delay the commit until the previous one finishes.

Another technical limitation earlier animation libraries suffered from
is only have the option to either interrupt or sequence animations since
it's modeling just one change set. Like showing one toast at a time.
That's bad. We don't have that limitation because we can interrupt a
previously suspended commit and start working on a new one instead.
That's what we do for suspended transitions in general. The net effect
is that we batch the commits.

Therefore if you get multiple toasts flying in fast, they can animate as
a batch in together all at once instead of overlapping slightly or being
staggered. Interruptions (often) bad. Staggered animations bad. Batched
animations good.

This PR stashes the currently active View Transition with an expando on
the container that's animating (currently always document). This is
similar to what we do with event handlers etc. We reason we do this with
an expando is that if you have multiple Reacts on the same page they
need to wait for each other. However, one of those might also be the SSR
runtime. So this lets us wait for the SSR runtime's animations to finish
before starting client ones. This could really be a more generic name
since this should ideally be shared across frameworks. It's kind of
strange that this property doesn't already exist in the DOM given that
there can only be one. It would be useful to be able to coordinate this
across libraries.
github-actions bot pushed a commit that referenced this pull request Jan 8, 2025
…tion to finish (#32002)

Stacked on #31975.

View Transitions cannot handle interruptions in that if you start a new
one before the previous one has finished, it just stops and then
restarts. It doesn't seamlessly transition into the new transition.

This is generally considered a bad thing but I actually think it's quite
good for fire-and-forget animations (gestures is another story). There
are too many examples of bad animations in fast interactions because the
scenario wasn't predicted. Like overlapping toasts or stacked layers
that look bad. The only case interrupts tend to work well is when you do
a strict reversal of an animation like returning to the page you just
left or exiting a modal just being opened. However, we're limited by the
platform even in that regard.

I think one reason interruptions have traditionally been seen as good is
because it's hard if you have a synchronous framework to not interrupt
since your application state has already moved on. We don't have that
limitation since we can suspend commits. We can do all the work to
prepare for the next commit by rendering while the animation is going
but then delay the commit until the previous one finishes.

Another technical limitation earlier animation libraries suffered from
is only have the option to either interrupt or sequence animations since
it's modeling just one change set. Like showing one toast at a time.
That's bad. We don't have that limitation because we can interrupt a
previously suspended commit and start working on a new one instead.
That's what we do for suspended transitions in general. The net effect
is that we batch the commits.

Therefore if you get multiple toasts flying in fast, they can animate as
a batch in together all at once instead of overlapping slightly or being
staggered. Interruptions (often) bad. Staggered animations bad. Batched
animations good.

This PR stashes the currently active View Transition with an expando on
the container that's animating (currently always document). This is
similar to what we do with event handlers etc. We reason we do this with
an expando is that if you have multiple Reacts on the same page they
need to wait for each other. However, one of those might also be the SSR
runtime. So this lets us wait for the SSR runtime's animations to finish
before starting client ones. This could really be a more generic name
since this should ideally be shared across frameworks. It's kind of
strange that this property doesn't already exist in the DOM given that
there can only be one. It would be useful to be able to coordinate this
across libraries.

DiffTrain build for [98418e8](98418e8)
github-actions bot pushed a commit that referenced this pull request Jan 8, 2025
…tion to finish (#32002)

Stacked on #31975.

View Transitions cannot handle interruptions in that if you start a new
one before the previous one has finished, it just stops and then
restarts. It doesn't seamlessly transition into the new transition.

This is generally considered a bad thing but I actually think it's quite
good for fire-and-forget animations (gestures is another story). There
are too many examples of bad animations in fast interactions because the
scenario wasn't predicted. Like overlapping toasts or stacked layers
that look bad. The only case interrupts tend to work well is when you do
a strict reversal of an animation like returning to the page you just
left or exiting a modal just being opened. However, we're limited by the
platform even in that regard.

I think one reason interruptions have traditionally been seen as good is
because it's hard if you have a synchronous framework to not interrupt
since your application state has already moved on. We don't have that
limitation since we can suspend commits. We can do all the work to
prepare for the next commit by rendering while the animation is going
but then delay the commit until the previous one finishes.

Another technical limitation earlier animation libraries suffered from
is only have the option to either interrupt or sequence animations since
it's modeling just one change set. Like showing one toast at a time.
That's bad. We don't have that limitation because we can interrupt a
previously suspended commit and start working on a new one instead.
That's what we do for suspended transitions in general. The net effect
is that we batch the commits.

Therefore if you get multiple toasts flying in fast, they can animate as
a batch in together all at once instead of overlapping slightly or being
staggered. Interruptions (often) bad. Staggered animations bad. Batched
animations good.

This PR stashes the currently active View Transition with an expando on
the container that's animating (currently always document). This is
similar to what we do with event handlers etc. We reason we do this with
an expando is that if you have multiple Reacts on the same page they
need to wait for each other. However, one of those might also be the SSR
runtime. So this lets us wait for the SSR runtime's animations to finish
before starting client ones. This could really be a more generic name
since this should ideally be shared across frameworks. It's kind of
strange that this property doesn't already exist in the DOM given that
there can only be one. It would be useful to be able to coordinate this
across libraries.

DiffTrain build for [98418e8](98418e8)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jan 8, 2025
…tion to finish (facebook#32002)

Stacked on facebook#31975.

View Transitions cannot handle interruptions in that if you start a new
one before the previous one has finished, it just stops and then
restarts. It doesn't seamlessly transition into the new transition.

This is generally considered a bad thing but I actually think it's quite
good for fire-and-forget animations (gestures is another story). There
are too many examples of bad animations in fast interactions because the
scenario wasn't predicted. Like overlapping toasts or stacked layers
that look bad. The only case interrupts tend to work well is when you do
a strict reversal of an animation like returning to the page you just
left or exiting a modal just being opened. However, we're limited by the
platform even in that regard.

I think one reason interruptions have traditionally been seen as good is
because it's hard if you have a synchronous framework to not interrupt
since your application state has already moved on. We don't have that
limitation since we can suspend commits. We can do all the work to
prepare for the next commit by rendering while the animation is going
but then delay the commit until the previous one finishes.

Another technical limitation earlier animation libraries suffered from
is only have the option to either interrupt or sequence animations since
it's modeling just one change set. Like showing one toast at a time.
That's bad. We don't have that limitation because we can interrupt a
previously suspended commit and start working on a new one instead.
That's what we do for suspended transitions in general. The net effect
is that we batch the commits.

Therefore if you get multiple toasts flying in fast, they can animate as
a batch in together all at once instead of overlapping slightly or being
staggered. Interruptions (often) bad. Staggered animations bad. Batched
animations good.

This PR stashes the currently active View Transition with an expando on
the container that's animating (currently always document). This is
similar to what we do with event handlers etc. We reason we do this with
an expando is that if you have multiple Reacts on the same page they
need to wait for each other. However, one of those might also be the SSR
runtime. So this lets us wait for the SSR runtime's animations to finish
before starting client ones. This could really be a more generic name
since this should ideally be shared across frameworks. It's kind of
strange that this property doesn't already exist in the DOM given that
there can only be one. It would be useful to be able to coordinate this
across libraries.

DiffTrain build for [98418e8](facebook@98418e8)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jan 8, 2025
…tion to finish (facebook#32002)

Stacked on facebook#31975.

View Transitions cannot handle interruptions in that if you start a new
one before the previous one has finished, it just stops and then
restarts. It doesn't seamlessly transition into the new transition.

This is generally considered a bad thing but I actually think it's quite
good for fire-and-forget animations (gestures is another story). There
are too many examples of bad animations in fast interactions because the
scenario wasn't predicted. Like overlapping toasts or stacked layers
that look bad. The only case interrupts tend to work well is when you do
a strict reversal of an animation like returning to the page you just
left or exiting a modal just being opened. However, we're limited by the
platform even in that regard.

I think one reason interruptions have traditionally been seen as good is
because it's hard if you have a synchronous framework to not interrupt
since your application state has already moved on. We don't have that
limitation since we can suspend commits. We can do all the work to
prepare for the next commit by rendering while the animation is going
but then delay the commit until the previous one finishes.

Another technical limitation earlier animation libraries suffered from
is only have the option to either interrupt or sequence animations since
it's modeling just one change set. Like showing one toast at a time.
That's bad. We don't have that limitation because we can interrupt a
previously suspended commit and start working on a new one instead.
That's what we do for suspended transitions in general. The net effect
is that we batch the commits.

Therefore if you get multiple toasts flying in fast, they can animate as
a batch in together all at once instead of overlapping slightly or being
staggered. Interruptions (often) bad. Staggered animations bad. Batched
animations good.

This PR stashes the currently active View Transition with an expando on
the container that's animating (currently always document). This is
similar to what we do with event handlers etc. We reason we do this with
an expando is that if you have multiple Reacts on the same page they
need to wait for each other. However, one of those might also be the SSR
runtime. So this lets us wait for the SSR runtime's animations to finish
before starting client ones. This could really be a more generic name
since this should ideally be shared across frameworks. It's kind of
strange that this property doesn't already exist in the DOM given that
there can only be one. It would be useful to be able to coordinate this
across libraries.

DiffTrain build for [98418e8](facebook@98418e8)
@nilsingwersen
Copy link

That's a great feature, is this getting a new minor?

@Ctrlmonster
Copy link

Similarly, libraries like react-three-fiber could implement GL based transitions as long as it follows the general model and implements the required config hooks that React exposes.

Hey, not sure if someone else from the r3f side gave any feedback, but this wouldn't be able to be implemented in r3f directly as that would assume way too much about how the user's app (3D-)rendering works. So it's just react native and DOM.

Screenshot_20250110-011408

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.

9 participants