webdevpuneet.com

Scroll Direction Theme Shift — Free Direction-Aware Color Theme Effect

Scroll Direction Theme Shift · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="dt-indicator" id="dtIndicator"><span id="dtArrow">↓</span><span id="dtLabel">Scrolling down</span></div>
<section class="dt-intro"><h1>Scroll up and down</h1><p>The accent palette shifts to a "down" theme while scrolling down, and a distinct "up" theme while scrolling up — based on comparing consecutive scroll positions, not just how far you've scrolled.</p></section>
<section class="dt-block"><h2>Section one</h2><p>Direction is recomputed on every scroll event by comparing the current position against the last one.</p></section>
<section class="dt-block"><h2>Section two</h2><p>Scroll down a while, then scroll back up mid-page — the theme flips immediately, not at the top.</p></section>
<section class="dt-block"><h2>Section three</h2><p>A small deadband ignores sub-pixel jitter so the theme doesn't flicker on a still page.</p></section>
<section class="dt-outro"><p>Try scrolling down, pausing, then scrolling up from anywhere on the page.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

True direction detection
Compares consecutive scrollY reads, not absolute position.
Works anywhere on the page
Not limited to "near the top" logic.
Jitter deadband
Small deltas are ignored so the theme doesn't flicker.
CSS variable driven
Four custom properties control the whole palette.
State-change only updates
applyTheme only runs when direction actually flips.
Smooth cross-fade
CSS transitions animate the palette shift, no JS tweening.
Live direction indicator
A fixed badge shows the detected direction and arrow.
No dependencies
Pure vanilla JS and a passive scroll listener.

About this UI Snippet

Scroll Direction Theme Shift — A Palette That Responds to Which Way You Scroll

Screenshot of the Scroll Direction Theme Shift snippet rendered live

Most "scroll-aware" theme effects only check one thing: has the user scrolled past some threshold, usually near the top of the page. This snippet checks something different and more genuinely direction-aware — which way the user is currently scrolling, determined fresh on every scroll event by comparing the current position against the previous one. Scroll down anywhere on the page and the accent colors shift to a cyan/indigo palette; scroll up from anywhere — not just near the top — and it shifts to a distinct pink/amber palette instead. Built entirely in vanilla JavaScript with CSS custom properties.

Comparing consecutive positions, not absolute position

The core of the effect is four lines: read window.scrollY, subtract the value recorded on the previous scroll event, and the sign of that difference is the direction. This is fundamentally different from checking scrollY > 100 or scrollY === 0 — those only know *where* you are on the page, not *which way* you're currently moving. A user paused halfway down the page could be about to scroll either direction; only comparing consecutive reads reveals which.

A deadband against jitter

Trackpads, certain mice, and even some scroll-restoration behaviors can fire scroll events with a pixel or two of noise even when the page is visually still. A small DEADBAND of 2px means deltas smaller than that are ignored entirely rather than treated as a direction change, so the theme doesn't flicker between states when nothing meaningful is happening.

CSS custom properties as the single source of truth

Rather than toggling many individual color declarations, the direction handler writes four CSS custom properties (--dt-accent, --dt-accent-2, --dt-bg, --dt-bg-2) on the root element, and every colored element in the CSS simply references those variables with a transition. Changing direction is then just four setProperty calls, and the whole page's palette cross-fades together via CSS transitions with no JavaScript animation loop needed.

Direction only changes state when it flips

The handler tracks the last known direction string and only calls applyTheme when the newly computed direction actually differs from it — so hundreds of consecutive "still scrolling down" events don't re-trigger the same transition repeatedly; only the moment of reversal does.

Customizing it

Add a third, more neutral theme for "scroll velocity near zero" using the deadband window, tie the shift to a background gradient position instead of flat colors, or apply it per-section instead of globally. Pair it with scroll color sections for position-driven color, or a dark mode toggle for manual theme control.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn intro, three content blocks, an indicator, and an outro render.
  2. 2
    Scroll downThe palette shifts to cyan/indigo and the indicator shows ↓.
  3. 3
    Scroll up from mid-pageThe palette flips to pink/amber immediately, not just near the top.
  4. 4
    Pause, then continueA small deadband prevents flicker while the page is still.
  5. 5
    Watch the indicatorIt shows the currently detected direction and arrow.
  6. 6
    Customize the themesEdit DOWN_THEME and UP_THEME's color values.

Real-world uses

Common Use Cases

Editorial long reads
Subtly signal reading versus reviewing by shifting the accent palette when the reader scrolls down or back up, rather than based on distance from the top.
Portfolio direction-aware accents
Pair with scroll colour sections so one effect responds to position and the other to direction.
Landing page scroll feel
Give scrolling a tactile, responsive feel, with a small deadband ignoring tiny deltas so the theme never flickers.
Documentation reading direction
Distinguish forward reading from going back to check something, using four CSS custom properties to control the whole palette.
Dashboards with manual themes
Combine with a dark mode toggle so users keep manual control while direction adds a playful ambient layer.
Related: Scroll Data Story Counters
See the Scroll Data Story Counters for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A "scrolled past top" effect only checks the absolute scroll position — usually a single threshold like scrollY > 100. This snippet instead subtracts the previous scroll position from the current one on every scroll event; the sign of that difference is the real, instantaneous scroll direction, which works correctly no matter where on the page the user currently is, including mid-scroll reversals far from the top.

Some input devices and browser scroll-restoration behaviors fire scroll events with a pixel or two of noise even when the page looks visually still. The DEADBAND constant ignores any delta smaller than 2px, so that noise doesn't get misread as a direction change and cause the theme to flicker when nothing meaningful is happening.

No — the handler computes the direction on every event but compares it against the last known direction, and only calls applyTheme (which writes the CSS custom properties and toggles the class) when the direction has actually flipped. Hundreds of consecutive "still scrolling down" events do nothing beyond updating the stored last position.

Writing four CSS custom properties on the root element means every element that references var(--dt-accent) and similar updates together automatically, and the built-in CSS transition on those properties handles the cross-fade with no JavaScript animation loop. It also makes customizing the palettes a one-line edit to the DOWN_THEME and UP_THEME objects rather than hunting through class definitions.

Attach the passive scroll listener in a mount effect, keeping lastY and direction in refs so they persist across renders without causing re-renders on every scroll event. Write the CSS custom properties via document.documentElement.style.setProperty exactly as in the vanilla version, since that state lives outside the component tree. Return a cleanup that removes the scroll listener.