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.
Updated
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.
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
Light rowing
Warm-up · 5 min
Hip mobility
Warm-up · 2 × 10
Bodyweight squat
Warm-up · 2 × 15
Walking lunges
Endurance · 3 × 12
Push-ups
Endurance · 3 × 12
Plank
Endurance · 3 × 45 s
Barbell squat
Effort · 4 × 8
Deadlift
Effort · 4 × 6
Bench press
Effort · 4 × 8
Sprints
Max intensity · 6 × 20 s
Burpees
Max intensity · 3 × 15
Kettlebell swing
Max intensity · 4 × 15
Server header disclosed
2.3Debug HTML comments in production
2.6Outdated jQuery version
3.7Session cookie without SameSite
5.3User enumeration at login
5.9Missing Content-Security-Policy
6.5IDOR on /api/invoices/{id}
7.5Stored XSS in the profile page
8.2Predictable password reset token
8.8SQL injection in the search
9.4JWT alg=none authentication bypass
9.8Remote code execution via file upload
10.0How 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
tabindexin 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 withouttabindex="0": add it with anaria-labeland a:focus-visiblestyle. - 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#f43f5ebut 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#141419background: 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
divread as “01”; its color is not announced — if it carries meaning (family, zone, severity), spell it out, as the three scenes do. Fourh4s with no h2/h3 above break the page outline. Panels atopacity: 0stay in the accessibility tree and are all read from the start, which is intended. Withoutanimation-timeline(Firefox), everything is visible, flat.overscroll-behavior: containkeeps 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.
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.
<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>
.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;
}
}
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
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.