ScrollFree

Sticky Image Change

A 120 px “01” block pinned 10 px from the top of a 280 px box, whose gradient and shadow change in steps — indigo, violet, magenta, pink-amber — over 920 px of scroll while four panels reveal below: two CSS timelines, scroll() and view(), zero JavaScript.

CSSStickyScroll

Updated

01

First Section

The image stays fixed while content scrolls. Ideal technique for presenting features.

Second Section

Each text section triggers a visual change in the sticky image.

Third Section

Smooth transitions guide the user through content seamlessly.

Fourth Section

A pattern used in interactive journalism and product websites.

Scroll ↓
Hover or click the scene to interact

This effect is free — the Scroll category contains 70 effects total, including 12 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

MAISON ORÉE Autumn 26 collection · 60 pieces Filter

Indigo

15 pieces
Loden coat189 €
Roll-neck jumper95 €
Pleated skirt120 €
Wool scarf49 €

Plum

15 pieces
Midi dress145 €
Mohair cardigan129 €
Beret39 €
Wide trousers110 €

Terracotta

15 pieces
Short trench210 €
Linen shirt85 €
Bucket bag160 €
Silk scarf45 €

Ivory

15 pieces
Cream blazer230 €
Cable knit115 €
Ecru jeans99 €
Beanie35 €

① Chromatic band for a textile collection — indigo, plum, terracotta, ivory across 60 pieces

WhenCatalog page of a fashion brand: 60 pieces in four color families; a sticky band under the site bar follows the scroll and changes hue with the chromatic weight of the products on screen.
WhyA 46 px band, not the page background: it flips cleanly from one hue to the next at the offset where the next family reaches the top third of the box, and the 60 pieces keep their own colors below, on white cards — Color Shift (fx-0559) would tint the whole section behind the products, right where the shopper is comparing colors; Scroll Snap Sections (fx-0584, dots + sections) would freeze the collection into screens whereas the grid must scroll freely.
Settings46 px band (top: 8px) under a 42 px bar, 318 px box, four families of four pieces out of 60; @keyframes exTextile reuses the percentages of ssImgChange with the collection's gradients and animates color too (brown text on ivory); animation-range: -38px 590px aligns the steps with the families (482 px travel, switches measured at 103, 260 and 417 px, when the next family reaches the top third of the box); “Indigo · pieces 1 to 15” label as four stacked spans on the same scroll() timeline; panels as white cards revealed by the sold code's view().
PULSE Power session 12 exercises · 42 min. The band pinned at the bottom follows the effort zone of the exercise on screen.
Warm-up Endurance Effort Max intensity
9:41●●● ▲ ▮
Power sessionThursday · 42 min · 12 exercises

Light rowing

Warm-up · 5 min

50 %

Hip mobility

Warm-up · 2 × 10

—

Bodyweight squat

Warm-up · 2 × 15

—

Walking lunges

Endurance · 3 × 12

60 %

Push-ups

Endurance · 3 × 12

—

Plank

Endurance · 3 × 45 s

—

Barbell squat

Effort · 4 × 8

70 %

Deadlift

Effort · 4 × 6

75 %

Bench press

Effort · 4 × 8

70 %

Sprints

Max intensity · 6 × 20 s

95 %

Burpees

Max intensity · 3 × 15

90 %

Kettlebell swing

Max intensity · 4 × 15

85 %

② Effort-zone indicator in a fitness app — the band pinned at the bottom goes from blue to red

WhenSports app on a phone: a list of exercises of increasing power; a sticky band at the bottom of the screen goes from blue (warm-up) to green, orange (effort) and red (maximum intensity), with the zone and the target heart rate.
WhyThe band stays pinned to the bottom of the screen and its color flips when the first exercise of the next zone reaches the middle of the list: the effort zone is read from the scroll position, not from a timer, and scrolling back up turns it blue again, while each row reveals below it through view() — Split Screen Sticky (fx-0589), built on the same base, keeps a fixed background and rotates the icon inside it: it never says which zone you are in.
SettingsBlock pinned at the bottom — top: auto; bottom: 8px, last child of the content (a bottom sticky clings to the lower edge as long as its parent extends past it); 212 × 258 px box inside a phone, twelve exercises in four zones of three, 40 px band; animation-range: -80px 604px (474 px travel, switches at 74, 245 and 416 px, when the first exercise of the next zone reaches the middle of the list); panels revealed on entry 48px entry 96px, once the row is out from under the band; scrollbar hidden.
Penetration test report
Customer portal v4.2 · 12 findings, ascending severity
CVSS 3.1
Informational (3)
VULN-01

Server header disclosed

2.3
VULN-02

Debug HTML comments in production

2.6
VULN-03

Outdated jQuery version

3.7
Medium (3)
VULN-04

Session cookie without SameSite

5.3
VULN-05

User enumeration at login

5.9
VULN-06

Missing Content-Security-Policy

6.5
High (3)
VULN-07

IDOR on /api/invoices/{id}

7.5
VULN-08

Stored XSS in the profile page

8.2
VULN-09

Predictable password reset token

8.8
Critical (3)
VULN-10

SQL injection in the search

9.4
VULN-11

JWT alg=none authentication bypass

9.8
VULN-12

Remote code execution via file upload

10.0

③ Severity signage in a security report — from green to red down the findings

WhenPenetration test report in an internal security tool: twelve vulnerabilities sorted by ascending severity; a sticky band goes from green (informational) to amber, orange and red (critical) as the analyst scrolls down.
WhyFour steps for four groups: the band turns amber, then orange, then red the instant the next group's heading reaches the middle of the box, with no half-tone between two levels — an orange gliding toward red would announce a severity that does not exist. Scroll Progress Beam (fx-0793, side rail) measures the position in the document; it does not say the severity of what is on screen.
Settings44 px band under a 40 px bar, 320 px box, twelve findings in four groups of three with a heading per group; @keyframes exSeverity green → amber → orange → red, animation-range: -67px 533px (452 px travel, switches at 68, 218 and 368 px, when the next group's heading reaches the middle of the box); left border of each finding in its group's color, identifier and CVSS score in monospace; the “Medium · CVSS 4.0 – 6.9” label is spelled out, color never carries the information alone.

How it works

The sold HTML: the .demo-preview frame (flex, 280px, overflow: hidden, #0a0a0f background, sans-serif), the .sdc-scroll-container box (100% × 280px, overflow-y: auto, scroll-behavior: smooth, a 4 px scrollbar via ::-webkit-scrollbar), the .ss-imgchange-content content (1200px in border-box, padding: 20px 12px), the .ss-imgchange-display block and the .ss-imgchange-panels column (gap: 16px). Travel: 1200 − 280 = 920 px (measured). The 120px block (14 px radius, “01” at 2rem weight 800) holds with position: sticky; top: 10px; z-index: 2: 20 px from the top at rest, pinned at 10 px after 10 px of scrolling. Its animation: ssImgChange linear; animation-timeline: scroll(nearest) swaps time for the position of the nearest scroller, the box. The @keyframes ssImgChange is nothing but plateaus — 0%, 20% indigo → violet (#6366f1 → #8b5cf6), 25%, 45% violet → magenta (→ #d946ef), 50%, 70% magenta → pink (→ #f43f5e), 75%, 100% pink → amber (→ #f59e0b), each with a 0 8px 30px shadow tinted at 30%. In pixels: 0-184, 230-414, 460-644, 690-920, separated by three 46 px transitions.

Measured in Chromium on a blank page, the transitions are asymmetric: the shadow interpolates continuously (at 200 px, rgba(113, 99, 243, .3)), but a linear-gradient is not interpolated between two keyframes — the engine jumps to the next value halfway through the segment: 207 → 208 px (22.5%), 437 → 438 (47.5%), 666 → 667 (72.5%). WebKit 26 jumps too, right at the start of the transition (measured 185 px). A color that changes cleanly under a shadow that glides: that is the effect's character, one section, one color. The “01” never changes — static text, no counter drives it.

The panels have their own timeline. Each .ss-imgchange-panel (white background at 4%, border at 8%, 13.6 px h4, 11.5 px p in white at 50%) starts at opacity: 0; transform: translateY(20px) and plays sbFadeUp on animation-timeline: view(), animation-range: entry 0% entry 50%: the fade starts when its top crosses the bottom edge of the box and ends when it is halfway in — 36 px of travel for a 72 px panel (measured at 1,280 px wide); both keeps the final state. At rest: panel 1 full, 2 at 0.88, 3 and 4 at 0; panel 4 reaches 1 at 184 px. The block covers the top 130 px, the panels pass underneath: 150 px of reading. They occupy pixels 160 to 517 of the content: from 387 px of scrolling on, none shows any more, and the last 533 px of travel — the third and fourth steps — scroll over empty space (at 390 px wide, the text wraps and fills part of it). scroll-behavior: smooth only eases programmatic scrolls (scrollTop = 460: 332 ms in Chromium, 528 ms in Firefox). The JavaScript field is empty; .sdc-scroll-container, the .sdc-scroll-hint hint (“Scroll ↓”, absolute inside the frame, pulsing from 0.3 to 0.7 over 2 s) and sbFadeUp are the category's shared base, identical in Split Screen Sticky (fx-0589): pasting both on one page creates no conflict.

Accessibility

  • prefers-reduced-motion not in the code: measured under emulation, nothing changes — steps, the panels' 20 px slide, the hint's pulse (running), smooth. The timelines are driven by the reader (interaction-triggered animation, WCAG 2.3.3), the pulse is not. Add @media (prefers-reduced-motion: reduce) { .ss-imgchange-panel { animation: none; opacity: 1; transform: none } .sdc-scroll-hint { animation: none } .sdc-scroll-container { scroll-behavior: auto } }; the color steps move nothing and can stay.
  • Keyboard: measured, Tab focuses the box without a tabindex in Chromium and Firefox; Arrow Down moves 40 px (Chromium) or 51 px (Firefox), Page Down 285 / 303 px, End goes to 920 — steps and panels follow, the timelines read the position, not the gesture. WebKit does not focus a scrolling box without tabindex="0": add it with an aria-label and a :focus-visible style.
  • Contrast: the white “01”, 32 px weight 800 (large text, 3:1 threshold), reads 4.5:1 on #6366f1, 4.2:1 on #8b5cf6, 3.5:1 on #d946ef, 3.7:1 on #f43f5e but 2.2:1 on the amber #f59e0b: the bottom-right corner of the last step fails — switch to #b45309 (5.0:1). Panel headings 18:1; paragraph in white at 50% on the effective #141419 background: 5.3:1 at 11.5 px, just above the threshold. The “Scroll ↓” hint, white at 30% on #0a0a0f, reads 2.6:1 static, 1.2:1 to 1.8:1 under the pulse — the two opacities multiply, 0.09 to 0.21: decorative, aria-hidden="true".
  • Screen readers: the block is a div read as “01”; its color is not announced — if it carries meaning (family, zone, severity), spell it out, as the three scenes do. Four h4s with no h2/h3 above break the page outline. Panels at opacity: 0 stay in the accessibility tree and are all read from the start, which is intended. Without animation-timeline (Firefox), everything is visible, flat. overscroll-behavior: contain keeps the page from being dragged along at the end of the box.

Browser compatibility

CSS only: animation-timeline: scroll(nearest) and view(), animation-range, position: sticky, gradients, @keyframes. Empty JavaScript field, zero dependencies. Chrome and Edge 115+, Safari and iOS 26+; Firefox: no version (measured: CSS.supports('animation-timeline: scroll()') = false, computed duration 0s). What a Firefox visitor sees, measured on a blank page: the box scrolls, the block stays pinned at 10 px, indigo → violet from start to finish, with no shadow (it only exists in the keyframes), and the four panels are visible, in place, at opacity 1 — the 0 s animation with both freezes on its final state. Nothing hidden: the code has no @supports and needs none. For a shadow in Firefox, put a box-shadow in the block's base rule.

Chrome 115+✓ Full
Firefox✓ No version — flat: fixed indigo block with no shadow, panels visible
Safari 26+✓ Full (before 26: flat, as in Firefox)
Edge 115+✓ Full
Mobile iOS 26+✓ Full (touch scrolling of the box)
Android Chrome 115+✓ Full

Same in Safari before 26 and Chrome before 115: two 0 s animations, panels at their final state, block on its base rule, the page readable in full — only the color no longer tells anything. Without position: sticky, the block would rise with the content; where overflow is not honored (mail clients), the content stretches to 1,200 px, panels visible.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="sdc-scroll-container">
    <div class="ss-imgchange-content">
      <div class="ss-imgchange-display">01</div>
      <div class="ss-imgchange-panels">
        <div class="ss-imgchange-panel"><h4>First Section</h4><p>The image stays fixed while content scrolls. Ideal technique for presenting features.</p></div>
        <div class="ss-imgchange-panel"><h4>Second Section</h4><p>Each text section triggers a visual change in the sticky image.</p></div>
        <div class="ss-imgchange-panel"><h4>Third Section</h4><p>Smooth transitions guide the user through content seamlessly.</p></div>
        <div class="ss-imgchange-panel"><h4>Fourth Section</h4><p>A pattern used in interactive journalism and product websites.</p></div>
      </div>
    </div>
  </div>
  <span class="sdc-scroll-hint">Scroll ↓</span>
CSS
.ss-imgchange-content {
  height: 1200px;
  padding: 20px 12px;
}

.ss-imgchange-display {
  width: 100%;
  height: 120px;
  border-radius: 14px;
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
  font-weight: 800;
  color: white;
  position: sticky;
  top: 10px;
  z-index: 2;
  animation: ssImgChange linear;
  animation-timeline: scroll(nearest);
}

.ss-imgchange-panels {
  padding-top: 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.ss-imgchange-panel {
  padding: 16px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  opacity: 0;
  transform: translateY(20px);
  animation: sbFadeUp linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 50%;
}

.ss-imgchange-panel h4 {
  font-size: 0.85rem;
  font-weight: 700;
  color: #fff;
  margin: 0 0 6px;
}

.ss-imgchange-panel p {
  font-size: 0.72rem;
  color: rgba(255, 255, 255, 0.5);
  margin: 0;
  line-height: 1.5;
}

.sdc-scroll-container {
  width: 100%;
  height: 280px;
  overflow-y: auto;
  position: relative;
  scroll-behavior: smooth;
}

.sdc-scroll-container::-webkit-scrollbar {
  width: 4px;
}

.sdc-scroll-container::-webkit-scrollbar-track {
  background: rgba(255, 255, 255, 0.05);
}

.sdc-scroll-container::-webkit-scrollbar-thumb {
  background: rgba(139, 92, 246, 0.4);
  border-radius: 2px;
}

.sdc-scroll-hint {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.65rem;
  color: rgba(255, 255, 255, 0.3);
  z-index: 5;
  pointer-events: none;
  animation: sdcHintPulse 2s ease-in-out infinite;
}

@keyframes sbFadeUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes ssImgChange {

  0%,
  20% {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    box-shadow: 0 8px 30px rgba(99, 102, 241, 0.3);
  }

  25%,
  45% {
    background: linear-gradient(135deg, #8b5cf6, #d946ef);
    box-shadow: 0 8px 30px rgba(139, 92, 246, 0.3);
  }

  50%,
  70% {
    background: linear-gradient(135deg, #d946ef, #f43f5e);
    box-shadow: 0 8px 30px rgba(217, 70, 239, 0.3);
  }

  75%,
  100% {
    background: linear-gradient(135deg, #f43f5e, #f59e0b);
    box-shadow: 0 8px 30px rgba(244, 63, 94, 0.3);
  }
}

@keyframes sdcHintPulse {

  0%,
  100% {
    opacity: 0.3;
  }

  50% {
    opacity: 0.7;
  }
}
JavaScript (fx-0585)

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Color steps (CSS — @keyframes ssImgChange) 4 plateaus: 0-20%, 25-45%, 50-70%, 75-100%, 135° gradients Each pair of percentages is a plateau, the 5% gap (46 px) a transition: the shadow fades through it, the gradient flips halfway (measured 207, 437, 667 px). Fifth step: redistribute the percentages (0-16, 20-36…); true fade: animate background-color on a solid color.
Travel (CSS — .ss-imgchange-content { height: 1200px }, .sdc-scroll-container { height: 280px }) 920 px (230 px per step) The percentages are those of the whole travel, 1,200 − 280. The panels stop at 517 px: bring the content down to 800px (520 px travel) so the four steps play while there is still something to read, or add panels.
Aligning the steps (CSS — animation-range to add on .ss-imgchange-display) absent (normal: 0% at the top, 100% at the end of the travel) Two pixel lengths redefine where 0% and 100% of the timeline fall: animation-range: -38px 590px puts the switches (22.5 / 47.5 / 72.5%) at the offsets where each group takes over. The key setting of the three scenes; recompute it if the heights change.
Panel reveal (CSS — .ss-imgchange-panel, animation-range: entry 0% entry 50%) fade over the first half of the entry, 20 px slide entry 0% entry 100% spreads the fade over the whole entry; entry 48px entry 96px delays it by 48 px (under a bottom band). Keep both: without it, a panel past its range drops back to opacity: 0.
Pin point and block height (CSS — top: 10px, height: 120px) 10 px / 120 px (130 px covered, 150 px of reading) A 60 px block leaves 210 px to the panels. top: auto; bottom: 8px pins it at the bottom — it must then be the last child of the content. The z-index: 2 keeps it above the panels.
Block content (HTML — “01”, CSS — font-size: 2rem; font-weight: 800) “01”, static The text is not driven by the scroll: image, SVG or label, as you like. For one label per step, stack spans at opacity: 0 on the same scroll(nearest) timeline, each with its own animation-range slice — that is what the scenes do.
Smooth scrolling, hint and scrollbar (CSS — scroll-behavior: smooth, .sdc-scroll-hint, ::-webkit-scrollbar) smooth / “Scroll ↓” pulsing 2 s / 4 px bar smooth only acts on positions set by script or anchor (measured: 460 px in 332 ms). The hint is hard-coded text (“Scroll ↓”), absolute inside .demo-preview: edit it or remove the span along with its @keyframes sdcHintPulse. The styled bar only exists in Chromium and Safari; for Firefox, scrollbar-width: thin.

FAQ

Why does the color jump at once while the shadow fades?

Because the keyframes animate a linear-gradient, which no measured engine interpolates: Chromium switches to the next value halfway through the segment (22.5%, 47.5%, 72.5%: 207 → 208, 437 → 438, 666 → 667 px), WebKit 26 right at its start (185 px). The shadow — a color, some lengths — interpolates normally over the 46 px of each transition (at 200 px, rgba(113, 99, 243, .3)). That clean flip is the effect's character: one section, one color. For a continuous fade, animate background-color on a solid color, or a custom property registered with @property that the gradient reads.

How does it differ from Split Screen Sticky (fx-0589)?

The base is identical — same 280 px .sdc-scroll-container box, same block pinned at top: 10px, same panels revealed by sbFadeUp on view() between entry and entry 50% — but what is animated on scroll differs. In fx-0589 (premium), the 100 px block keeps a fixed background (#1a1a3e → #2d1b69) and it is the SVG icon inside it that rotates 360° on animation-timeline: scroll(), continuously, over 720 px of travel, in front of three list cards. In fx-0585, the block itself changes: gradient and shadow in steps, four colors for four panels, 920 px of travel, nothing animated inside. Pick fx-0585 when the color must say where you are (family, zone, severity); fx-0589 when a fixed visual must live while the text scrolls.

How do I line up one step with each panel, or each group of panels?

With animation-range in pixels on the block, without touching the percentages. Note the offset at which each group should take over (its top in the content, minus the height at which you want it to pass: bottom edge of the block, middle of the box); those offsets must land on the switches, at 22.5%, 47.5% and 72.5% of the range. For evenly spaced hand-overs, from the first two offsets o₁ and o₂: length = (o₂ − o₁) ÷ 0.25, start = o₁ − 0.225 × length — a negative start is allowed, the first step is simply already under way at rest. The three scenes fit in one line each (-67px 533px for twelve findings in four groups, 452 px travel). If the groups differ a lot in height, change the @keyframes percentages instead. In the sold code nothing is aligned: the steps spread over the whole travel and the panels stop at 517 px out of 920.