Last updated: First published:
Where to Place CSS for Cross-Document View Transitions?
You’re aware that enabling cross-document view transitions is as simple as adding the view transition at-rule to your pages.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; }}
You also know that both pages, the old one and the new one, must opt in by including this at-rule. Additionally, you might know that this only works if both pages share the same origin, meaning the same protocol, host, and port.
However, when you start adding more CSS to introduce additional view transition names or customize the animations, you might wonder: Do I need this CSS on both pages?
View transition names are tied to the DOM, whether they’re set directly on elements or added via CSS rules. You must define the names for the old images on the old page and for the new images on the new page. This does not need to be static. You can use view transition types to add or remove view transition names via CSS, see the Turn-Signal example.
The last chance to set these names on the old page using JavaScript is during the pageswap
event, which occurs right before the screenshots for the old images are taken and navigation leaves the page.
For the new images, your last opportunity to define names using JavaScript is the pagereveal
event, triggered after the new page is loaded and just before screenshots of the new images are taken.
This might come as a surprise, but it’s simple to explain: Besides defining view transition names and taking screenshots of the old images, the old page has no control over how the view transition is styled. Any pseudo-element related CSS defined on the old page will be ignored — unless the old page is also the new one.
The reason for this is that the animations are triggered by the appearance of the view transition pseudo-elements on the :root
element of the new page. At that moment, only the styles from the new page are available.
While it makes sense in theory, it still feels a bit counterintuitive that the CSS for an exit animation affecting an element that only exists on the old page has to be defined on the new page.
This might help: Keep in mind that it is not the old element being animated but the image of the old element, which is inserted as a pseudo-element on the new page.
And the new page can be any page the user navigates to, including those accessed through a global navigation bar if your site has one. Even without a navigation bar, the user could select a page several entries back in the browser history. That could be just about anything.
-
So should I put the styles for all exit animations in a site-wide global CSS file? Most likely yes, so they are available on all pages.
-
And should I split the CSS, keeping entry and morph animations separate from exit animations, with only the needed styles on each page? Probably not, since that would break up things that logically belong together.
-
But will the global file become huge, and won’t it be loaded on every page? Yes, that is possible. However, since it is the same for all pages, the browser can cache it.
If you’re accustomed to building with components, it might not always be clear which page a component will end up on and where the corresponding CSS should be placed. Additionally, the requirement that view-transition-name
values must be unique on a page makes you reconsider whether it’s wise to hardcode view transition names inside reusable components. Especially if you craft a component that can end up with multiple copies on the same page. This is very similar to how to handle id
attributes.
A promising approach with components might be the following (still under evaluation):
Use dynamic view transition names:
- Often, you can come up with a dynamic value for the
view-transition-name
, e.g. a slug from underlying data, or a context parameter given to the component from the outside. - If so, use this for the
view-transition-name
.
Otherwise:
- Do not explicitly assign a static
view-transition-name
as non unique names will break view transitions if the component is use multiple times on the same page. - Instead, defer the assignment of the view transition name to a global step
- Use
view-transition-class
to select pseudo elements for styling
<div class="some-CSS-class-name">...</div><style> .some-CSS-class-name { view-transition-class: some-view-transition-class-name; } ::view-transition-group(.some-view-transition-class-name) { ... }</style><script>// assign unique names to all elements that have the some-CSS-class-name class document.querySelectorAll(".some-CSS-class-name") .forEach(e => e.style.viewTransitionName = ...)</script>
This way, the styling will even work if the component is used several times on the same page. Automatic name assignment for same-document view transitions is easy with auto-generated names
is ease with the declarative-names script. For the example above set data-vtbag-decl=".some-CSS-class-name = some-id-"
We are all looking forward to scoped view transitions, but in the meanwhile we need some practical solutions.
Currently, I use the following approach to organize CSS definitions for view transitions. All starts with a modular definition of how I want to apply a view transition.
- I define a use case for a view transition animation and give it a name. This not on the global level like: On this event I want many things to animate on my page. It is very component oriented, like: This section of the form should expand when that checkbox got checked. I give this case
- Then I list the elements that should play an active role in the animation. Those I will assign view-transition-names.
- I check for buttons and links that will end up being captured by pseudo-elements. Those will not be interactive during the animation and they definitively should look disabled.
- Next I figure out which of the pseudo-elements and animations generated by the View Transition API are not required for the effect, like cross-fades of identical images, or old images that can be replaced by the new images (or vice versa), or group animation that do not change the geometry.
- The next task is defining the styling of the view transition pseudo-elements. Its in the nature of view transitions that this styling often addresses
animation-*
properties. But there are also often other fun properties for clipping / masking, object placement, perspective, and others that might be worth to think of. - In cases, where I want to take advantage that the
::view-transition-new
pseudo-elements are life images of their named elements, I define the styling for those. - Finally, I define new key frames and timing function as required, always checking whether there is some potential for standardization and reuse.
After those preparations, I use pattern where I guard the CSS rules with an active view transition type. For same-document view transitions, the type is passed in the options object to startViewTransition()
. This way the type is active right from the begin of the view transition, even before the old images are captured. And it is automatically removed right after the the lat animation ends. For cross-document view transitions, you can set the type with the types
property of the @view-transition
at-rule or within a listener for pageswap
and pagereveal
events.
Thus all contained definitions only exist just during the view transition. Here is an example:
/* use the use case name as a guard to scope definitions */:active-view-transition-type(use-case-name) { /* repeat all the following definitions as needed */ selector-for-named-element { view-transition-name: some-view-transition-name; } selector-for-named-element button { /* style as disabled */ } /* define where you do not want animations */ &::view-transition-group(x) { /* or ...-new, or ...-old */ animation-name: none; } /* define new animations */ &::view-transition-group(x) { /* or some other pseudo-element */ animation-name: keyframes; /* or other styling */ } selector-for-named-element { /* intra-animation styles for the new element */ }}/* keyframe definitions outside the type guard because you cant have `:root @keyframes name {}` */
One of the main benefits of this pattern is that view transition names are only defined during the view transition with that specific type and thus can not conflict with other unrelated view transition use cases. When the transition ends, they are gone. No need to tidy up afterward.
There are also ways to handle generated view transition names: instead of directly setting an element’s view transition name, set a CSS custom property and copy it over inside the guarded rule:
:active-view-transition-type(some-type) { [style*="--some-vtn:"] { view-transition-name: var(--some-vtn); }}
…or even…
:active-view-transition-type(some-type) { [style*="--some-vtn:"] { view-transition-name: match-element; }}
Using different custom properties for different use case gives you a lot of freedom on how to set them.
Other style properties set this way are also only defined throughout the view transition. An examples is styling buttons as disabled: you can not click he pseudo-elements inserted by the View Transition API during the animation. And with that styling applied, they also look disabled, just for the few hundred milliseconds of the view transition.
If you choose to adopt this pattern, pay attention to ampersands. The :active-view-transition-type
pseudo-class returns the :root
element while the type matches. Thus the first nested rule in the example above is equivalent to…
:root selector-for-named-element { view-transition-name: some-view-transition-name;}
…while the type matches. If you want to select the document element, just use &
as the selector-for-named-element.
Be careful not to forget the &
in front of the pseudo element selectors. You want to select…
:root::view-transition-group(*)
…not…
:root ::view-transition-group(*) /* avoid that space character */
Same hold for the auto-generated view transition name example: it works for all elements but the initial <html>
element. If you want to include it in the rule, change [style*="--some-vtn:"]
to [style*="--some-vtn:"], &[style*="--some-vtn:"]
.
I’m still exploring paths and alternatives but this would be my current recommendations:
- For complex pages, ensuring the uniqueness of view transition names might be easier if you assign them dynamically in the browser using JavaScript, or in a page global action on the server.
- Placing your view transition animation definitions in a global CSS file can be a good approach. This ensures that both the old and new pages use the same definitions, so you do not have to worry about where to put them. Just make sure your keyframe and view transition names are either globally unique or intentionally the same.
- Utilizing view transition classes and types can help decouple usage from definitions, making your code more flexible.”