DEV Community

Cover image for Build a Scroll-Driven Day-to-Night Animation Using Pure CSS
Artclick
Artclick

Posted on

Build a Scroll-Driven Day-to-Night Animation Using Pure CSS

Scroll-driven animations are one of the most exciting additions to CSS in recent years. They let you tie animations directly to a user's scroll position β€” no JavaScript, no heavy libraries, just clean, performant CSS.

In this tutorial, we'll build a complete day-to-night landscape scene that transforms as you scroll. The sky shifts from bright blue to sunset orange to deep midnight purple. The sun arcs across the horizon and sets. The moon rises. Stars fade in. Mountains and trees slowly darken into silhouettes.

And we will do it all with zero JavaScript.


πŸ‘‰ Live Demo & Source Code:

https://jsfiddle.net/artclick/zsxfd5u3/5/


Browser Support

As of 2025, animation-timeline: scroll() is supported in Chrome, Edge, and Safari. Firefox support is currently behind a feature flag.

For production projects, it's a good idea to provide a fallback or wrap your scroll-driven animations inside an @supports rule:

@supports (animation-timeline: scroll()) {
  .sky {
    animation: skyChange linear;
    animation-timeline: scroll(root);
  }
}
Enter fullscreen mode Exit fullscreen mode

For this tutorial, we'll focus on the modern CSS syntax. Thanks to progressive enhancement, users on unsupported browsers will still see a beautiful static scene, while supported browsers will enjoy the full scroll-driven animation.


Step 1: Build the HTML Foundation

The HTML structure is intentionally simple. We only need two main elements:

  1. A tall scroll container that creates enough scroll distance to drive the animation.
  2. A sticky viewport that keeps the scene fixed while the user scrolls.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Day to Night β€” Scroll Scene</title>

    <style>
        /* All CSS goes here */
    </style>
</head>

<body>
    <div class="scroll-spacer">
        <div class="scene">
            <!-- Scene elements go here -->
        </div>
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Why This Structure Works

The .scroll-spacer uses a height of 400vh, giving us four viewport heights of scrolling.

Inside it, the .scene uses:

position: sticky;
top: 0;
height: 100vh;
Enter fullscreen mode Exit fullscreen mode

This keeps the scene fixed to the viewport while the user scrolls through the 400vh container.

The scene itself never movesβ€”the scroll progress becomes the animation timeline.

Add the Base CSS

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: system-ui, -apple-system, sans-serif;
    background: #0f0f0f;
    color: white;
}

.scroll-spacer {
    height: 400vh;
    position: relative;
}

.scene {
    position: sticky;
    top: 0;
    height: 100vh;
    width: 100%;
    overflow: hidden;

    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}
Enter fullscreen mode Exit fullscreen mode

Key Properties

  • height: 400vh creates the scroll distance that drives the animation.
  • position: sticky keeps the scene fixed during scrolling.
  • height: 100vh makes the scene fill the viewport.
  • overflow: hidden prevents animated elements from appearing outside the viewport.

Note: overflow: hidden is essential because many elements will be positioned outside the viewport during their animations.


Step 2: Layer the Scene

A convincing scene needs depth. We'll create that by stacking elements from back to front using position: absolute and z-index.

<div class="scene">
    <!-- z-index: 1 β€” Farthest back -->
    <div class="sky"></div>

    <!-- z-index: 2 β€” Celestial bodies -->
    <div class="stars">
        <div class="star"></div>
        <!-- 11 more stars... -->
    </div>

    <div class="sun"></div>
    <div class="moon"></div>

    <!-- z-index: 3 β€” Atmosphere -->
    <div class="clouds">
        <div class="cloud cloud1"></div>
        <div class="cloud cloud2"></div>
        <div class="cloud cloud3"></div>
    </div>

    <!-- z-index: 4–5 β€” Landscape -->
    <div class="mountains-back"></div>
    <div class="mountains-front"></div>

    <!-- z-index: 6 β€” Foreground -->
    <div class="tree tree1">
        <div class="tree-leaves"></div>
        <div class="tree-trunk"></div>
    </div>

    <!-- More trees... -->

    <div class="ground"></div>

    <!-- z-index: 10 β€” UI -->
    <div class="scene-title">
        <h1>Day to Night</h1>
        <p>Scroll to experience the transition</p>
    </div>

    <div class="end-content">
        <h2>Good Night πŸŒ™</h2>
        <p>The stars are out. Time to rest.</p>
    </div>

    <div class="scroll-hint">
        <p>Scroll down</p>
        <div class="scroll-arrow"></div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Why This Works

Each part of the landscape occupies its own visual layer.

  • Sky forms the background.
  • Sun, moon, and stars sit above the sky.
  • Clouds float in front.
  • Mountains add depth.
  • Trees and ground create the foreground.
  • UI elements remain on top.

Most layers use position: absolute, while z-index controls which elements appear in front of others.


Step 3: Create the Sky

The sky is the foundation of the entire effect. As the user scrolls, it transitions through several color palettes representing different times of day.

Scroll Progress Sky
0% Bright blue
30% Sunset pink and gold
50% Deep dusk
70% Purple night
100% Midnight
.sky {
    position: absolute;
    inset: 0;
    z-index: 1;

    background: linear-gradient(180deg, #4facfe 0%, #00f2fe 100%);

    animation: skyChange linear;
    animation-timeline: scroll(root);
}

@keyframes skyChange {
    0% {
        background: linear-gradient(180deg, #4facfe 0%, #00f2fe 100%);
    }

    30% {
        background: linear-gradient(180deg, #fa709a 0%, #fee140 100%);
    }

    50% {
        background: linear-gradient(
            180deg,
            #ff6b6b 0%,
            #ffa502 50%,
            #2c3e50 100%
        );
    }

    70% {
        background: linear-gradient(
            180deg,
            #2c3e50 0%,
            #4a148c 60%,
            #0f0f23 100%
        );
    }

    100% {
        background: linear-gradient(
            180deg,
            #0a0a1a 0%,
            #1a1a3e 50%,
            #0f0f23 100%
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Why Use linear?

With scroll-driven animations, linear creates a direct relationship between scroll position and animation progress.

  • 25% scroll β†’ 25% animation
  • 50% scroll β†’ 50% animation
  • 75% scroll β†’ 75% animation

Using easing functions such as ease-in-out causes parts of the animation to speed up or slow down, which feels unnatural for a continuous day-to-night transition.


Step 4: Animate the Sun

The sun performs several animations simultaneously:

  • Moves across the sky.
  • Changes from yellow to orange-red.
  • Slightly grows near midday.
  • Shrinks as it sets.
  • Fades below the horizon.
.sun {
    position: absolute;
    width: 120px;
    height: 120px;

    background: radial-gradient(
        circle,
        #ffd700 30%,
        #ffa500 70%,
        transparent 100%
    );

    border-radius: 50%;
    z-index: 2;

    left: 50%;
    top: 20%;

    transform: translateX(-50%);

    box-shadow:
        0 0 60px rgba(255, 215, 0, 0.6),
        0 0 120px rgba(255, 165, 0, 0.3);

    animation: sunMove linear;
    animation-timeline: scroll(root);
}

@keyframes sunMove {
    0% {
        top: 15%;
        left: 10%;
        opacity: 1;
        transform: translateX(-50%) scale(1);
        filter: hue-rotate(0deg);
    }

    40% {
        top: 10%;
        left: 50%;
        opacity: 1;
        transform: translateX(-50%) scale(1.1);
        filter: hue-rotate(-20deg);
    }

    60% {
        top: 45%;
        left: 70%;
        opacity: 0.8;
        transform: translateX(-50%) scale(0.9);
        filter: hue-rotate(-40deg) brightness(1.2);
    }

    80% {
        top: 70%;
        left: 85%;
        opacity: 0.3;
        transform: translateX(-50%) scale(0.7);
        filter: hue-rotate(-60deg) brightness(0.8);
    }

    100% {
        top: 90%;
        left: 95%;
        opacity: 0;
        transform: translateX(-50%) scale(0.5);
    }
}
Enter fullscreen mode Exit fullscreen mode

Animating Multiple Properties

This single animation updates several properties together:

  • top
  • left
  • opacity
  • transform
  • filter

Because they're all defined within the same keyframes, the sun's movement, scaling, color change, and fade remain perfectly synchronized.

The box-shadow creates a soft glow. Since the element itself fades out, the glow fades automatically without requiring a separate animation.


Step 5: Animate the Moon

The moon is the counterpart to the sun. Instead of setting, it rises from below the viewport as night begins.

.moon {
    position: absolute;
    width: 80px;
    height: 80px;
    background: radial-gradient(
        circle at 30% 30%,
        #f5f5f5 20%,
        #e0e0e0 50%,
        #c0c0c0 100%
    );
    border-radius: 50%;
    z-index: 2;

    left: 20%;
    top: 100%; /* Starts below the viewport */

    transform: translateX(-50%);

    box-shadow:
        0 0 40px rgba(255, 255, 255, 0.3),
        0 0 80px rgba(255, 255, 255, 0.1);

    animation: moonRise linear;
    animation-timeline: scroll(root);
}

/* Moon craters */
.moon::before {
    content: "";
    position: absolute;
    width: 15px;
    height: 15px;
    background: #d0d0d0;
    border-radius: 50%;
    top: 20px;
    left: 25px;
    opacity: 0.5;
}

.moon::after {
    content: "";
    position: absolute;
    width: 10px;
    height: 10px;
    background: #d0d0d0;
    border-radius: 50%;
    top: 45px;
    left: 45px;
    opacity: 0.4;
}

@keyframes moonRise {
    0% {
        top: 110%;
        opacity: 0;
    }

    50% {
        top: 110%;
        opacity: 0;
    }

    70% {
        top: 60%;
        opacity: 0.8;
    }

    100% {
        top: 20%;
        opacity: 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Delaying the Animation

The moon remains hidden during the first half of the animation.

  • 0–50%: Hidden below the viewport.
  • 50–70%: Begins rising during dusk.
  • 100%: Fully visible in the night sky.

This staggered timing ensures the moon only appears once the sun has nearly disappeared.

Creating Craters with CSS

The moon's craters are built entirely with ::before and ::after pseudo-elements. No images or SVGs are required.


Step 6: Add the Stars

Stars should only become visible after sunset. Once they appear, they continuously twinkle.

.stars {
    position: absolute;
    inset: 0;
    z-index: 2;

    opacity: 0;

    animation: starsAppear linear;
    animation-timeline: scroll(root);
}

@keyframes starsAppear {
    0% {
        opacity: 0;
    }

    60% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

.star {
    position: absolute;
    width: 3px;
    height: 3px;
    background: white;
    border-radius: 50%;
    box-shadow: 0 0 6px rgba(255,255,255,.8);
}

/* Position individual stars */
.star:nth-child(1) {
    top: 10%;
    left: 15%;
    animation: twinkle 2s infinite;
}

.star:nth-child(2) {
    top: 5%;
    left: 35%;
    animation: twinkle 3s infinite .5s;
}

.star:nth-child(3) {
    top: 15%;
    left: 55%;
    animation: twinkle 2.5s infinite 1s;
}

/* ...more stars... */

.star:nth-child(12) {
    top: 16%;
    left: 70%;
    animation: twinkle 2.1s infinite 1.4s;
}

@keyframes twinkle {
    0%,100% {
        opacity: .3;
        transform: scale(.8);
    }

    50% {
        opacity: 1;
        transform: scale(1.2);
    }
}
Enter fullscreen mode Exit fullscreen mode

Combining Multiple Animations

Each star participates in two different animations:

  • starsAppear controls when the stars become visible using the scroll timeline.
  • twinkle runs continuously using a regular time-based animation.

Because these animations affect different aspects of the stars, they work together seamlessly.

The varying animation-delay values make each star twinkle independently, producing a much more natural night sky.


Step 7: Create the Clouds

Clouds make the daytime scene feel alive, but gradually fade away as night approaches.

.clouds {
    position: absolute;
    inset: 0;
    z-index: 3;

    animation: cloudsFade linear;
    animation-timeline: scroll(root);
}

@keyframes cloudsFade {
    0% {
        opacity: .9;
    }

    50% {
        opacity: .6;
        filter: hue-rotate(-30deg) brightness(1.1);
    }

    100% {
        opacity: .1;
    }
}

.cloud {
    position: absolute;
    background: rgba(255,255,255,.8);
    border-radius: 50px;

    animation: cloudFloat 20s infinite ease-in-out;
}

.cloud::before,
.cloud::after {
    content: "";
    position: absolute;
    background: rgba(255,255,255,.8);
    border-radius: 50%;
}

.cloud1 {
    width: 120px;
    height: 40px;
    top: 15%;
    left: 20%;
}

.cloud1::before {
    width: 50px;
    height: 50px;
    top: -25px;
    left: 15px;
}

.cloud1::after {
    width: 40px;
    height: 40px;
    top: -15px;
    left: 55px;
}

/* cloud2 and cloud3 follow the same pattern */

@keyframes cloudFloat {
    0%,100% {
        transform: translateX(0);
    }

    50% {
        transform: translateX(30px);
    }
}
Enter fullscreen mode Exit fullscreen mode

Building Clouds with CSS

Each cloud consists of:

  • A rounded rectangle.
  • Two circular pseudo-elements.

By combining these simple shapes, you can create soft cloud silhouettes without using any images.

The clouds gradually fade as the animation progresses, while a subtle hue-rotate() warms their color during sunset.


Step 8: Build the Landscape

The mountains are created using clip-path, allowing us to generate sharp peaks with pure CSS.

.mountains-back {
    position: absolute;
    bottom: 80px;
    left: 0;
    right: 0;
    height: 250px;
    z-index: 4;
}

.mountains-back::before {
    content: "";
    position: absolute;
    bottom: 0;
    left: -10%;
    width: 60%;
    height: 100%;
    background: #7c9885;

    clip-path: polygon(
        50% 0%,
        100% 100%,
        0% 100%
    );

    animation: mountainDarken linear;
    animation-timeline: scroll(root);
}

.mountains-back::after {
    content: "";
    position: absolute;
    bottom: 0;
    right: -10%;
    width: 70%;
    height: 120%;
    background: #6b8f71;

    clip-path: polygon(
        30% 0%,
        100% 100%,
        0% 100%
    );

    animation: mountainDarken linear;
    animation-timeline: scroll(root);
}

@keyframes mountainDarken {
    0% {
        background: #7c9885;
    }

    50% {
        background: #8b6914;
    }

    100% {
        background: #1a1a2e;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why Use clip-path?

clip-path: polygon() lets us create geometric shapes directly in CSS.

Layering multiple triangles with different sizes creates a convincing mountain range without SVGs or images.

As the animation progresses, the mountains transition from green to warm sunset tones before becoming dark silhouettes.

The front mountains and ground use the same approach with different sizes and colors to create depth.


Step 9: Build the Trees

Each tree is composed of a rectangular trunk and a triangular canopy.

.tree {
    position: absolute;
    bottom: 60px;
    z-index: 6;

    animation: treeDarken linear;
    animation-timeline: scroll(root);
}

@keyframes treeDarken {
    0% {
        filter: brightness(1);
    }

    50% {
        filter: brightness(.7) sepia(.3);
    }

    100% {
        filter: brightness(.2);
    }
}

.tree-trunk {
    width: 8px;
    height: 30px;
    background: #5d4037;
    margin: 0 auto;
}

.tree-leaves {
    width: 0;
    height: 0;

    border-left: 20px solid transparent;
    border-right: 20px solid transparent;
    border-bottom: 50px solid #2e7d32;

    margin-bottom: -5px;

    animation: leavesDarken linear;
    animation-timeline: scroll(root);
}

@keyframes leavesDarken {
    0% {
        border-bottom-color: #2e7d32;
    }

    50% {
        border-bottom-color: #4a3c14;
    }

    100% {
        border-bottom-color: #0a0a14;
    }
}

/* Position trees */
.tree1 {
    left: 10%;
}

.tree2 {
    left: 25%;
    transform: scale(.8);
}

.tree3 {
    right: 15%;
}

.tree4 {
    right: 30%;
    transform: scale(1.1);
}
Enter fullscreen mode Exit fullscreen mode

Creating CSS Triangles

A CSS triangle is created by setting:

width: 0;
height: 0;
Enter fullscreen mode Exit fullscreen mode

and then using borders to form the shape.

Only the bottom border receives a color, while the left and right borders remain transparent.

This technique has been used for years because it's lightweight, scalable, and requires no additional assets.


Step 10: Add the UI Overlays

Finally, we'll add the title, scroll hint, and ending message.

These elements fade in and out based on the scroll position.

.scene-title {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 10;
    text-align: center;

    animation: titleFade linear;
    animation-timeline: scroll(root);
}

@keyframes titleFade {
    0% {
        opacity: 1;
        transform: translate(-50%, -50%) scale(1);
    }

    15% {
        opacity: 1;
    }

    30% {
        opacity: 0;
        transform: translate(-50%, -60%) scale(.9);
    }

    100% {
        opacity: 0;
    }
}

.scroll-hint {
    position: fixed;
    bottom: 30px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 100;
    text-align: center;

    animation: hintFade linear;
    animation-timeline: scroll(root);
}

@keyframes hintFade {
    0%,10% {
        opacity: 1;
    }

    20%,100% {
        opacity: 0;
    }
}

.scroll-arrow {
    width: 24px;
    height: 24px;

    border-right: 3px solid white;
    border-bottom: 3px solid white;

    transform: rotate(45deg);

    margin: 0 auto;

    animation: bounce 1.5s infinite;
}

@keyframes bounce {
    0%,100% {
        transform: rotate(45deg) translateY(0);
    }

    50% {
        transform: rotate(45deg) translateY(10px);
    }
}

.end-content {
    position: absolute;
    bottom: 100px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10;
    text-align: center;

    opacity: 0;

    animation: endFade linear;
    animation-timeline: scroll(root);
}

@keyframes endFade {
    0% {
        opacity: 0;
    }

    85% {
        opacity: 0;
        transform: translateX(-50%) translateY(20px);
    }

    100% {
        opacity: 1;
        transform: translateX(-50%) translateY(0);
    }
}
Enter fullscreen mode Exit fullscreen mode

fixed vs absolute

The scroll hint uses position: fixed so it always remains at the bottom of the viewport, regardless of the sticky scene.

The title and ending message use position: absolute because they belong to the scene itself and should animate with it.

Adding a small upward translateY() movement to the ending message makes its appearance feel much smoother than simply fading it in.

Wrapping Up

Congratulations! πŸŽ‰

You've just built a complete scroll-driven day-to-night scene using nothing but modern CSSβ€”no JavaScript, no SVGs, and no images.

Along the way, you learned how to:

  • Use CSS Scroll-Driven Animations with animation-timeline
  • Create layered scenes with position and z-index
  • Build shapes using gradients, clip-path, borders, and pseudo-elements
  • Combine scroll-driven and time-based animations
  • Synchronize multiple elements into a smooth, immersive experience

Modern CSS has become incredibly powerful, making it possible to create rich interactive experiences while keeping your code lightweight and maintainable.

Live Demo & Full Working Code

Want to see the finished animation in action or explore the complete source code?

πŸ‘‰ Live Demo & Source Code:

https://jsfiddle.net/artclick/zsxfd5u3/5/

If you found this tutorial useful, consider leaving a ❀️ and sharing it with other developers.


At ArtClick, we build fast, scalable WordPress websites, company websites, and custom web systems that balance design, performance, and long-term maintainability.

Whether you're starting from scratch or improving an existing platform, we'd love to help.

🌐 https://artclickdev.com/

Top comments (0)