Search code examples
cssscrollcss-animationscss-variables

Making value invalid on scroll doesn't make property revert to initial value


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.

dev tools screenshot showing the computed value of --bit is 1 and our div element is 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.

dev tools screenshot showing the animation-play-state value is still paused

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?


Solution

  • 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)