← Scroll

Scroll Colour Shift

The page background fades to a new colour as each section scrolls in, and the text colour follows, so the whole page changes mood from one part to the next. Scroll back up and the colours fade back.

Scroll inside ↓

MorningSoft light and fresh coffee.
MiddayClear skies all round.
DuskThe sky turns pink.
NightLights out, stars on.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-scroll-colour-shift to the <body>, or to any wrapper around your sections.
  3. Give each section a data-bg colour, and a data-fg text colour where it needs one.

Options

Add any of these attributes to the same element.

data-bgOn each section: the background colour it brings in.
data-fgOn each section: the text colour that goes with it. Optional.
data-lineWhere in the view a section takes over, from 0 (top) to 1 (bottom). Default 0.5.
data-durationHow long each fade takes. Default 700 ms.

Leave the sections themselves without a background so the colour shows through. Works inside scrolling boxes too.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Scroll Colour Shift: the page background fades to a new colour as each
    // section scrolls in, and the text colour follows. Scroll back and the
    // colours fade back too.
    // Usage: <body data-scroll-colour-shift>   (or any wrapper around the sections)
    //          <section data-bg="#fef3c7">…</section>
    //          <section data-bg="#1e1b4b" data-fg="#fff">…</section>
    //        </body>
    // On each section: data-bg (the background it brings in), data-fg (text
    // colour, optional).
    // Optional on the wrapper: data-line (where in the view a section takes
    // over, 0 top to 1 bottom, default 0.5), data-duration (the fade, in ms,
    // default 700).
    // Works inside scrolling boxes too: on a box that scrolls, or inside one.
    var DEFAULTS = { line: 0.5, duration: 700 };

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function num(v, fallback) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : fallback; }

    function scroller(el) {
      var own = getComputedStyle(el).overflowY;
      if (own === 'auto' || own === 'scroll') return el;   // the wrapper scrolls itself
      for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
        var o = getComputedStyle(p).overflowY;
        if (o === 'auto' || o === 'scroll') return p;
      }
      return null;
    }

    function setup(el) {
      var line = Math.max(0, Math.min(1, num(el.dataset.line, DEFAULTS.line)));
      var duration = reduced() ? 0 : num(el.dataset.duration, DEFAULTS.duration);
      var sections = Array.prototype.slice.call(el.querySelectorAll('[data-bg]'));
      if (!sections.length) return;
      var box = el === document.body ? null : scroller(el);
      var current = null, queued = false;

      var baseFg = getComputedStyle(el).color;

      function update() {
        queued = false;
        var top = 0, h = window.innerHeight;
        if (box) { var b = box.getBoundingClientRect(); top = b.top + box.clientTop; h = box.clientHeight; }
        var y = top + h * line;
        // The last section whose top has passed the line takes over.
        var pick = sections[0];
        for (var i = 0; i < sections.length; i++) {
          if (sections[i].getBoundingClientRect().top <= y) pick = sections[i];
        }
        if (pick === current) return;
        var first = !current;
        current = pick;
        var to = { backgroundColor: pick.dataset.bg, color: pick.dataset.fg || baseFg };
        anime.utils.remove(el);
        if (first || !duration) anime.utils.set(el, to);
        else anime.animate(el, { backgroundColor: to.backgroundColor, color: to.color, duration: duration, ease: 'inOut(2)' });
      }

      function onScroll() {
        if (queued) return;
        queued = true;
        requestAnimationFrame(update);
      }

      update();
      (box || window).addEventListener('scroll', onScroll, { passive: true });
      window.addEventListener('resize', onScroll);
    }

    function init() {
      document.querySelectorAll('[data-scroll-colour-shift]').forEach(function (el) {
        if (el._sclr) return;
        el._sclr = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<body data-scroll-colour-shift>
  <section data-bg="#fef3c7">…</section>
  <section data-bg="#bae6fd">…</section>
  <section data-bg="#1e1b4b" data-fg="#fff">…</section>
</body>