I'm currently using CSS scroll-driven animations with the new animation-timeline
property. An example of the technique I'm referring to can be seen here: Image Reveal Example.
However, I'm facing an issue: I would like to add an animation delay to smooth out the animation, especially when scrolling with a mouse. Currently, the animation looks a bit choppy, as mouse scrolls on a PC can be less smooth and occur in larger, instant steps.
I've tried using the traditional CSS property animation-delay, but it doesn't seem to have any effect when used with scroll-driven animations and the animation-timeline
property.
Here is an example of how I want the animation to feel:
// Initialize Lenis
const lenis = new Lenis();
// Listen for the scroll event and log the event data
lenis.on('scroll', (e) => {
console.log(e);
});
// Use requestAnimationFrame to continuously update the scroll
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
.animation-element-wrapper {
display: grid;
justify-content: center;
background-color: green;
}
.animation-element {
background-color: red;
height: 50px;
width: 50px;
animation: move;
animation-timeline: view(block);
}
@keyframes move {
from {
transform: translateY(0) rotate(0deg);
}
to {
transform: translateY(-150px) rotate(360deg);
}
}
// Smooth scroll styling
html.lenis, html.lenis body {
height: auto;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
<script src="https://unpkg.com/lenis@1.1.14/dist/lenis.min.js"></script>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<div class="animation-element-wrapper">
<div class="animation-element"></div>
</div>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
In this case, I'm using a JavaScript library to achieve smooth scrolling for the entire page. My goal is to apply a similar smooth, slow-moving effect for a CSS scroll-based animation without using a JS library.
To highlight the issue, here's another example without the smooth scroll effect:
.animation-element-wrapper {
display: grid;
justify-content: center;
background-color: green;
}
.animation-element {
background-color: red;
height: 50px;
width: 50px;
animation: move;
animation-timeline: view(block);
}
@keyframes move {
from {
transform: translateY(0) rotate(0deg);
}
to {
transform: translateY(-150px) rotate(360deg);
}
}
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<div class="animation-element-wrapper">
<div class="animation-element"></div>
</div>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
The difference in fluidity is noticeable, and I'd like to replicate the smoother experience using pure CSS.
Is there a way to introduce a delay or smooth out this animation behavior using just CSS, or is JavaScript still required? I'm primarily looking for a CSS-based solution.
I've been able to figure it out by using only CSS:
@property --scroll-position {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
@property --scroll-position-delayed {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
@keyframes adjust-pos {
to {
--scroll-position: 1;
--scroll-position-delayed: 1;
}
}
.animation-element-wrapper {
animation: adjust-pos linear both;
animation-timeline: view(block);
display: grid;
justify-content: center;
background-color: green;
}
.animation-element {
transition: --scroll-position-delayed 0.15s linear;
}
.red-square {
background-color: red;
height: 50px;
width: 50px;
transform: translateY(calc(-150px * var(--scroll-position-delayed)));
}
/* Display debugging information */
#debug {
position: fixed;
top: 50%;
left: 75%;
translate: -50% -50%;
background: white;
border: 1px solid #ccc;
padding: 1rem;
& li {
list-style: none;
}
counter-reset: scroll-position calc(var(--scroll-position) * 100) scroll-position-delayed calc(var(--scroll-position-delayed) * 100);
[data-id="--scroll-position"]::after {
content: "--scroll-position: " counter(scroll-position);
}
[data-id="--scroll-position-delayed"]::after {
content: "--scroll-position-delayed: " counter(scroll-position-delayed);
}
}
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<div class="animation-element-wrapper">
<div class="animation-element">
<div class="red-square"></div>
<div id="debug">
<ul>
<li data-id="--scroll-position"></li>
<li data-id="--scroll-position-delayed"></li>
</ul>
</div>
</div>
</div>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
<h1>Hello World!</h1>
This is all thanks to this article: https://www.bram.us/2023/10/23/css-scroll-detection/#lerp-effects
Explanation.
Here the scroll position is fetched (and animated) from the parent of the red-square:
@keyframes adjust-pos {
to {
--scroll-position: 1;
--scroll-position-delayed: 1;
}
}
.animation-element-wrapper {
animation: adjust-pos linear both;
animation-timeline: view(block);
Here the scroll-position is delayed (responsible for the smoothness).
.animation-element {
transition: --scroll-position-delayed 0.15s linear;
}
Here the delayed scroll-position is used to animate the "red-square":
.red-square {
background-color: red;
height: 50px;
width: 50px;
transform: translateY(calc(-150px * var(--scroll-position-delayed)));
}