Color Shift
A section background (.sdc-color-bg, absolute, inset 0) whose 135° gradient runs through five palettes — night, indigo, violet, pink, orange — over a 520 px travel, in CSS alone: animation-timeline: scroll() reads the 280 px box, zero JavaScript. Measured: the palette switches in one step, no cross-fade, at 66, 196, 326 and 456 px in Chromium.
Updated
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Scroll has 70 effects, including 12 free. Explore the category →
3 usage examples



How it works
The sold HTML stacks five boxes: the frame .demo-preview (centered flex, min-height: 300px, position: relative, #0a0a0f background, white text), the scrolling box .sdc-scroll-container (100% × 280px, overflow-y: auto, scroll-behavior: smooth, a 4 px ::-webkit-scrollbar), the stage .sdc-color-content (800px, flex column, position: relative) and, inside it, side by side, the background .sdc-color-bg (position: absolute; inset: 0, empty) then the section .sdc-color-section (flex: 1, centered flex) holding .sdc-color-text: a 24 px weight-800 h3 and a 12 px p in white at 60%, at z-index: 1 above the background. The travel is 800 − 280 = 520 px. The JS field is empty; no site variable, no hosted file, no console error: 2.4 KB of CSS.
The background has no background of its own: it comes entirely from @keyframes sdcColorShift, five steps each setting a two-color linear-gradient(135deg, …) — #1e1b4b → #0f172a (0%), #4c1d95 → #1e1b4b (25%), #7c3aed → #4c1d95 (50%), #ec4899 → #7c3aed (75%), #f97316 → #ec4899 (100%) — read by animation-timeline: scroll(): with no argument, the timeline is that of the nearest scroll container, here .sdc-scroll-container; animation-duration: auto and animation-range: normal spread the 100% over the whole travel. Measured in Chromium 151, wheel in 104 px notches: progress 0 → .2 → .4 → .6 → .8 → 1, and the background keeps the last palette at the end (finished state; nothing vanishes despite animation-fill-mode: none). But a gradient does not interpolate: between two steps the browser does not blend the colors, it switches. Chromium changes palette halfway through each segment — to the pixel: 66, 196, 326 and 456 px — and WebKit 26.5 from the very first pixel of the segment — 1, 131, 261 and 391 px —, so the “night” palette only exists there at scrollTop = 0. Five colors, four clean cuts; for a real cross-fade, animate a color, not a gradient (see the FAQ).
The background is a sibling of the section, not its child: its inset: 0 is measured against the first positioned ancestor. The position: relative line on .sdc-color-content is what makes it cover the stage's 800 px; without it (measured with the line removed), the positioned ancestor becomes the 280 px box, the background is only 280 px tall, scrolls away with the content and leaves the frame — at 260 px only a 20 px strip remains, at 326 px nothing, and the pink and orange palettes are never seen. The text, meanwhile, travels with the stage: centered at 400 px, it is out of frame at rest (375–425 px for a 280 px window), enters from the bottom at 96 px, leaves through the top at 425 px — visible over 329 of the 520 px of travel, under the indigo, violet and pink palettes. At rest, the visitor therefore sees an empty “night” box; the title only appears while scrolling. Two copies pasted in the same page give two independent boxes, each reading its own container; the h3 font comes from an inline style (Inter is not loaded, the browser falls back to the system font).
Accessibility
- prefers-reduced-motion not in the code: measured under emulation, the palettes switch at the same pixels and the hint keeps pulsing (
sdcHintPulse running). The background change is driven by the reader's gesture — nothing moves without it —, which makes it an interaction-triggered animation (WCAG 2.3.3). Add@media (prefers-reduced-motion: reduce) { .sdc-scroll-hint { animation: none } .sdc-scroll-container { scroll-behavior: auto } }; if the hard cuts bother you for a sensitive reader, freeze one palette:.sdc-color-bg { animation: none; background: linear-gradient(135deg, #1e1b4b, #0f172a) }. - Contrast (computed on both colors of each palette): the white 24 px weight-800 title (3:1 threshold) passes on night (16:1), indigo (11:1), violet (5.7:1) and pink (3.5:1), but drops to 2.8:1 on the orange
#f97316. The 12 px paragraph in white at 60% (4.5:1 threshold) only passes on night (6.5:1) and indigo (4.9:1): 3.0:1 on violet, 2.1:1 on pink, 1.9:1 on orange. For text read over the whole travel, keep#fffat 100% and darken the last two palettes, or put the text on a translucent card (the tower scene). - Keyboard: measured, Chromium and Firefox focus the box on Tab without a
tabindex(Arrow Down: 40 px in Chromium, 51 px in Firefox; End: 520 px); WebKit does not — focus stays onbodyand the arrows do nothing. Addtabindex="0", anaria-labeland a:focus-visiblestyle on.sdc-scroll-container. Wheel and finger are native (no listener);overscroll-behavior: containkeeps the page from being dragged along at the end. - Screen readers and meaning of color: the palette is not announced and must not carry information on its own — the hour, floor or month it illustrates must be written in the content (WCAG 1.4.1).
.sdc-color-bgis an emptydiv: putaria-hidden="true"on it, and on the “Scrollez ↓” hint (hard-coded French; white at 30% whose opacity pulses from .3 to .7, the two multiply: 1.2:1 at the low point of its pulse, 1.8:1 at the peak). The soldh3must follow your page's current heading level. Without CSS, everything reads in document order.
Browser compatibility
CSS only: animation-timeline: scroll(), animation-duration: auto, animation-range, inset, @keyframes on background, flexbox, scroll-behavior, ::-webkit-scrollbar (Blink and WebKit only). Zero JavaScript, zero dependencies. Scroll-driven animations exist in Chrome and Edge since version 115, in Safari and iOS since 26; Firefox reads them in no version (measured in Firefox 153: CSS.supports('animation-timeline: scroll()') returns false, animation-duration: auto is rejected and falls back to 0s). The sold code contains no @supports fallback.
Firefox, measured: the background computes background-image: none, the box is flat, the color of the frame (#0a0a0f); the white title and the paragraph stay readable as they pass, the hint pulses, nothing is hidden, no console error — but no color ever changes. To give those visitors the starting palette, three lines to paste under the code: @supports not (animation-timeline: scroll()) { .sdc-color-bg { background: linear-gradient(135deg, #1e1b4b, #0f172a) } }. Before Chrome 115 and Safari 26, same rendering as Firefox. Without JavaScript: nothing to lose, there is none.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="sdc-scroll-container">
<div class="sdc-color-content">
<div class="sdc-color-bg"></div>
<div class="sdc-color-section">
<div class="sdc-color-text">
<h3>…</h3>
<p>…</p>
</div>
</div>
</div>
</div>
<span class="sdc-scroll-hint">…</span>
</div>
The full HTML + CSS + JS of Color Shift, ready to paste — plus all 811 effects. Or €299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Palettes (CSS — @keyframes sdcColorShift) |
5 steps: #1e1b4b→#0f172a, #4c1d95→#1e1b4b, #7c3aed→#4c1d95, #ec4899→#7c3aed, #f97316→#ec4899 | Each step is a complete two-color gradient: replace them with yours, keeping the form. The number of steps sets the number of cuts — four steps, three switches, at 1/6, 3/6 and 5/6 of the travel in Chromium. A solid color per step (background: #1e1b4b) behaves the same: by switch, no fade. |
Travel (CSS — .sdc-color-content { height: 800px }, .sdc-scroll-container { height: 280px }) |
520 px (switches at 66, 196, 326 and 456 px) | The timeline covers exactly stage − box. Your content replaces the 800 px (height: auto) and the switches reposition themselves at 12.5 / 37.5 / 62.5 / 87.5% of the new travel; a taller box (60vh) needs nothing else changed. |
Scroller (CSS — animation-timeline: scroll()) |
scroll() = nearest scroll container | scroll(root) reads the page's scroll: the background then follows the whole page, with no box (remove overflow-y, set the background to position: fixed; inset: 0 behind the content). A named timeline — scroll-timeline: --fond y on the scroller, animation-timeline: --fond on the background — drives a background placed outside the scroller. |
Travel window (CSS — animation-range: normal) |
normal (0 → 100%) | animation-range: 0 60% runs through the five palettes by three fifths of the travel then keeps the orange; 20% 100% leaves the night on the first fifth. The switches tighten accordingly. |
Gradient angle (CSS — 135deg in each step) |
135° (top left → bottom right) | The gradient is drawn over the stage's 800 px, not the 280 px window: the box only shows a slice of it, almost flat, sliding with the scroll. 90deg makes it horizontal and fully visible at every position. |
Text (CSS — .sdc-color-text; h3 1.5 rem weight 800, p .75 rem at 60%) |
centered at 400 px, visible from 96 to 424 px | The block travels with the stage. For a title visible from rest, switch it to position: sticky; top: 0 inside the section, or fill the whole height (lists, paragraphs): the background covers the whole stage. |
Box dressing (CSS — scroll-behavior: smooth, ::-webkit-scrollbar 4 px, .sdc-scroll-hint) |
smooth / 4 px purple bar / “Scrollez ↓” pulsing 2 s | smooth only eases programmatic scrolls; the styled bar only exists in Blink and WebKit (Firefox keeps its own). The hint is a catalog leftover: hard-coded French, anchored to .demo-preview — delete the span, the @keyframes sdcHintPulse goes with it. |
FAQ
background with linear-gradient() values, and a gradient is not interpolable: the specification treats it as a discrete value, the browser shows one step or the other, never a mix. Measured in Chromium 151: the switch happens at mid-segment (66, 196, 326, 456 px for 520 of travel); in WebKit 26.5, from the first pixel of the segment (1, 131, 261, 391 px). For a real cross-fade, animate colors, not images: declare two registered properties — @property --c1 { syntax: '<color>'; inherits: false; initial-value: #1e1b4b }, same for --c2 —, write background: linear-gradient(135deg, var(--c1), var(--c2)) on .sdc-color-bg and vary --c1 and --c2 in the steps: a registered <color> interpolates, so the gradient then slides continuously (Chrome 85+, Safari 16.4+)..sdc-color-bg and all your content is positioned and spans the whole scrollable height. The background is position: absolute; inset: 0: it takes the dimensions of the first positioned ancestor. In the sold code that is .sdc-color-content (position: relative, 800 px). If your section has no position, the positioned ancestor becomes the scrolling box itself (.sdc-scroll-container is position: relative): the background is only as tall as the visible height, 280 px, leaves with the content and exits through the top — measured: a 20 px strip at 260 px of scrolling, nothing from 326 on, the last two palettes never shown. One line is enough: position: relative on the background's direct parent.animation-timeline: scroll(), but neither the same property nor the same surface. fx-0554 animates the background-color of a 180 px square — #6366f1 → #d946ef → #06b6d4 → #10b981, four steps — and the color of a caption: a color interpolates, so it slides continuously (measured: rgb(133, 93, 240) at 50 px, rgb(168, 83, 240) at 100 px), but around the square the background stays black. fx-0559 animates the background of the whole backdrop with five gradients: the whole section changes mood, in clean switches. fx-0585 Sticky Image Change is closer on the mechanism — four linear-gradient(135deg, …) held in stages (0–20, 25–45, 50–70, 75–100%) on animation-timeline: scroll(nearest), hence the same clean switches — but not on the surface nor the structure: what changes color is a 120 px band in position: sticky; top: 10px, which stays in view and signals the section being read by its tint, while four text panels each reveal themselves on entry through animation-timeline: view() (animation-range: entry 0% entry 50%); the box's own background stays dark (#0a0a0f). fx-0559 has neither sticky nor panels: a single absolute background under the whole section, and nothing else moves. Pick fx-0554 to tint an object set in a text, fx-0585 for a pinned marker that goes along with panels, fx-0559 to change the atmosphere of an entire section.