Browse by section

Web Design 日本語

5 Ways to Change Colour on Scroll With CSS and JavaScript

This article covers five ways to change an element’s colour as the page scrolls.

The short answer: for new work, animation-timeline: scroll() is the lightest option — it ties the animation to scroll position with no JavaScript at all.

▼Choosing an approach

Approach JavaScript Best for
animation-timeline: scroll() None Continuous change tied to scroll position
Intersection Observer Required Changing colour when an element becomes visible
scroll event Required Branching on specific conditions
jQuery Required Sites that already load jQuery
CSS keyframes alone None Time-based, not scroll-based

Sponsored

Driving the animation from scroll position in CSS

animation-timeline: scroll() replaces time with scroll distance as the thing that drives an animation. No JavaScript is involved.

A normal animation progresses as time passes. Setting animation-timeline makes it progress as the page scrolls instead — and scrolling back rewinds it.

@keyframes change-color {
  from { background-color: #3498db; }
  to   { background-color: #e74c3c; }
}

.scroll-color {
  animation: change-color linear;
  /* driven by scroll distance, not time */
  animation-timeline: scroll();
}

scroll() uses the nearest scrollable ancestor, usually the page itself. The element is blue at the top of the document, red at the bottom, and interpolates continuously in between.

To run the animation only while the element is on screen, use view().

.fade-in-color {
  animation: change-color linear;
  /* progresses as the element crosses the viewport */
  animation-timeline: view();
  /* limit the range it covers */
  animation-range: entry 0% cover 50%;
}

Handling browsers that do not support it

Chrome and Edge 115 and later support it, as does Safari 18. Firefox still has it behind a flag as of September 2026, so branch with @supports.

/* the state unsupported browsers will show */
.scroll-color {
  background-color: #3498db;
}

@supports (animation-timeline: scroll()) {
  .scroll-color {
    animation: change-color linear;
    animation-timeline: scroll();
  }
}

Unsupported browsers show a fixed colour and nothing breaks. As long as the page still reads without the effect, this is safe to ship.

Respect reduced-motion preferences as well:

@media (prefers-reduced-motion: reduce) {
  .scroll-color {
    animation: none;
  }
}

Why this beats a scroll event listener

animation-timeline runs on the compositor, so it does not stall when the main thread is busy.

Changing colour from a scroll event means running JavaScript on every scroll tick. On a page doing other work, the colour change stutters or lags behind the scroll.

▼Where the work happens

animation-timeline scroll event
Runs on The compositor The JavaScript main thread
Affected by heavy scripts No Stutters
Amount of code A few lines of CSS Listener plus calculation
Conditional logic Limited Unrestricted

Use CSS for “change continuously with scroll” and JavaScript for “do something different under specific conditions”. Changing a colour falls in the first category.

Changing colour when an element becomes visible

Intersection Observer fires a callback when an element enters or leaves the viewport, which is the right tool when the change is a state switch rather than a continuous ramp.

const target = document.querySelector('.section');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    entry.target.classList.toggle('is-visible', entry.isIntersecting);
  });
}, { threshold: 0.5 });

observer.observe(target);
.section {
  background-color: #3498db;
  transition: background-color 0.4s;
}

.section.is-visible {
  background-color: #e74c3c;
}

threshold: 0.5 means the callback fires when half the element is visible. Unlike a scroll listener, the browser does the visibility maths itself and only calls you when the answer changes.

Sponsored

Using a scroll event

A scroll listener is the right choice when the logic is genuinely conditional — different colours at different named breakpoints, for instance.

const el = document.querySelector('.scroll-color');

window.addEventListener('scroll', () => {
  const ratio = window.scrollY / (document.body.scrollHeight - window.innerHeight);
  el.style.backgroundColor = ratio > 0.5 ? '#e74c3c' : '#3498db';
}, { passive: true });

Pass { passive: true }. It tells the browser the listener will not call preventDefault(), so scrolling does not have to wait for your code to finish.

Using jQuery

On a site that already loads jQuery, the same idea is shorter to write:

$(window).on('scroll', function () {
  const ratio = $(window).scrollTop() / ($(document).height() - $(window).height());
  $('.scroll-color').css('background-color', ratio > 0.5 ? '#e74c3c' : '#3498db');
});

It carries the same main-thread cost as the plain JavaScript version. Do not add jQuery to a project for this alone — the CSS approach is both shorter and faster.

Which one to use

Continuous change with scroll: CSS. A state change when something becomes visible: Intersection Observer. Anything genuinely conditional: a scroll event.

The mistake worth avoiding is reaching for a scroll listener by default. It is the most flexible option and also the most expensive, and most colour effects do not need that flexibility.

For a related effect driven by pointer position rather than scroll, see building a mouse stalker that trails the cursor.