Tutorials 4 min read

How to change background color on scroll with Tailwind CSS: JavaScript and Alpine.js

Change the header and section background colors on scroll with Tailwind CSS, two ways: a vanilla JavaScript scroll listener or Alpine.js with @scroll.window.

Today we are changing the background color of a header and a few sections based on the user’s scroll position. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.

What is a background change on scroll?

It’s a small effect with a big impact: as the user scrolls past set thresholds, the header and each section swap their background color. It gives people a visual cue of where they are on the page and makes the site feel more interactive, and all it takes is toggling a class or a style based on the scroll position.

Use cases

  • Sticky headers: fade the header from transparent to solid so it stays readable over content.
  • Storytelling pages: shift section colors as the reader progresses to mark chapters.
  • Landing pages: draw attention to key sections the moment they scroll into view.
  • Wayfinding: help users keep track of where they are on a long page.

The markup

  • id="section-1": the id of the first section.
  • id="section-2": the id of the second section.
  • id="section-3": the id of the third section.

Classes are removed for brevity, but I’ll keep those classes relevant to the tutorial.

html
<section id="section-1">
  <h2>Section #01</h2>
</section>
<section id="section-2">
  <h2 class="text-background">Section #02</h2>
</section>
<section id="section-3">
  <h2 class="text-background">Section #03</h2>
</section>

As you can see, each section has an id that corresponds to the number of the section. Not a big deal here. But what if we want to change the background color of each section based on the user’s scroll position? We can do that with JavaScript.

The script

  • document.addEventListener("scroll", () => {: listens for scroll events on the window object.
  • const header = document.getElementById("header");: grabs the header element and stores it in a variable.
  • const sections = [...]: an array of objects, one per section, each holding its element, the scroll offset that triggers it, and the color to apply.
  • setTimeout(() => { ... }, 100);: delays the style changes by 100ms.
  • header.classList.toggle("bg-background", window.scrollY > 50); and header.classList.toggle("bg-transparent", window.scrollY <= 50);: swap the header classes once the user scrolls past 50px.
  • sections.forEach((section) => {: loops over the sections and sets each background to its color once window.scrollY passes its offset, or back to white otherwise.
js
document.addEventListener("scroll", () => {
  const header = document.getElementById("header");
  const sections = [
    {
      element: document.getElementById("section-1"),
      offset: 50,
      color: "#e9e9e9",
    },
    {
      element: document.getElementById("section-2"),
      offset: 150,
      color: "#f92c1c",
    },
    {
      element: document.getElementById("section-3"),
      offset: 250,
      color: "#000000",
    },
  ];

  setTimeout(() => {
    header.classList.toggle("bg-background", window.scrollY > 50);
    header.classList.toggle("bg-transparent", window.scrollY <= 50);

    sections.forEach((section) => {
      if (window.scrollY > section.offset) {
        section.element.style.backgroundColor = section.color;
      } else {
        section.element.style.backgroundColor = "white";
      }
    });
  }, 100);
});

The Alpine.js version

Same component, no separate script: the state lives in x-data on the header and on each section.

  • x-data="{ scroll: false }": initializes each component with a scroll property that tracks whether the user has scrolled past its threshold.
  • @scroll.window="scroll = (window.pageYOffset > 50) ? true : false": listens for scroll events on the window and updates scroll based on the scroll Y offset.
  • :class="{ 'bg-background': scroll, 'bg-transparent': !scroll }": applies the matching background classes as scroll flips.
html
<header
  x-data="{ scroll: false }"
  @scroll.window="scroll = (window.pageYOffset > 50) ? true : false"
  :class="{ 'bg-background': scroll, 'bg-transparent': !scroll }"
  class="fixed top-0 flex flex-col items-center justify-center w-full p-20 text-2xl transition-colors duration-500 ease-in-out"
>
  <h1>Scroll to see effect</h1>
</header>

Each section is set up the same way, with its own x-data, @scroll.window and :class directives; the only difference is the scroll threshold (50, 150, 250) that dictates when its background color changes.

html
<section
  x-data="{ scroll: false }"
  @scroll.window="scroll = (window.pageYOffset > 150) ? true : false"
  :class="{ 'bg-[#e1f1fd]': scroll, 'bg-background': !scroll }"
  class="flex flex-col items-center justify-center p-20 text-2xl h-svh transition-colors duration-500 ease-in-out"
>
  <h2>Section 2</h2>
</section>

You can try the Alpine version separately: live demo and source code.

Which one should you use?

If Alpine is already on the page, the @scroll.window version keeps each threshold right in the markup. If not, the vanilla listener handles the header and all sections from one place without adding a dependency.

Conclusion

A few thresholds and a class toggle are all it takes to make a page react to scrolling. Keep the code clean, and test it on different devices and browsers so it works well for everyone.

Hope you enjoyed this tutorial and have a great day!

/Michael Andreuzza