CSS View Transitions have landed in Chrome and are (for better or worse) widely available to most end users now. Safari is not far behind, with the feature being available in TP and Firefox is at least working on it.
I love how much simpler the implementation of multi-page transitions have become, but at the same time, I realized a big poet-peeve with them: Elements that are out-of-viewport start swooshing around wildly.
My blog deals with this with some Javascript. An IntersectionObserver
sets the view-transition-name
in a custom property while it’s visible. As long as it’s out of sight, the property is unset and the transition won’t trigger. It works, but it needs the HTML, Javascript, and CSS to depend on one another. In the spirit of keeping presentation matters to CSS, I’d like a simpler solution.
Another feature that might be able to help out has landed in Chrome recently: Scroll-driven animations. Sadly, this is only available in Chrome yet. Firefox has it behind a flag and Safari shows no signs of activity here. But we can fall back gracefully, either to the Javascript solution mentioned before or even to simply ignore viewport detection and show the animation anyway (This would be the case in Safari at the time of writing).
A simple transition
Let’s put it all together, starting with the view transition itself:
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
[data-view-transition] {
view-transition-name: var(--view-transition-name, default-view-transition);
}
}
<!-- on page1.html: -->
<div data-view-transition>Whoosh!</div>
<!-- on page2.html: -->
<div data-view-transition>Whoosh!</div>
Now you’ll see the div
from page1.html
transform into its version on page2.html
upon navigation. The @view-transition
at-rule in CSS initiates a crossfade on the whole document as well. The transition duration is set by default to 0.4s
, but we can ass a few lines to control this:
@property --view-transition-duration {
syntax: "<time>";
inherits: true;
initial-value: 0.4s;
}
::view-transition-group(*) {
animation-duration: var(--view-transition-duration);
}
The duration is set by --view-transition-duration
The @property
rule at the top is entirely optional, but it lets the animation-duration
fall back to 0.4s
instead of an invalid value when we set --view-transition-duration: bogus
.
Dealing with the viewport
Still, when an element is outside of the viewport at the time of a page transition, it will still animate, and sometimes pass into or over the entire visible screen without indicating where it hass come from or where it’s going. I find that behavior very irritating.
With scroll-driven animations, we now have a tool that can control styles based on a scroll and viewport-relative position. So we can also control the trigger for view transitions:
@property --view-transition-duration {
syntax: "<time>";
inherits: true;
initial-value: 0.4s;
}
@keyframes enable-vt {
0%,
100% {
--enable-view-transitions: none;
}
0.1%,
99.9% {
--enable-view-transitions: var(--view-transition-name, default-view-transition);
}
}
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
::view-transition-group(*) {
animation-duration: var(--view-transition-duration);
}
[data-view-transition] {
view-timeline-name: --enable-vt;
view-timeline-axis: block;
animation: linear enable-vt both;
animation-timeline: --enable-vt;
animation-range: cover;
view-transition-name: var(--view-transition-name, default-view-transition);
@supports (animation-timeline: --enable-vt) {
view-transition-name: var(--enable-view-transitions);
}
}
}
The enable-vt
keyframe animation can’t transition smoothly from none
to a custom property with a text string, so it will do a hard cut. The animation-range
triggers when the element enters or leaves the viewport even partially. The animation itself sets --enable-view-transition
on 0.1% and 99.9% to trigger as promptly as it can. I want to enable view transitions as soon as we can get a hint of where the animation started or ended.
Finally --enable-view-transitions
is set by the scroll driven animation to either none
or a new custom property called --view-transition-name
. It’s being set between being 0.1% and 99.9% of screen coverage, disabling view transitions both above and below the viewport (or left and right, whichever way you want to hold it). This new property is useful for setting individual animations on different elements. Each element can get its own transition by setting a unique value to --view-transition-name
.
<div data-view-transition style="--view-transition-name: my-new-transition">Whoosh</div>
This needs to be done on both the source and the target page. If no value is set, it defaults to default-view-transition
which controls all remaining data-view-transition
elements uniformly.
Real-world implementation
I added this implementation to my little CSS boilerplate, Ssstyles. Giving an element the data-view-transition
attribute on the source and target page will let it transition between both. Giving it a --view-transition-name
will let it have its own unique animation, independent from other similar elements. Setting the --view-transition-duration
controls the duration of the transition.
Top comments (0)