Triggering Animations on Scroll
animation-timeline
is not supported in this browser. 😢
Here’s the idea: I don’t want to use animation-timeline to control animations based on scroll progress. I don’t want an animation that plays slower if you scroll slow or speeds up if you scroll fast. Instead, I want the animation to trigger once an element crosses a certain threshold. Then let it play out on its own. These 4 squares will act as our elements I want to animate in.
I want an approach similar to what I would get with an interaction observer. Once it is determined to be enough on screen, it flips a class and plays the animation. I want to do this with just CSS. No JS needed.
First I define the trigger. This need to be a parent component as the the trigger changes a custom prop that the child elements use for their transition. I use animation-name
to define the animation that should play. I use animation-fill-mode
to keep the animation in its final state. This needs to be both
so it will keep its starting and ending keyframes. I use animation-timeline
to define set that the animation should play based on the view of the section. I use animation-range
to define the range the animation should play in. It’s defined to start when 100px
are on screen and 150px
are on screen. You can use percentages here also but you’ll run into the issue where it takes longer to trigger as your .trigger
grows in length.
.trigger {
animation-name: enter;
animation-fill-mode: both;
animation-timeline: view();
animation-range: entry 100px 150px;
}
Here is the markup
<div class="trigger-wrapper">
<div class="layout">
<div class="box" style="--index: 0"></div>
<div class="box" style="--index: 1"></div>
<div class="box" style="--index: 2"></div>
<div class="box" style="--index: 3"></div>
</div>
</div>
I then define some keyframes. I use --value
to define the progress of the animation. I use from
and to
to define the start and end of the animation. I use --value
to define the progress of the animation. I use from
and to
to define the start and end of the animation.
@keyframes enter {
from {
--value: 0;
}
to {
--value: 1;
}
}
I then define the transitions for the boxes. I then use the --value
as a psuedo boolean
value that goes from 0
to 1
. I use the value in transform to to translate on the Y axis between 100px
and 0px
. The opacity is simply the --value
due to it being 0
or 1
. and the transition delay is based on the --index
which is a custom prop that is passed to the child elements. This is so the boxes animate in one after the other.
.box {
transform: translateY(calc(100px - var(--value) * 100px));
transition: transform 1s ease-in-out, opacity 1s ease-in-out;
transition-delay: calc(200ms * var(--index));
opacity: var(--value);
}
Here is it all put together.
HTML
<div class="trigger">
<div class="layout">
<div class="box" style="--index: 0"></div>
<div class="box" style="--index: 1"></div>
<div class="box" style="--index: 2"></div>
<div class="box" style="--index: 3"></div>
</div>
</div>
CSS
:root {
--gap: 30px;
--duration: 1s;
--delay: 200ms;
/* https://easingwizard.com/ */
--glide: linear(
0,
0.012 0.9%,
0.049 2%,
0.409 9.3%,
0.513 11.9%,
0.606 14.7%,
0.691 17.9%,
0.762 21.3%,
0.82 25%,
0.868 29.1%,
0.907 33.6%,
0.937 38.7%,
0.976 51.3%,
0.994 68.8%,
1
);
}
.trigger {
margin-block: 3rem;
animation-name: enter;
animation-fill-mode: both;
animation-timeline: view();
animation-range: entry 100px 150px;
}
.layout {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
gap: var(--gap);
}
.box {
display: inline-flex;
width: 100%;
max-width: 500px;
aspect-ratio: 1;
background: #b3c33a;
transform: translateY(calc(100px - var(--value) * 100px));
transition: transform var(--duration) var(--glide), opacity var(--duration) var(
--glide
);
transition-delay: calc(var(--delay) * var(--index));
opacity: var(--value);
}
@keyframes enter {
from {
--value: 0;
}
to {
--value: 1;
}
}
JS
// 404