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.
<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 itselement, the scrolloffsetthat triggers it, and thecolorto apply.setTimeout(() => { ... }, 100);: delays the style changes by 100ms.header.classList.toggle("bg-background", window.scrollY > 50);andheader.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 itscoloroncewindow.scrollYpasses itsoffset, or back towhiteotherwise.
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 ascrollproperty 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 updatesscrollbased on the scroll Y offset.:class="{ 'bg-background': scroll, 'bg-transparent': !scroll }": applies the matching background classes asscrollflips.
<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.
<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