Introducing Visual Copilot 2.0: Make Figma designs real

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Platform
Developers
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web Development

Animate hero elements with scroll-driven CSS animations

December 13, 2024

Written By Stefan Judis

When you navigate to github.com (you must be logged out because otherwise you'll enter the GitHub app), you'll be greeted by this landing page.

It's a well-designed centered hero design with a sign-up box. It stands out that the hero text fades away while moving behind the code editor once you scroll down. How can you achieve such an effect?

I was curious and kicked off a quick reverse engineering session to discover that everything's built with React, and the application listens to scroll events. When scrolling, the JS updates the hero text's scale and opacity properties in a requestAnimationFrame loop. In short, a lot is going on to create this effect!

In this article, I will explain how to achieve a similar effect with modern (and future) CSS and zero JavaScript.

If you want to look at the final example, check a deployed demo version online!

Some prep work

To have some code ready, I quickly designed a hero section in Figma and used the Builder Figma plugin to generate some React code with Visual Copilot.

While my Figma-to-code result is less pretty than the GitHub example, this workflow lets me create something to work off in five minutes.

A reasonable first step to recreate the GitHub effect is to slide the hero image over the hero text.

To do this, we can stick the hero text at the top while scrolling. By using position: sticky, we can place an element inside a container and "make it stick" somewhere when people scroll around.

To give the hero text some air, we'll also add some space using the top CSS property.

.hero-text {
  /* Make the element sticky */
  position: sticky;
  /* Give the element some space at the top */
  top: 1rem;
}

When we add position: sticky, we'll discover that the hero text scrolls on top of the other scrolling elements. This is exactly what you want in most sticky scenarios, but in our case, we want the sticky element to go below the rest.

To make the hero text "slide under", we need to create a new stacking context for the other elements. There are many ways to do this, but I like to set isolation: isolate to create a new stacking context because it's a nifty one-liner, and more people should know about the isolation CSS property.

.hero-image {
  /* Create a new stacking context */
  isolation: isolate;
}

And voila — look at this!

That's not too shabby, isn't it? Now, we only need to add the scroll-fade behavior. How can we add it?

If you're reading this blog, you're probably aware of the ongoing CSS evolution. Let me tell you, there hasn't been a better time to be a web developer. To name a few recent CSS features: we now have container queries, the :has() selector entered the stage, and view transitions make animating things easier than ever before.

The modern web is fantastic, and one other exciting feature in the making is scroll-driven CSS animations. Scroll what? Yes, you heard that right!

Scroll-driven animations allow you to remove all these custom JavaScript scroll handlers and use the CSS Animations API to add scroll animation behavior.

The API runs off the main thread and provides a massive performance boost compared to thousands of firing scroll handlers in JavaScript land. With scroll-driven CSS animations, you can create scroll effects with less and more performant code.

The new CSS feature is currently supported only by Chromiums, but Webkit is open to this platform addition, and Firefox ships an initial implementation behind a flag! Native scroll-driven animations will be coming to the web!

If Chromium-only isn't good enough to use the new CSS feature yet, I understand. However, with Chromium-based browsers having a global market share of over 70%, it's fair to treat scroll effects as progressive enhancement. Browsers that support scroll-driven animations will show users some eye candy, while the other browsers will only render a headline on top of a hero image.

Side note: if scroll-driven animations are essential for your site, there is also a polyfill. I haven't used it, but it might be a valid alternative to add custom scroll behavior.

But let's get to it and add some eye candy!

Scroll-driven animations are based on good old @keyframes.

@keyframes fade-out {
  0% {
    opacity: 1;
    scale: 1;
  }

  100% {
    opacity: 0;
    scale: 0.5;
  }
}

We can define a fade-out animation that makes an element disappear by scaling it down and reducing its opacity. Next, we take our hero-text class and set our scroll-driven animation.

.hero-text {
  /* Set the animation */
  animation: fade-out linear;
  /* Connect the animation to the nearest scroller's scroll progress */
  animation-timeline: scroll();
}

And look at this: two added CSS declarations give us something to work off of already.

Let's understand what's going on here!

The hero-text class now sets the fade-out keyframes using the animation property. It isn't a "normal" animation declaration, though.

The animation shorthand doesn't define an animation-duration. Usually, time controls the keyframe animation progress, but in our case, the animation-duration is set to auto because we want to make it dependent on scroll progress and a scroll timeline.

Below the animation declaration, you'll see the new animation-timeline property. Theoretically, animation-timeline is also part of the animation shorthand, but you can only use it to reset a timeline. To define an animation other than auto we must define an animation followed by a specified animation-timeline. Order matters in this case!

But I got ahead of myself; what's an animation timeline?

An animation timeline specifies the timeline used to control the progress of a CSS keyframe animation. Usually, you control your animations by time which maps to animation-timeline: auto. But with scroll-driven animations, there are now two new animation scroll timelines: scroll progress (animation-timeline: scroll()) and view progress (animation-timeline: view()) timelines.

A scroll progress timeline defined with the scroll() CSS function maps the scroll progress of a wrapping scroll container (also called scroller) to values from 0% to 100% and applies them to a keyframe animation. In our case, the nearest scroll container is the html element. When scrolling down, the overall document scroll progress controls the fade-out animation.

And while this works, you might now wonder what happens with massively long HTML documents.

When the fade-out animation relies on document scroll progress, a longer document affects the animation progress.

scroll() comes in handy if you want to display a document scroll progress indicator, but for our hero animation, it doesn't make sense to use a scroll progress timeline. If you scroll 500px down in a 2000px high document, the animation will progress by 25%. But when the document grows to 10000px, the scroll timeline will only progress to 5%. The animation will be barely noticeable. Our animation should rely on something other than the document length!

Luckily, we can use another scroll timeline.

If you don't want to consider the overall scroll progress, there's another way to control scroll-driven animations. The new view() CSS function allows you to consider the visibility of an element inside a scroller. The view() function is quite a beast to wrap your head around, but most importantly, view progress timelines are also wildly powerful!

I will only go into some of the details here (check the resources below if you want to learn more), but let's take a step back to rethink how the visual effect should work.

We want to:

  • Start the keyframe animation at 0% when our hero starts moving off the screen.
  • Finish the keyframe animation at 100% when the hero disappears.

To achieve this behavior, we have to adjust the animation range, and of course, there's another new CSS property for it — say hello to animation-range. animation-range enables us to define the start and end of our scroll-driven animation. For our fade-out animation, we're interested in kicking off the animation when an element starts disappearing (0%) until it's entirely gone (100%).

For this use case, we can add an exit animation range. Let's go ahead and put this into the CSS.

.hero-text {
  animation: fade-out auto ease-in-out;
  animation-timeline: view();
  /* Apply a custom animation range */
  animation-range: exit 0% exit 100%;
}

When you add the custom animation range, you'll discover that, for our hero animation, it doesn't work, though.

We defined the hero text to be sticky, and this positioning implies that it will leave the scroller only when the surrounding container is moving off the screen. The hero text animation is applied, but it's not visible because our hero dummy image covers the scroll exit animation.

When we remove the stickiness, we'll discover that our animation-range works appropriately. What can we do now that we want to animate a sticky element?

Of course, the new scroll-driven animations have a solution to this problem, too. To make our animation visible, we need to control the animation depending on the view progress of another element with a named view progress timeline.

In our CSS, we can go into the wrapping hero container and specify a view-timeline-name

.hero-container {
  /* Define a named view progress timeline */
  view-timeline-name: --hero-scroll;
}

… to then change our hero text to use the view timeline instead of view().

.hero-text {
  animation: fade-out auto ease-in-out;
  /* Specify the container animation timeline */
  animation-timeline: --hero-scroll;
  animation-range: exit 0% exit 100%;
}

And now check this out!

Our hero text element fades out and scales down right when the surrounding container starts leaving the screen. And how much CSS did this take us to write? It was only a few CSS declarations and one keyframe animation. I'm amazed!

Let's consider a few more things to be good web citizens, though.

First, some folks can feel discomfort or even sickness when faced with unexpected movements on screen. To build "a good web™", we can add a CSS reduced-motion media query to allow people to opt out of moving animations.

Second, while creating the demo, I discovered that Firefox's flagged implementation doesn't support the animation-range property yet. To resolve this, we can also add a @supports feature query to avoid a broken animation in browsers that don't support scroll-driven animations yet.

If you ask me, the fallback looks excellent as well, but of course, you could also remove the stickiness if you don't like the image sliding over the text.

So, here's the final code.

@supports (animation-range: exit) {
  @media (prefers-reduced-motion: no-preference) {
    .hero-container {
      view-timeline-name: --hero-scroll;
    }

    .hero-text {
      animation: fade-out auto ease-in-out;
      animation-timeline: --hero-scroll;
      animation-range: exit 0% exit 100%;
    }
  }
}

.hero-text {
  position: sticky;
  top: 1rem;
}

.hero-image {
  isolation: isolate;
}

@keyframes fade-out {
  0% {
    opacity: 1;
    scale: 1;
  }

  100% {
    opacity: 0;
    scale: 0.5;
  }
}

Isn't this magical and fun? I'm a big fan!

So, with only a few lines of modern CSS, we rebuilt this JavaScript-heavy effect on GitHub's landing page. Our scroll animation is now more maintainable and performant because we're not relying on any JavaScript blocking the main thread!

And because we're relying on modern CSS paired with progressive enhancement, we can give users the best experience if their browsers support it. Win-win!

If you want to see the effect in action, here's the deployed version.

Are you hooked already? I indeed am!

If you want to learn more about scroll-driven animations, there's a single place to go. Bramus from the Chrome DevRel team, aka "Mr. ScrollAnimation", maintains scroll-driven-animations.style. The site lists many tools, resources, and visual examples.

If you prefer learning with videos, Bramus has also released a free course on YouTube.

And if you wonder how to debug scroll-driven animations, this Chrome extension is invaluable for figuring out what's going on.

And now we're done, have fun animating, and let me know how it goes!

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual CopilotGet a demo
Written by
Stefan loves getting into web performance, new technologies, and accessibility, sends out a weekly web development newsletter and enjoys sharing nerdy discoveries on his blog.

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual CopilotGet a demo
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI10 MIN
AI Pair Programming in 2025: The Good, Bad, and Ugly
December 13, 2024
AI15 MIN
Devin review: is it a better AI coding agent than Cursor?
December 11, 2024
AI8 MIN
Is OpenAI's o1 model a breakthrough or a bust?
December 9, 2024