# Scroll Colour Shift

> A scroll-linked palette system: each section declares its colours and the page background, text, rules and accents transition together as you scroll, with text contrast guaranteed at every point of the blend.

Canonical: https://gsapvault.com/effects/scroll-color-shift
Live demo: https://gsapvault.com/demos/scroll-color-shift/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | ScrollTrigger |
| Techniques | scroll-linked-color, token-theming, contrast-guard, scrub |
| Uses Lenis | Yes |

## Overview

A scroll-driven colour journey for long pages. Each section carries three data attributes for its background, ink and accent, and a single ScrollTrigger interpolates between neighbouring palettes in Oklab as the boundary crosses the viewport, writing the result into CSS custom properties that the whole page reads.

The hard part is the middle of the transition. Cross-fading a light section into a dark one moves the background and the text toward each other, and halfway across they meet as grey on grey. This effect measures the WCAG contrast ratio on every frame and corrects the ink's lightness until the ratio holds, so the type stays readable at the exact moment a naive version disappears.

## Features

- Declarative per-section palettes: data-color-bg, data-color-ink and data-color-accent, with ink and accent optional
- Seven CSS custom properties written live, covering background, ink, secondary ink, accent, on-accent label, rules and surfaces
- Contrast guard holds a configurable minimum ratio (default 4.5:1) at every frame, including the midpoint of a light-to-dark crossing
- Oklab interpolation keeps the midpoint of two saturated colours on the line a designer would draw, instead of dragging it through grey
- Adjustable reference line so a tall sticky header always shows the palette of whatever sits directly beneath it
- Position-derived state: flick-scrolling, hash jumps and reverse scrolling all land on exactly the right colour
- Optional live contrast readout and palette position markers driven from the same state
- Native scrollbars and form controls follow the ground automatically through color-scheme
- Reduced motion keeps every section's colours correct and drops only the blend
- Lenis integration on the shared GSAP ticker, with a full teardown of tickers, listeners and ScrollTriggers

## Use Cases

- Long-form landing pages that need a designed colour journey rather than one flat ground
- Product stories where each chapter owns a mood and the chrome has to follow it
- Brand and studio sites moving between light and dark sections without a jarring cut
- Editorial features and scrollytelling pieces with per-chapter colourways
- Documentation-heavy pages where an accessible minimum contrast is non-negotiable
- Design system demos showing tokens re-theming a page in real time

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

Every element carrying data-color-palette is read once at init and its declared colours are converted into Oklab. A single ScrollTrigger spanning the container calls one render function on update, which derives the colour state purely from the scroll position: it finds which boundary is currently inside the transition band, computes a smoothstepped mix of the two neighbouring palettes, and derives the seven tokens from that mix.

Each text-bearing token then passes through a contrast guard. It converts the candidate ink and background to WCAG relative luminance, and when the ratio falls short it binary-searches the ink's Oklab lightness toward whichever pole can reach the target, changing lightness only as far as needed and leaving hue and chroma alone. The tokens are written with setProperty on the document element, so the background, headings, body copy, rules, buttons and the sticky header all move together.

gsap.matchMedia collapses the transition band to zero under prefers-reduced-motion, which turns the same render function into a per-section step change, and cleanup kills the ScrollTrigger, removes the Lenis ticker and refresh listener, and clears the color-scheme override.

## Integration Preview

How this effect integrates into a page. The full documentation (examples, events, programmatic API, customization guide) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`:**

```html
<link rel="stylesheet" href="assets/style.css">
```

**2. Add before the closing `</body>` tag:**

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
<!-- Optional. Delete this line and the Lenis block in script.js if you do not want smooth scroll. -->
<script src="https://unpkg.com/lenis@1.3.17/dist/lenis.min.js"></script>
<script src="assets/script.js"></script>
```

**3. Add to your `<body>`:**

```html
<main data-color-shift>
  <section data-color-palette
           data-color-bg="#F2ECE1"
           data-color-ink="#14110D"
           data-color-accent="#C1440E">
    <h1>Light section</h1>
  </section>

  <section data-color-palette
           data-color-bg="#0F131C"
           data-color-ink="#EBEEF6"
           data-color-accent="#F0B429">
    <h2>Dark section</h2>
  </section>
</main>
```

**4. Point your CSS at the tokens the effect writes:**

```css
html, body { background: var(--cs-bg); color: var(--cs-ink); }
.lede      { color: var(--cs-ink-soft); }
hr, .rule  { border-color: var(--cs-line); }
.btn       { background: var(--cs-accent); color: var(--cs-accent-ink); }
.card      { background: var(--cs-surface); }
```

That is the whole integration. Nothing animates the sections themselves; the colour lives entirely in the tokens.

### Options

Set on the `[data-color-shift]` container:

| Attribute | Values | Default | Description |
|---|---|---|---|
| `data-color-shift` | (presence) | required | Marks the container that owns the palette journey |
| `data-color-min-contrast` | number | `4.5` | Minimum WCAG contrast ratio held between `--cs-ink` and `--cs-bg` at every frame. `7` for AAA body copy; `3` if you want the declared inks respected more literally |
| `data-color-line` | `0` to `1` | `0.5` | Where in the viewport a boundary counts as arrived, as a fraction of viewport height. Lower it toward `0.2` when a tall sticky header sits over the page |
| `data-color-band` | number | `0.42` | Length of the transition, as a fraction of viewport height. `0` gives an instant cut at the reference line; `1` spreads the blend across a full screen of scrolling |
| `data-color-scope` | `root` \| `self` | `root` | Whether the tokens are written to `<html>` or only to this container. Use `self` for a themed block inside an otherwise fixed page |

Set on each `[data-color-palette]` element:

| Attribute | Values | Default | Description |
|---|---|---|---|
| `data-color-palette` | (presence) | required | Marks a section that owns a palette |
| `data-color-bg` | any CSS colour | white | The section's ground |
| `data-color-ink` | any CSS colour | derived | Primary text colour. Omit it and a legible one is derived from the background |
| `data-color-accent` | any CSS colour | the ink | Links, eyebrows, filled buttons, markers |

Optional hooks, placed anywhere inside the container:

| Attribute | Description |
|---|---|
| `data-color-contrast` | Element whose text is set to the live ink/background ratio, e.g. `8.4:1`. Also receives `data-level="aaa" \| "aa" \| "low"` for styling |
| `data-color-marker` | One element per palette, in order. The one matching the current palette gets `.is-active` |

### Accessibility

- **Reduced motion**: `gsap.matchMedia` collapses the transition band to zero, so each section still gets its own correct palette and only the blend is dropped. Nothing is hidden and nothing is left mid-transition.
- **No JavaScript**: the `:root` defaults in `style.css` render the page in the first section's palette. Every section stays legible; only the journey is absent.
- **Contrast**: the guard enforces the configured minimum at every frame, not only at the endpoints. Set `data-color-min-contrast="7"` for AAA.
- **Native UI**: `color-scheme` is set from the live background luminance, so scrollbars, form controls and focus rings follow the page instead of standing out against it.
- **Keyboard**: the demo's links and buttons carry `:focus-visible` styles built from the same tokens, so focus is visible in every palette.

### Dependencies

- GSAP 3.14.2
- ScrollTrigger
- Lenis 1.3.17 (optional, for smooth scroll)

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Scroll Colour Shift](https://gsapvault.com/effects/scroll-color-shift)
- [Effects & Templates Vault (£39 one-time, best value): every current and future effect and template](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Parallax Hero](https://gsapvault.com/effects/parallax-hero.md): A pinned hero that separates its background photograph, copy, and foreground card into distinct scroll depths from a single scrubbed ScrollTrigger.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
