Beeldsequentie op scroll New
A sequence of 24 frames on a canvas that follows the scroll, like product pages showing a rotating product. Here generated from three photos of an attic conversion (crossfade and gentle zoom); in production you preload an exported sequence (frame-01.webp to frame-24.webp). Canvas 2D + scroll event with requestAnimationFrame; with reduced motion no zoom and no automatic preview scroll.



Week 1: kale zolder van 22 m²
Week 2: isoleren en aftimmeren
Week 3: een slaapkamer erbij
Daarna loopt de pagina door met de offerte en de planning.
How to use it
- Paste the HTML where the scroll effects should appear.
- Add the CSS to your stylesheet. All classes are prefixed, so nothing clashes with your own styles.
- Add the JavaScript and call init() with the element that contains the HTML.
<!-- Beeldsequentie op scroll · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="sbs">
<div class="sbs__src" hidden><img src="https://neddev.com/lab-media/scroll-beeldsequentie-1.webp" alt=""><img src="https://neddev.com/lab-media/scroll-beeldsequentie-2.webp" alt=""><img src="https://neddev.com/lab-media/scroll-beeldsequentie-3.webp" alt=""></div>
<div class="sbs__sc" tabindex="0" aria-label="Voorbeeld: zolderverbouwing in Haarlem, scroll om de verbouwing te volgen">
<div class="sbs__track"><div class="sbs__pin">
<canvas class="sbs__cv" role="img" aria-label="Zolder in Haarlem, van kale ruimte tot slaapkamer"></canvas>
<div class="sbs__cap"><p class="sbs__t is-on">Week 1: kale zolder van 22 m²</p><p class="sbs__t">Week 2: isoleren en aftimmeren</p><p class="sbs__t">Week 3: een slaapkamer erbij</p></div>
<div class="sbs__bar" aria-hidden="true"><i class="sbs__fill"></i></div>
</div></div>
<div class="sbs__na"><p>Daarna loopt de pagina door met de offerte en de planning.</p></div>
</div>
</div>
</div>/* Beeldsequentie op scroll · NedDev · eigen werk. */
.sbs{position:absolute;inset:0;background:#0a0a0a;color:#fff;font-family:'Switzer',sans-serif;container-type:size;text-align:left}
.sbs__sc{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:#5c5a57 transparent}
.sbs__sc:focus-visible{outline:2px solid #F5480F;outline-offset:-2px}
.sbs__track{height:420cqh;position:relative}
.sbs__pin{position:sticky;top:0;height:100cqh;overflow:hidden}
.sbs__cv{display:block;width:100%;height:100%;background:#222}
.sbs__cap{position:absolute;left:16px;right:16px;bottom:20px;height:1.4em;font:600 clamp(14px,4.4cqi,24px)/1.2 'Switzer',sans-serif;letter-spacing:-.01em;text-shadow:0 1px 12px rgba(0,0,0,.55)}
.sbs__t{position:absolute;left:0;bottom:0;margin:0;opacity:0;transform:translateY(6px);transition:opacity .25s,transform .25s}
.sbs__t.is-on{opacity:1;transform:none}
.sbs__bar{position:absolute;left:16px;right:16px;bottom:10px;height:2px;background:rgba(255,255,255,.3)}
.sbs__fill{display:block;height:100%;width:100%;background:#F5480F;transform-origin:left;transform:scaleX(0)}
.sbs__na{padding:18px 16px 28px;font:400 14px/1.5 'Switzer',sans-serif;color:#d6d3ce}
.sbs__na p{margin:0}
@media (prefers-reduced-motion:reduce){.sbs__t{transition:none;transform:none}}// Beeldsequentie op scroll · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.sbs');if(!w)return;
var sc=w.querySelector('.sbs__sc'),cv=w.querySelector('.sbs__cv'),ctx=cv.getContext('2d');
var imgs=[].slice.call(w.querySelectorAll('.sbs__src img')),caps=[].slice.call(w.querySelectorAll('.sbs__t')),fill=w.querySelector('.sbs__fill');
var rm=matchMedia('(prefers-reduced-motion: reduce)').matches,F=24,frames=[],cur=-1,W=0,H=0;
function cover(c,im,z,a){if(!im.naturalWidth)return;var s=Math.max(W/im.naturalWidth,H/im.naturalHeight)*z,dw=im.naturalWidth*s,dh=im.naturalHeight*s;c.globalAlpha=a;c.drawImage(im,(W-dw)/2,(H-dh)/2,dw,dh);c.globalAlpha=1;}
function build(){var d=Math.min(2,window.devicePixelRatio||1);W=Math.round(cv.clientWidth*d);H=Math.round(cv.clientHeight*d);if(!W||!H)return;cv.width=W;cv.height=H;frames=[];
for(var f=0;f<F;f++){var t=f/(F-1)*(imgs.length-1),k=Math.min(imgs.length-2,Math.floor(t)),a=t-k,e=a*a*(3-2*a);
var oc=document.createElement('canvas');oc.width=W;oc.height=H;var c=oc.getContext('2d');c.fillStyle='#222';c.fillRect(0,0,W,H);
var z1=rm?1:1+.07*a,z2=rm?1:1.07-.07*a;cover(c,imgs[k],z1,1);cover(c,imgs[k+1],z2,e);frames.push(oc);}cur=-1;draw();}
function draw(){var max=sc.scrollHeight-sc.clientHeight,tr=w.querySelector('.sbs__track').offsetHeight-sc.clientHeight;var p=tr>0?Math.min(1,sc.scrollTop/tr):0;
var f=Math.round(p*(F-1));fill.style.transform='scaleX('+p.toFixed(3)+')';
if(f!==cur&&frames[f]){cur=f;ctx.clearRect(0,0,W,H);ctx.drawImage(frames[f],0,0);}
var ci=p<.36?0:(p<.72?1:2);caps.forEach(function(el,j){el.classList.toggle('is-on',j===ci);});}
var tick=false;sc.addEventListener('scroll',function(){if(!tick){tick=true;requestAnimationFrame(function(){tick=false;draw();});}},{passive:true});
var left=imgs.length;function ready(){if(--left<=0)build();}
imgs.forEach(function(im){if(im.complete&&im.naturalWidth)ready();else{im.addEventListener('load',ready);im.addEventListener('error',ready);}});
var rt;if('ResizeObserver' in window)new ResizeObserver(function(){clearTimeout(rt);rt=setTimeout(build,120);}).observe(cv);
if(!rm){var on=true,t0=0;['wheel','touchstart','pointerdown','keydown','focusin'].forEach(function(ev){w.addEventListener(ev,function(){on=false;},{passive:true});});
var tr=w.querySelector('.sbs__track');function drift(now){if(!on)return;if(!t0)t0=now;var m=tr.offsetHeight-sc.clientHeight;sc.scrollTop=m*(.5-.5*Math.cos((now-t0)/1000*.4));requestAnimationFrame(drift);}
setTimeout(function(){if(on)requestAnimationFrame(drift);},900);}
}
init(document.querySelector('.jouw-container'));
More scroll effects
Seen something you like?
Add it to your selection and send it over. We build your site around it, in your brand.
Get in touch →