Because View Transitions animate snapshots, you can morph any element into another element. But for some type of transitions, the use of snapshots can sometimes work against you. For example: when the border-radius
changes between the old and the new snapshot, you most likely want the border-radius
to nicely animate from the old to the new state instead of seeing two snapshots fade.
~
🌟 This post is about View Transitions. If you are not familiar with the basics of it, check out this 30-min talk of mine to get up to speed.
~
UPDATE 2025.05.15 – An alternative, and easier, solution to this problem is to not create an animation on the ::view-transition-group
pseudo, but to rely on CSS Transition on the original element. See https://brm.us/view-transitions-border-radius-revisited/ for the details on this.
~
# The Problem
The problem can be seen in the following demo that morphs a .card
using a View Transition. The .card
has a view-transition-name
and animates its border-radius
from 0.25rem
to 3rem
and back, amongst a few other properties that change along with it (such as the font-size
and aspect-ratio
).
Pay close attention the corners of the .card
element as it transitions: because View Transitions fade snapshots, the border-radius
does not nicely animate but simply fades from the old state to the new state.
Note that the text inside the .card
also has a view-transition-name
set to it, so that it gets captured separately from the .card
itself. This will turn out to be key later on.
~
# The (partial) Solution
The solution to this problem is to manipulate the ::view-transition-group
which contains the snapshots. The thing you need to do is add an extra animation the ::view-transition-group
which performs the smooth animation you want. In my case, that is an animation of the border-radius
. Don’t forget to set the overflow
to clip
to make sure the snapshots don’t bleed out.
@keyframes adjust-border-radius { from { border-radius: 0.25rem; } to { border-radius: 3rem; } }
::view-transition-group(card) { animation-name: -ua-view-transition-group-anim-card, adjust-border-radius; overflow: clip; background: #ccc; }
:active-view-transition-type(shrink)::view-transition-group(card) { animation-direction: normal, reverse; }
The adjust-border-radius
animation – which animates the border-radius
– gets added to the existing -ua-view-transition-group-anim-card
animation. The newly added animation gets reversed when the card shrinks, which is communicated from JS to CSS using View Transition Types.
One of the key aspects to making this actually work, is that you have to individually capture the background part of the card
and the content/foreground part of it. It’s only the background part that you animate.
.card { view-transition-name: card; }
.card > .card-content { view-transition-name: card-content; }
Check out the following visualization which, upon hovering, shows the two layers that get captured. It’s the background layer that gets animated:
(Another aspect is that you need to invert the easing curve, but I won’t go into details here)
~
# Dealing with changing backgrounds
In the previous demo I cheated a bit by duplicating the background-color
onto the group, this to cover up some inaccuracies. You can see these inaccuracies more clearly when the background-color of the .card
also changes. Pay close attention to the corners of the following demo: you can see it’s not 100% perfect.
(While at it, I updated the code to also change the text inside the card).
The solution here too is to move more properties that get animated onto the ::view-transition-group
. In this case, it’s the background-color
.
To actually see the background animating as part of a transition, the snapshots that make up the card (but not its contents – which are captured separately) need to be hidden while the transition runs. This can be done by setting the ::view-transition-image-pair(card)
to display: none;
. This can safely be done because of the fact that the text content of the card gets captured separately.
@keyframes adjust-group {
from {
background: #ccc;
border-radius: 0.25rem;
}
to {
background: lightblue;
border-radius: 3rem;
}
}
::view-transition-group(card) {
animation-name: -ua-view-transition-group-anim-card, adjust-group;
}
:active-view-transition-type(shrink)::view-transition-group(card) {
animation-direction: normal, reverse;
}
::view-transition-image-pair(card) {
display: none;
}
If the snapshot with the foreground stuff of the element bleeds out of the snapshot with the background stuff, you also need to duplicate the border-radius
and clip
onto that snapshot’s ::view-transition-group
.
In the future Nested View Transition Groups will solve this. This feature is currently getting implemented in Chrome.
~
# Dealing with changing borders
A tricky thing to incorporate in the solution is animating borders. The trickiness comes from the fact that the snapshots are taken using the border-box
. This means that when duplicating a border onto the ::view-transition-group
you need to make sure its box-sizing
is set to border-box
, regardless of the box-sizing
of the snapshotted element.
With this set, you can safely duplicate the border onto the ::view-transition-group
and then animate it as part of its keyframes.
@keyframes adjust-group {
from {
border-radius: 0.25rem;
background: #ccc;
border-width: 2px;
}
to {
border-radius: 3rem;
background: lightblue;
border-width: 8px;
}
}
::view-transition-group(card) {
box-sizing: border-box;
border: 2px solid black;
animation-name: -ua-view-transition-group-anim-card, adjust-group;
}
:active-view-transition-type(shrink)::view-transition-group(card) {
animation-direction: normal, reverse;
}
::view-transition-image-pair(card) {
display: none;
}
~
# In Summary
To smoothly animate things like borders as part of a View Transition, you need to duplicate that animation onto the ::view-transition-group
. For best effect, have the View Transition separately capture the background and foreground of the element you’re animating. This can be done by giving each a view-transition-name
.
~
# Spread the word
Feel free to reshare one of the following posts on social media to help spread the word:
~
🔥 Like what you see? Want to stay in the loop? Here's how:
I can also be found on 𝕏 Twitter and 🐘 Mastodon but only post there sporadically.