Changing Background Color on Scroll with JavaScript
Tags: tailwindcss, webdev, html, javascript
Published: 2023-11-21 09:11:22

####**[Live Demo](https://cruip-tutorials.vercel.app/background-scroll/) / [Download](https://github.com/cruip/cruip-tutorials/blob/main/background-scroll/bg-scroll.js)**

Scroll animations seem to be a growing trend lately, especially for [landing pages for SaaS and modern tech startups](https://saaslandingpage.com). 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](https://cruip.com/create-a-stacking-cards-waterfall-effect-with-tailwind-and-alpine-js/) or the [gradient text reveal](https://cruip.com/create-a-gradient-text-reveal-on-scroll-with-tailwind-css-and-js/). 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](https://tailwindcss.com/docs/customizing-colors#default-color-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](https://developer.mozilla.org/en-US/docs/Web/API/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:

```html
<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:

```html
<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:

```js
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:

```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:

```js
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:

```html
<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:

```js
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:

```html
<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](https://twitter.com/Cruip_com) if you've already successfully integrated a similar effect into your site.