I am looking to make a CSS animation
start running once the parent of the element I want to animate is fully scrolled into view with pure CSS using the View Timeline (currently only working in Chrome 115+).
A bit of background on what I am attempting to do here, illustrated with a simple :hover
example. Let's say I want to make an element spin infinitely, but only while it's hovered, the animation
is paused
otherwise.
I can do something like this:
div {
margin: 2em auto 0;
width: 8em;
aspect-ratio: 1;
background: color-mix(in hsl, hotpink calc(var(--bit, 0)*100%), gold);
animation: rot 2s linear infinite;
animation-play-state: var(--bit, paused)
}
@keyframes rot { to { rotate: 1turn } }
div:hover { --bit: 1 }
<div>Hover me!</div>
The animation-play-state
is set to the custom property --bit
, which hasn't been explicitly set anywhere (this is important), but has a fallback value of paused
. So the animation
is paused
while the element isn't hovered.
If the element is hovered, then --bit
is explicitly set to 1
, which is an invalid value for animation-play-state
. So then animation-play-state
reverts to its initial value, which is running
.
I prefer this technique because it allows me to change the values of multiple properties on :hover
by only setting --bit
to 1
(in the example above, I'm also changing the background
using --bit
and other properties can be changed as well, but I wanted to leave this example as simple as possible).
Back to triggering animations on scroll. Let's say we have a header
and a section
, both 100vh
tall. The section
contains the element we want to animate. When this section
containing our element has fully (100%
) entered the viewport, we change the value of --bit
to 1
, which is an invalid value for animation-play-state
, so it should make it revert to initial value, running
, which should make the rotation start. Except that doesn't happen.
* { margin: 0 }
header, section {
display: grid;
place-items: center;
height: 100vh
}
section {
animation: b linear both;
animation-timeline: view();
animation-range: entry 100% entry 100%
}
@keyframes b { to { --bit: 1 } }
div {
width: 9em;
aspect-ratio: 1;
background:
color-mix(in hsl, hotpink calc(var(--bit, 0)*100%), gold);
animation: rot 2s linear infinite;
animation-play-state: var(--bit, paused)
}
@keyframes rot { to { rotate: 1turn } }
<header>
<h2>Scroll!</h2>
</header>
<section>
<div></div>
</section>
You can see that when the section has fully entered into view, the value of --bit
has been explicitly set to 1
and it also has been used to alter the background
from gold
to hotpink
.
However, the element hasn't started rotating. And if we check the computed value for animation-play-state
, it's still paused
, even though the computed value for --bit
is now 1
.
What is happening here? How can I make this work? Am I doing something wrong? Is this a bug?
Triggering transitions this way works just fine.
* { margin: 0 }
header, section {
display: grid;
place-items: center;
height: 100vh
}
section {
animation: b linear both;
animation-timeline: view();
animation-range: entry 100% entry 100%
}
@keyframes b { to { --bit: 1 } }
div {
width: 9em;
aspect-ratio: 1;
rotate: calc(var(--bit, 0)*1turn);
background: color-mix(in hsl, hotpink calc(var(--bit, 0)*100%), gold);
transition: 2s;
transition-property: rotate, background-color
}
<header><h2>Scroll!</h2></header>
<section><div></div></section>
Using a style query to change the value for animation-play-state
works fine as well. You can see that as soon as you scroll back up a bit from being scrolled all the way, the animation pauses and the background
changes from hotpink
back to gold.
* { margin: 0 }
header, section {
display: grid;
place-items: center;
height: 100vh
}
section {
--bit: 0;
animation: b linear both;
animation-timeline: view();
animation-range: entry 100% entry 100%
}
@keyframes b { to { --bit: 1 } }
div {
width: 9em;
aspect-ratio: 1;
background: color-mix(in hsl, hotpink calc(var(--bit)*100%), gold);
animation: rot 2s linear infinite;
}
@container style(--bit: 0) {
div { animation-play-state: paused }
}
@keyframes rot { to { rotate: 1turn } }
<header><h2>Scroll!</h2></header>
<section><div></div></section>
But I find this clunky and I'd really rather find a way to get the invalid value technique to work. Any ideas on how to do that?
I am not sure but your issue is probably related to this:
However, any custom property used in a @keyframes rule becomes animation-tainted, which affects how it is treated when referred to via the var() function in an animation property. ref
The --bit
is used in a keyframes and is defined within an animation property so is concerned about that part but I never understood what "animation-tainted" means.
Maybe if you define the custom property using @property
it will work (I cannot try it)