· Updated on

Changing Background Color on Scroll with JavaScript

A sneak peek of the demo

Scroll animations seem to be a growing trend lately, especially for landing pages for SaaS and modern tech startups. These animations are conceived to beautify the user experience, as long as they are wisely coded and not too intrusive for the final user.

We’ve previously created tutorials on this topic, like the card waterfall animation on scroll or the gradient text reveal. Today we’ll be adding another example to the list: changing the background color of a page as you scroll.

This effect will allow you to add dynamism to your design and make every section unique.

Structuring the HTML

In this demo, we will create a <main> element with four sections, each associated with a different color from the Tailwind palette: Sky 200, Purple 200, Pink 200, and Emerald 200.

To ensure smooth color transitions, we won’t directly apply background colors to individual sections. Instead, we’ll use the Intersection Observer API to determine which section is dominant and apply the corresponding background color to the main container.

Here’s the basic HTML structure:

<main class="relative min-h-screen flex flex-col bg-sky-200 overflow-hidden">
    <div class="w-full max-w-5xl mx-auto px-4 md:px-6">
        <div class="pt-6 pb-12 md:pt-10 md:pb-20">

            <!-- Sky section -->
            <section>
                <!-- ... content ... -->
            </section>

            <!-- Purple section -->
            <section>
                <!-- ... content ... -->
            </section>

            <!-- Pink section -->
            <section>
                <!-- ... content ... -->
            </section>

            <!-- Emerald section -->
            <section>
                <!-- ... content ... -->
            </section>

        </div>
    </div>
</main>

We’ve set the default background color of the <main> element to Sky 200 (bg-sky-200, which is the color of the first section) and ensured it fills at least the height of the viewport using min-h-screen.

Creating a JS class

Let’s create a file bg-scroll.js in the project’s root, and include it in our HTML document:

<script src="./bg-scroll.js"></script>

In that file, we’ll define a class named BgScroll exposing a init method for initializing the observer. Here’s the code to start:

class BgScroll {
  constructor(element) {
    this.element = element;
    this.init();
  }

  init() {
  }
}

// Init BgScroll
const el = document.querySelector('[data-bg-scroll]');
new BgScroll(el);

At the end of the file, we specify how to initialize the class by adding a data-bg-scroll attribute to the <main> element in the HTML:

<main class="relative min-h-screen flex flex-col bg-sky-200 overflow-hidden" data-bg-scroll>

Creating the observer

Now, we’ll write the code to initialize the observer:

class BgScroll {
  constructor(element) {
    this.element = element;
    this.sections = this.element.querySelectorAll('section');
    this.observeSection = this.observeSection.bind(this);
    this.init();
  }

  observeSection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        console.log(entry.target);
      }
    });
  }  

  init() {
    const observer = new IntersectionObserver(this.observeSection, { rootMargin: '-50% 0% -50% 0%' });
    this.sections.forEach(section => {
      observer.observe(section);
    });
  }
}

// Init BgScroll
const el = document.querySelector('[data-bg-scroll]');
new BgScroll(el);

We have created a new instance in the constructor, called sections, which contains an array of all the sections within the main element. In the init() method, we loop through this array and create a new observer for each section.

The first argument of the IntersectionObserver is the callback that will be executed every time a section enters or exits the viewport (i.e., observeSection). The second argument is an options object, which contains the rootMargin property. By using the values -50% 0% -50% 0%, we’re telling the IntersectionObserver to trigger the callback when the section intersects an imaginary horizontal line positioned 50% from the top of the viewport.

For testing purposes, we’ve added a console.log inside the observeSection method to identify the dominant section as you scroll.

Changing the background color

Now that we know which is the dominant section, we can apply the corresponding background color to the <main> element.

To do this, we’ll define the background color of each section using a data-bg-color attribute and the Tailwind class of the corresponding background color:

<main class="relative min-h-screen flex flex-col bg-sky-200 overflow-hidden" data-bg-scroll>
    <div class="w-full max-w-5xl mx-auto px-4 md:px-6">
        <div class="pt-6 pb-12 md:pt-10 md:pb-20">

            <!-- Sky section -->
            <section data-bg-class="bg-sky-200">
                <!-- ... content .... -->
            </section>

            <!-- Purple section -->
            <section data-bg-class="bg-purple-200">
                <!-- ... content .... -->
            </section>

            <!-- Pink section -->
            <section data-bg-class="bg-pink-200">
                <!-- ... content .... -->
            </section>

            <!-- Emerald section -->
            <section data-bg-class="bg-emerald-200">
                <!-- ... content .... -->
            </section>

        </div>
    </div>
</main>

Inside the observeSection method, we can now retrieve the attribute’s value – which is entry.target.dataset.bgClass – and apply it to the main element:

class BgScroll {
  constructor(element) {
    this.element = element;
    this.sections = this.element.querySelectorAll('section');
    this.observeSection = this.observeSection.bind(this);
    this.init();
  }

  observeSection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // Remove all bg classes
        this.element.classList.forEach(className => {
          if (className.startsWith('bg-')) {
            this.element.classList.remove(className);
          }
        });
        // Add the proper bg class
        this.element.classList.add(entry.target.dataset.bgClass);
      }
    });
  }  

  init() {
    const observer = new IntersectionObserver(this.observeSection, { rootMargin: '-50% 0% -50% 0%' });
    this.sections.forEach(section => {
      observer.observe(section);
    });
  }
}

// Init BgScroll
const el = document.querySelector('[data-bg-scroll]');
new BgScroll(el);

And, as you may guess, we also remove all classes starting with bg- to ensure only one background class is applied.

Adding a fade animation

To complete our effect, we’ll add a fade animation to the background color change. Simply add the transition-colors class to the <main> element.

The final and simplest step is to add a fade animation to the background color change. To do this, just add the transition-colors class to the main element. Like this:

<main class="relative min-h-screen flex flex-col bg-sky-200 transition-colors duration-700 overflow-hidden" data-bg-scroll>

We have also added the duration-700 class to extend the animation duration for a smoother effect.

Conclusions

Creating this dynamic background effect with the Intersection Observer is incredibly easy. It takes just around 30 lines of JavaScript, and no external libraries are required.

Feel free to make your own experiments and play around with the code to match your specific needs.

Don’t forget to shoot us a message on Twitter if you’ve already successfully integrated a similar effect into your site.