Skip to content

[Fizz] Add vt- prefix attributes to annotate <ViewTransition> in HTML #33206

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

Merged
merged 10 commits into from
May 15, 2025

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented May 14, 2025

Stacked on #33194 and #33200.

When Suspense boundaries reveal during streaming, the Fizz runtime will be responsible for animating the reveal if necessary (not in this PR). However, for the future runtime to know what to do it needs to know about the <ViewTransition> configuration to apply.

Ofc, these are virtual nodes that disappear from the HTML. We could model them as comments like we do with other virtual nodes like Suspense and Activity. However, that doesn't let us target them with querySelector and CSS (for no-JS transitions). We also don't have to model every ViewTransition since not every combination can happen using only the server runtime. So instead this collapses <ViewTransition> and applies the configuration to the inner DOM nodes.

<ViewTransition name="hi">
  <div />
  <div />
</ViewTransition>

Becomes:

<div vt-name="hi" vt-update="auto"></div>
<div vt-name="hi_1" vt-update="auto"></div>

I use vt- prefix as opposed to data- to keep these virtual attributes away from user specific ones but we're effectively claiming this namespace.

There are four triggers vt-update, vt-enter, vt-exit and vt-share. The server resolves which ones might apply to this DOM node. The value represents the class name (after resolving view-transition-type mappings) or "auto" if no specific class name is needed but this is still a trigger.

The value can also be "none". This is different from missing because for example an vt-update="none" will block mutations inside it from triggering the boundary where as a missing vt-update would bubble up to be handled by a parent.

vt-name is technically only necessary when vt-share is specified to find a pair. However, since an explicit name can also be used to target specific CSS selectors, we include it even for other cases.

We want to exclude as many of these annotations as possible.

vt-enter can only affect the first DOM node inside a Suspense boundary's content since the reveal would cause it to enter but nothing deeper inside. Similarly vt-exit can only affect the first DOM node inside a fallback. So for every other case we can exclude them. (For future MPA ViewTransitions of the whole document it might also be something we annotate to children inside the <body> as well.) Ideally we'd only include vt-enter for Suspense boundaries that actually flushed a fallback but since we prepare all that content earlier it's hard to know.

vt-share can be anywhere inside an fallback or content. Technically we don't have to include it outside the root most Suspense boundary or for boundaries that are inlined into the root shell. However, this is tricky to detect. It would also not be correct for future MPA ViewTransitions because in that case the shared scenario can affect anything in the two documents so it needs to be in every node everywhere which is effectively what we do. If a share class is specified but it has no explicit name, we can exclude it since it can't match anything.

vt-update is only necessary if something below or a sibling might update like a Suspense boundary. However, since we don't know when rendering a segment if it'll later asynchronously add a Suspense boundary later we have to assume that anywhere might have a child. So these are always included. We collapse to use the inner most one when directly nested though since that's the one that ends up winning.

There are some weird edge cases that can't be fully modeled by the lack of virtual nodes.

@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label May 14, 2025

expect(getVisibleChildren(container)).toEqual(
<div>
<div vt-name="«R0»" vt-update="auto" vt-share="auto">
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting special case. Removing the fallback and inserting the content is an update to the parent <ViewTransition> but since it has no representation without a virtual node there's nothing to handle that update.

Instead, we can rewrite this scenario as a shared element transition between the fallback and the content. Effectively it gets rewritten to as if there was a <ViewTransition> directly inside each one with a shared name.

This is currently the only case where we have to include an auto-generated name in the HTML to pair these, instead of letting the Fizz runtime generate the name.

@sebmarkbage sebmarkbage force-pushed the fizzvt branch 2 times, most recently from 9a30448 to 2a3b7a3 Compare May 14, 2025 23:08
@react-sizebot
Copy link

Comparing: 96eb84e...2a3b7a3

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 = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 529.74 kB 529.74 kB = 93.49 kB 93.49 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 = 651.48 kB 651.48 kB = 114.76 kB 114.75 kB
facebook-www/ReactDOM-prod.classic.js = 675.72 kB 675.72 kB = 118.85 kB 118.84 kB
facebook-www/ReactDOM-prod.modern.js = 666.00 kB 666.00 kB = 117.23 kB 117.23 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.js +3.97% 240.43 kB 249.98 kB +3.27% 43.25 kB 44.66 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.js +3.63% 262.53 kB 272.06 kB +3.21% 45.56 kB 47.02 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.js +3.60% 264.45 kB 273.98 kB +3.20% 46.73 kB 48.22 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.js +3.55% 268.48 kB 278.01 kB +3.12% 47.69 kB 49.18 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.development.js +3.48% 7.66 kB 7.93 kB +1.64% 1.64 kB 1.67 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.development.js +3.48% 7.66 kB 7.93 kB +1.64% 1.64 kB 1.67 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.development.js +3.48% 7.66 kB 7.93 kB +1.64% 1.64 kB 1.67 kB
oss-experimental/react-markup/cjs/react-markup.production.js +3.41% 219.82 kB 227.33 kB +2.39% 40.67 kB 41.65 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.production.js +3.35% 6.37 kB 6.58 kB +1.73% 1.56 kB 1.59 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.production.js +3.35% 6.37 kB 6.58 kB +1.73% 1.56 kB 1.59 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.production.js +3.35% 6.37 kB 6.58 kB +1.73% 1.56 kB 1.59 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +2.95% 227.56 kB 234.28 kB +2.58% 41.88 kB 42.97 kB
oss-experimental/react-markup/cjs/react-markup.react-server.production.js +2.34% 320.94 kB 328.44 kB +1.69% 60.02 kB 61.03 kB
facebook-www/ReactDOMServer-prod.classic.js +2.24% 226.03 kB 231.09 kB +1.67% 40.61 kB 41.29 kB
facebook-www/ReactDOMServer-prod.modern.js +2.17% 223.35 kB 228.18 kB +1.67% 40.28 kB 40.95 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +2.11% 370.83 kB 378.64 kB +1.62% 66.87 kB 67.95 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.js +2.01% 234.11 kB 238.81 kB +1.55% 41.84 kB 42.49 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-server.bun.production.js +3.97% 240.43 kB 249.98 kB +3.27% 43.25 kB 44.66 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.js +3.63% 262.53 kB 272.06 kB +3.21% 45.56 kB 47.02 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.js +3.60% 264.45 kB 273.98 kB +3.20% 46.73 kB 48.22 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.js +3.55% 268.48 kB 278.01 kB +3.12% 47.69 kB 49.18 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.development.js +3.48% 7.66 kB 7.93 kB +1.64% 1.64 kB 1.67 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.development.js +3.48% 7.66 kB 7.93 kB +1.64% 1.64 kB 1.67 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.development.js +3.48% 7.66 kB 7.93 kB +1.64% 1.64 kB 1.67 kB
oss-experimental/react-markup/cjs/react-markup.production.js +3.41% 219.82 kB 227.33 kB +2.39% 40.67 kB 41.65 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.production.js +3.35% 6.37 kB 6.58 kB +1.73% 1.56 kB 1.59 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.production.js +3.35% 6.37 kB 6.58 kB +1.73% 1.56 kB 1.59 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.production.js +3.35% 6.37 kB 6.58 kB +1.73% 1.56 kB 1.59 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +2.95% 227.56 kB 234.28 kB +2.58% 41.88 kB 42.97 kB
oss-experimental/react-markup/cjs/react-markup.react-server.production.js +2.34% 320.94 kB 328.44 kB +1.69% 60.02 kB 61.03 kB
facebook-www/ReactDOMServer-prod.classic.js +2.24% 226.03 kB 231.09 kB +1.67% 40.61 kB 41.29 kB
facebook-www/ReactDOMServer-prod.modern.js +2.17% 223.35 kB 228.18 kB +1.67% 40.28 kB 40.95 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +2.11% 370.83 kB 378.64 kB +1.62% 66.87 kB 67.95 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.js +2.01% 234.11 kB 238.81 kB +1.55% 41.84 kB 42.49 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.js +1.96% 239.19 kB 243.89 kB +1.48% 43.73 kB 44.38 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +1.92% 352.18 kB 358.93 kB +1.73% 67.02 kB 68.18 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +1.81% 415.94 kB 423.45 kB +1.72% 72.45 kB 73.70 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +1.79% 419.98 kB 427.49 kB +1.76% 73.06 kB 74.34 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +1.79% 420.98 kB 428.50 kB +1.77% 73.27 kB 74.57 kB
oss-experimental/react-server/cjs/react-server.production.js +1.61% 131.58 kB 133.70 kB +1.44% 22.80 kB 23.13 kB
oss-experimental/react-markup/cjs/react-markup.development.js +1.47% 362.05 kB 367.37 kB +1.06% 65.28 kB 65.98 kB
facebook-www/ReactDOMServer-dev.modern.js +1.42% 379.80 kB 385.19 kB +0.91% 68.19 kB 68.80 kB
facebook-www/ReactDOMServer-dev.classic.js +1.41% 383.26 kB 388.64 kB +0.91% 68.72 kB 69.34 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.development.js +1.34% 393.72 kB 399.00 kB +0.89% 69.93 kB 70.55 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.development.js +1.34% 393.73 kB 399.00 kB +0.89% 69.93 kB 70.55 kB
oss-experimental/react-server/cjs/react-server.development.js +1.22% 190.79 kB 193.11 kB +1.09% 33.26 kB 33.62 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.production.js +1.06% 219.11 kB 221.44 kB +0.87% 40.36 kB 40.72 kB
oss-stable/react-dom/cjs/react-dom-server.bun.production.js +1.06% 219.19 kB 221.51 kB +0.87% 40.39 kB 40.74 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.production.js +1.03% 214.14 kB 216.36 kB +0.85% 39.06 kB 39.39 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.production.js +1.03% 214.17 kB 216.38 kB +0.84% 39.08 kB 39.41 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.production.js +1.01% 218.66 kB 220.87 kB +0.84% 40.81 kB 41.15 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.production.js +1.01% 218.68 kB 220.90 kB +0.84% 40.83 kB 41.18 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.js +0.99% 233.99 kB 236.31 kB +0.78% 42.04 kB 42.37 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.js +0.99% 234.06 kB 236.39 kB +0.78% 42.06 kB 42.39 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.js +0.99% 235.78 kB 238.11 kB +0.82% 42.99 kB 43.34 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.js +0.99% 235.86 kB 238.18 kB +0.82% 43.01 kB 43.37 kB
oss-experimental/react-markup/cjs/react-markup.react-server.development.js +0.98% 544.02 kB 549.33 kB +0.75% 97.56 kB 98.30 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.production.js +0.97% 239.19 kB 241.52 kB +0.78% 43.98 kB 44.32 kB
oss-stable/react-dom/cjs/react-dom-server.edge.production.js +0.97% 239.27 kB 241.59 kB +0.78% 44.00 kB 44.35 kB
oss-stable-semver/react-server/cjs/react-server.production.js +0.72% 117.52 kB 118.36 kB +0.54% 20.92 kB 21.03 kB
oss-stable/react-server/cjs/react-server.production.js +0.72% 117.52 kB 118.36 kB +0.54% 20.92 kB 21.03 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.development.js +0.61% 365.72 kB 367.94 kB +0.40% 66.46 kB 66.72 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.development.js +0.61% 365.72 kB 367.94 kB +0.40% 66.46 kB 66.72 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.development.js +0.61% 365.74 kB 367.96 kB +0.40% 66.48 kB 66.75 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.development.js +0.61% 365.74 kB 367.96 kB +0.40% 66.48 kB 66.75 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.development.js +0.59% 326.44 kB 328.39 kB +0.43% 63.56 kB 63.84 kB
oss-stable/react-dom/cjs/react-dom-server.bun.development.js +0.59% 326.52 kB 328.46 kB +0.43% 63.59 kB 63.87 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.development.js +0.59% 379.36 kB 381.59 kB +0.44% 68.26 kB 68.56 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +0.59% 379.43 kB 381.66 kB +0.43% 68.31 kB 68.60 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.development.js +0.58% 382.83 kB 385.06 kB +0.43% 68.90 kB 69.20 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +0.58% 382.91 kB 385.14 kB +0.43% 68.95 kB 69.24 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.development.js +0.58% 383.61 kB 385.84 kB +0.43% 69.04 kB 69.34 kB
oss-stable/react-dom/cjs/react-dom-server.edge.development.js +0.58% 383.69 kB 385.92 kB +0.43% 69.09 kB 69.39 kB
oss-stable-semver/react-server/cjs/react-server.development.js +0.47% 174.06 kB 174.89 kB +0.32% 31.23 kB 31.32 kB
oss-stable/react-server/cjs/react-server.development.js +0.47% 174.06 kB 174.89 kB +0.32% 31.23 kB 31.32 kB

Generated by 🚫 dangerJS against 2a3b7a3

return createFormatContext(
parentContext.insertionMode,
parentContext.selectedValue,
parentContext.tagScope | FALLBACK_SCOPE | ENTER_SCOPE,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy pasta. This should not have fallback scope on it.

Comment on lines +1097 to +1106
pushStringAttribute(target, 'vt-update', viewTransition.update);
if (viewTransition.enter !== null) {
pushStringAttribute(target, 'vt-enter', viewTransition.enter);
}
if (viewTransition.exit !== null) {
pushStringAttribute(target, 'vt-exit', viewTransition.exit);
}
if (viewTransition.share !== null) {
pushStringAttribute(target, 'vt-share', viewTransition.share);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've tried hard to keep the output spec compliant where possible. squatting on a data namespace isn't as culturally problematic as squatting on a potential spec namespace. I think we should eat the bytes and use data-vt-*. It's not likely meaningfully incompatible with user code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

People enumerate over the data- set a lot. We lived with it for <link> and <style> for precedence since who would add data- to those but you enumerate dataSet a lot on your own owned elements. So I think data- is a no go. Can't add stuff to that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe r-vt- as the namespace? r- also seems unlikely to be used in the future

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x- will never be added to HTML so x-vt-* might be the closest thing to compliant. It’s still technically not but at least we know it’ll never block future attributes

Comment on lines +3934 to +3935
// TODO: ViewTransition attributes gets observed by the Custom Element which is a bit sketchy.
pushViewTransitionAttributes(target, formatContext);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah but isn't this still a plus for styling?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? The issue is that custom elements are likely to observe any attributes you set on them as their private name space so the extension is more problematic on those.

Allows for more of these to be used if needed like how we pass RenderState.
null means that we don't emit it as an instruction for that case.

We want to minimize the number of instructions we emit.
This needs to use an auto assigned name if no explicit one is provided to
associate deleted fallback nodes with inserted content nodes.
…n context

This is more or less every element except things that are always invisible.
@sebmarkbage sebmarkbage merged commit 65b5aae into facebook:main May 15, 2025
465 of 467 checks passed
github-actions bot pushed a commit that referenced this pull request May 15, 2025
…#33206)

Stacked on #33194 and #33200.

When Suspense boundaries reveal during streaming, the Fizz runtime will
be responsible for animating the reveal if necessary (not in this PR).
However, for the future runtime to know what to do it needs to know
about the `<ViewTransition>` configuration to apply.

Ofc, these are virtual nodes that disappear from the HTML. We could
model them as comments like we do with other virtual nodes like Suspense
and Activity. However, that doesn't let us target them with
querySelector and CSS (for no-JS transitions). We also don't have to
model every ViewTransition since not every combination can happen using
only the server runtime. So instead this collapses `<ViewTransition>`
and applies the configuration to the inner DOM nodes.

```js
<ViewTransition name="hi">
  <div />
  <div />
</ViewTransition>
```

Becomes:

```html
<div vt-name="hi" vt-update="auto"></div>
<div vt-name="hi_1" vt-update="auto"></div>
```

I use `vt-` prefix as opposed to `data-` to keep these virtual
attributes away from user specific ones but we're effectively claiming
this namespace.

There are four triggers `vt-update`, `vt-enter`, `vt-exit` and
`vt-share`. The server resolves which ones might apply to this DOM node.
The value represents the class name (after resolving
view-transition-type mappings) or `"auto"` if no specific class name is
needed but this is still a trigger.

The value can also be `"none"`. This is different from missing because
for example an `vt-update="none"` will block mutations inside it from
triggering the boundary where as a missing `vt-update` would bubble up
to be handled by a parent.

`vt-name` is technically only necessary when `vt-share` is specified to
find a pair. However, since an explicit name can also be used to target
specific CSS selectors, we include it even for other cases.

We want to exclude as many of these annotations as possible.

`vt-enter` can only affect the first DOM node inside a Suspense
boundary's content since the reveal would cause it to enter but nothing
deeper inside. Similarly `vt-exit` can only affect the first DOM node
inside a fallback. So for every other case we can exclude them. (For
future MPA ViewTransitions of the whole document it might also be
something we annotate to children inside the `<body>` as well.) Ideally
we'd only include `vt-enter` for Suspense boundaries that actually
flushed a fallback but since we prepare all that content earlier it's
hard to know.

`vt-share` can be anywhere inside an fallback or content. Technically we
don't have to include it outside the root most Suspense boundary or for
boundaries that are inlined into the root shell. However, this is tricky
to detect. It would also not be correct for future MPA ViewTransitions
because in that case the shared scenario can affect anything in the two
documents so it needs to be in every node everywhere which is
effectively what we do. If a `share` class is specified but it has no
explicit name, we can exclude it since it can't match anything.

`vt-update` is only necessary if something below or a sibling might
update like a Suspense boundary. However, since we don't know when
rendering a segment if it'll later asynchronously add a Suspense
boundary later we have to assume that anywhere might have a child. So
these are always included. We collapse to use the inner most one when
directly nested though since that's the one that ends up winning.

There are some weird edge cases that can't be fully modeled by the lack
of virtual nodes.

DiffTrain build for [65b5aae](65b5aae)
github-actions bot pushed a commit that referenced this pull request May 15, 2025
…#33206)

Stacked on #33194 and #33200.

When Suspense boundaries reveal during streaming, the Fizz runtime will
be responsible for animating the reveal if necessary (not in this PR).
However, for the future runtime to know what to do it needs to know
about the `<ViewTransition>` configuration to apply.

Ofc, these are virtual nodes that disappear from the HTML. We could
model them as comments like we do with other virtual nodes like Suspense
and Activity. However, that doesn't let us target them with
querySelector and CSS (for no-JS transitions). We also don't have to
model every ViewTransition since not every combination can happen using
only the server runtime. So instead this collapses `<ViewTransition>`
and applies the configuration to the inner DOM nodes.

```js
<ViewTransition name="hi">
  <div />
  <div />
</ViewTransition>
```

Becomes:

```html
<div vt-name="hi" vt-update="auto"></div>
<div vt-name="hi_1" vt-update="auto"></div>
```

I use `vt-` prefix as opposed to `data-` to keep these virtual
attributes away from user specific ones but we're effectively claiming
this namespace.

There are four triggers `vt-update`, `vt-enter`, `vt-exit` and
`vt-share`. The server resolves which ones might apply to this DOM node.
The value represents the class name (after resolving
view-transition-type mappings) or `"auto"` if no specific class name is
needed but this is still a trigger.

The value can also be `"none"`. This is different from missing because
for example an `vt-update="none"` will block mutations inside it from
triggering the boundary where as a missing `vt-update` would bubble up
to be handled by a parent.

`vt-name` is technically only necessary when `vt-share` is specified to
find a pair. However, since an explicit name can also be used to target
specific CSS selectors, we include it even for other cases.

We want to exclude as many of these annotations as possible.

`vt-enter` can only affect the first DOM node inside a Suspense
boundary's content since the reveal would cause it to enter but nothing
deeper inside. Similarly `vt-exit` can only affect the first DOM node
inside a fallback. So for every other case we can exclude them. (For
future MPA ViewTransitions of the whole document it might also be
something we annotate to children inside the `<body>` as well.) Ideally
we'd only include `vt-enter` for Suspense boundaries that actually
flushed a fallback but since we prepare all that content earlier it's
hard to know.

`vt-share` can be anywhere inside an fallback or content. Technically we
don't have to include it outside the root most Suspense boundary or for
boundaries that are inlined into the root shell. However, this is tricky
to detect. It would also not be correct for future MPA ViewTransitions
because in that case the shared scenario can affect anything in the two
documents so it needs to be in every node everywhere which is
effectively what we do. If a `share` class is specified but it has no
explicit name, we can exclude it since it can't match anything.

`vt-update` is only necessary if something below or a sibling might
update like a Suspense boundary. However, since we don't know when
rendering a segment if it'll later asynchronously add a Suspense
boundary later we have to assume that anywhere might have a child. So
these are always included. We collapse to use the inner most one when
directly nested though since that's the one that ends up winning.

There are some weird edge cases that can't be fully modeled by the lack
of virtual nodes.

DiffTrain build for [65b5aae](65b5aae)
github-actions bot pushed a commit to code/lib-react that referenced this pull request May 15, 2025
…facebook#33206)

Stacked on facebook#33194 and facebook#33200.

When Suspense boundaries reveal during streaming, the Fizz runtime will
be responsible for animating the reveal if necessary (not in this PR).
However, for the future runtime to know what to do it needs to know
about the `<ViewTransition>` configuration to apply.

Ofc, these are virtual nodes that disappear from the HTML. We could
model them as comments like we do with other virtual nodes like Suspense
and Activity. However, that doesn't let us target them with
querySelector and CSS (for no-JS transitions). We also don't have to
model every ViewTransition since not every combination can happen using
only the server runtime. So instead this collapses `<ViewTransition>`
and applies the configuration to the inner DOM nodes.

```js
<ViewTransition name="hi">
  <div />
  <div />
</ViewTransition>
```

Becomes:

```html
<div vt-name="hi" vt-update="auto"></div>
<div vt-name="hi_1" vt-update="auto"></div>
```

I use `vt-` prefix as opposed to `data-` to keep these virtual
attributes away from user specific ones but we're effectively claiming
this namespace.

There are four triggers `vt-update`, `vt-enter`, `vt-exit` and
`vt-share`. The server resolves which ones might apply to this DOM node.
The value represents the class name (after resolving
view-transition-type mappings) or `"auto"` if no specific class name is
needed but this is still a trigger.

The value can also be `"none"`. This is different from missing because
for example an `vt-update="none"` will block mutations inside it from
triggering the boundary where as a missing `vt-update` would bubble up
to be handled by a parent.

`vt-name` is technically only necessary when `vt-share` is specified to
find a pair. However, since an explicit name can also be used to target
specific CSS selectors, we include it even for other cases.

We want to exclude as many of these annotations as possible.

`vt-enter` can only affect the first DOM node inside a Suspense
boundary's content since the reveal would cause it to enter but nothing
deeper inside. Similarly `vt-exit` can only affect the first DOM node
inside a fallback. So for every other case we can exclude them. (For
future MPA ViewTransitions of the whole document it might also be
something we annotate to children inside the `<body>` as well.) Ideally
we'd only include `vt-enter` for Suspense boundaries that actually
flushed a fallback but since we prepare all that content earlier it's
hard to know.

`vt-share` can be anywhere inside an fallback or content. Technically we
don't have to include it outside the root most Suspense boundary or for
boundaries that are inlined into the root shell. However, this is tricky
to detect. It would also not be correct for future MPA ViewTransitions
because in that case the shared scenario can affect anything in the two
documents so it needs to be in every node everywhere which is
effectively what we do. If a `share` class is specified but it has no
explicit name, we can exclude it since it can't match anything.

`vt-update` is only necessary if something below or a sibling might
update like a Suspense boundary. However, since we don't know when
rendering a segment if it'll later asynchronously add a Suspense
boundary later we have to assume that anywhere might have a child. So
these are always included. We collapse to use the inner most one when
directly nested though since that's the one that ends up winning.

There are some weird edge cases that can't be fully modeled by the lack
of virtual nodes.

DiffTrain build for [65b5aae](facebook@65b5aae)
github-actions bot pushed a commit to code/lib-react that referenced this pull request May 15, 2025
…facebook#33206)

Stacked on facebook#33194 and facebook#33200.

When Suspense boundaries reveal during streaming, the Fizz runtime will
be responsible for animating the reveal if necessary (not in this PR).
However, for the future runtime to know what to do it needs to know
about the `<ViewTransition>` configuration to apply.

Ofc, these are virtual nodes that disappear from the HTML. We could
model them as comments like we do with other virtual nodes like Suspense
and Activity. However, that doesn't let us target them with
querySelector and CSS (for no-JS transitions). We also don't have to
model every ViewTransition since not every combination can happen using
only the server runtime. So instead this collapses `<ViewTransition>`
and applies the configuration to the inner DOM nodes.

```js
<ViewTransition name="hi">
  <div />
  <div />
</ViewTransition>
```

Becomes:

```html
<div vt-name="hi" vt-update="auto"></div>
<div vt-name="hi_1" vt-update="auto"></div>
```

I use `vt-` prefix as opposed to `data-` to keep these virtual
attributes away from user specific ones but we're effectively claiming
this namespace.

There are four triggers `vt-update`, `vt-enter`, `vt-exit` and
`vt-share`. The server resolves which ones might apply to this DOM node.
The value represents the class name (after resolving
view-transition-type mappings) or `"auto"` if no specific class name is
needed but this is still a trigger.

The value can also be `"none"`. This is different from missing because
for example an `vt-update="none"` will block mutations inside it from
triggering the boundary where as a missing `vt-update` would bubble up
to be handled by a parent.

`vt-name` is technically only necessary when `vt-share` is specified to
find a pair. However, since an explicit name can also be used to target
specific CSS selectors, we include it even for other cases.

We want to exclude as many of these annotations as possible.

`vt-enter` can only affect the first DOM node inside a Suspense
boundary's content since the reveal would cause it to enter but nothing
deeper inside. Similarly `vt-exit` can only affect the first DOM node
inside a fallback. So for every other case we can exclude them. (For
future MPA ViewTransitions of the whole document it might also be
something we annotate to children inside the `<body>` as well.) Ideally
we'd only include `vt-enter` for Suspense boundaries that actually
flushed a fallback but since we prepare all that content earlier it's
hard to know.

`vt-share` can be anywhere inside an fallback or content. Technically we
don't have to include it outside the root most Suspense boundary or for
boundaries that are inlined into the root shell. However, this is tricky
to detect. It would also not be correct for future MPA ViewTransitions
because in that case the shared scenario can affect anything in the two
documents so it needs to be in every node everywhere which is
effectively what we do. If a `share` class is specified but it has no
explicit name, we can exclude it since it can't match anything.

`vt-update` is only necessary if something below or a sibling might
update like a Suspense boundary. However, since we don't know when
rendering a segment if it'll later asynchronously add a Suspense
boundary later we have to assume that anywhere might have a child. So
these are always included. We collapse to use the inner most one when
directly nested though since that's the one that ends up winning.

There are some weird edge cases that can't be fully modeled by the lack
of virtual nodes.

DiffTrain build for [65b5aae](facebook@65b5aae)
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.

5 participants