Verhalen met voortgangsbalkjes New
Stories like on Instagram, but on your own site: portrait images with bars at the top that fill up, tap left or right for previous or next, press and hold to pause, and a visible pause button. Circles beside it jump to a story and turn grey once seen. Arrows, space and Home work; with reduced motion it starts paused and it stops when off screen. For bakeries, restaurants and shops that show something new every day.
Bakkerij De Korenaar
Twijnstraat, Utrecht
De Korenaar2 u
How to use it
- Paste the HTML where the components 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.
<!-- Verhalen met voortgangsbalkjes · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="vst">
<div class="vst__side">
<b class="vst__nm">Bakkerij De Korenaar</b>
<span class="vst__pl">Twijnstraat, Utrecht</span>
<div class="vst__hl" role="group" aria-label="Verhalen">
<button class="vst__h" type="button" data-i="0"><span class="vst__r"><img src="https://neddev.com/lab-media/verhalen-stories-1.webp" alt="" width="768" height="1376" loading="lazy"></span>Oven</button>
<button class="vst__h" type="button" data-i="1"><span class="vst__r"><img src="https://neddev.com/lab-media/verhalen-stories-2.webp" alt="" width="768" height="1376" loading="lazy"></span>Croissants</button>
<button class="vst__h" type="button" data-i="2"><span class="vst__r"><img src="https://neddev.com/lab-media/verhalen-stories-3.webp" alt="" width="768" height="1376" loading="lazy"></span>Winkel</button>
<button class="vst__h" type="button" data-i="3"><span class="vst__r"><img src="https://neddev.com/lab-media/verhalen-stories-4.webp" alt="" width="768" height="1376" loading="lazy"></span>Bezorgen</button>
</div>
</div>
<div class="vst__ph" role="region" aria-roledescription="verhaal" aria-label="Verhalen van Bakkerij De Korenaar">
<div class="vst__imgs">
<img class="vst__im is-on" src="https://neddev.com/lab-media/verhalen-stories-1.webp" alt="Bakker haalt zuurdesembroden uit de steenoven" width="768" height="1376">
<img class="vst__im" src="https://neddev.com/lab-media/verhalen-stories-2.webp" alt="Verse croissants op een rooster" width="768" height="1376" loading="lazy">
<img class="vst__im" src="https://neddev.com/lab-media/verhalen-stories-3.webp" alt="Etalage met brood op houten planken" width="768" height="1376" loading="lazy">
<img class="vst__im" src="https://neddev.com/lab-media/verhalen-stories-4.webp" alt="Bakfiets met broodkratten aan de gracht" width="768" height="1376" loading="lazy">
</div>
<div class="vst__bars" aria-hidden="true"><span><i></i></span><span><i></i></span><span><i></i></span><span><i></i></span></div>
<div class="vst__hd"><span class="vst__who">De Korenaar</span><span class="vst__ago">2 u</span><button class="vst__pause" type="button" aria-label="Pauzeren"><svg class="vst__i-pa" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"/></svg><svg class="vst__i-pl" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.5v13l10.5-6.5z"/></svg></button></div>
<button class="vst__tap vst__tap--l" type="button" aria-label="Vorig verhaal"></button>
<button class="vst__tap vst__tap--r" type="button" aria-label="Volgend verhaal"></button>
<p class="vst__cap" aria-live="polite"></p>
</div>
</div>
</div>/* Verhalen met voortgangsbalkjes · NedDev · eigen werk. */
.vst{position:absolute;inset:0;display:grid;grid-template-columns:1fr auto;gap:14px;padding:10px 14px 10px 16px;background:#f4f3f1;color:#0a0a0a;font-family:'Switzer',sans-serif;text-align:left;container-type:size}
.vst__side{display:flex;flex-direction:column;justify-content:center;min-width:0}
.vst__nm{font:600 15px/1.2 'Switzer',sans-serif;letter-spacing:-.01em}
.vst__pl{font:400 12px/1.3 'Switzer',sans-serif;color:#5c5a57;margin-bottom:10px}
.vst__hl{display:grid;grid-template-columns:repeat(2,minmax(0,64px));gap:8px 10px}
.vst__h{display:flex;flex-direction:column;align-items:center;gap:4px;padding:2px 0;border:0;background:none;color:#0a0a0a;font:500 12px/1.1 'Switzer',sans-serif;cursor:pointer;border-radius:4px}
.vst__r{display:block;width:44px;height:44px;padding:2px;border-radius:50%;border:2px solid #F5480F;box-sizing:border-box}
.vst__r img{display:block;width:100%;height:100%;border-radius:50%;object-fit:cover}
.vst__h.is-seen .vst__r{border-color:#c9c5bf}
.vst__h[aria-current="true"] .vst__r{border-color:#0a0a0a}
.vst__h:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.vst__ph{position:relative;height:calc(100cqh - 20px);aspect-ratio:9/16;border-radius:8px;overflow:hidden;background:#0a0a0a;color:#fff;user-select:none;-webkit-user-select:none;touch-action:manipulation}
.vst__imgs,.vst__im{position:absolute;inset:0}
.vst__im{width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .25s}
.vst__im.is-on{opacity:1}
.vst__ph.is-run .vst__im.is-on{animation:vst-zoom 5s linear both}
.vst__ph.is-hold .vst__im.is-on,.vst__ph.is-paused .vst__im.is-on{animation-play-state:paused}
@keyframes vst-zoom{from{transform:scale(1)}to{transform:scale(1.06)}}
.vst__ph::before,.vst__ph::after{content:"";position:absolute;left:0;right:0;z-index:1;pointer-events:none}
.vst__ph::before{top:0;height:60px;background:linear-gradient(rgba(10,10,10,.55),rgba(10,10,10,0))}
.vst__ph::after{bottom:0;height:90px;background:linear-gradient(rgba(10,10,10,0),rgba(10,10,10,.7))}
.vst__bars{position:absolute;left:6px;right:6px;top:6px;z-index:2;display:flex;gap:3px}
.vst__bars span{flex:1;height:3px;border-radius:2px;background:rgba(255,255,255,.4);overflow:hidden}
.vst__bars i{display:block;height:100%;background:#fff;transform-origin:left;transform:scaleX(0)}
.vst__hd{position:absolute;left:8px;right:4px;top:12px;z-index:3;display:flex;align-items:center;gap:6px;font:600 12px/1 'Switzer',sans-serif}
.vst__ago{font-weight:400;color:#d6d3ce}
.vst__who{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@container (max-width:360px){.vst__ago{display:none}}
.vst__pause{flex:none;margin-left:auto;width:26px;height:26px;padding:0;border:0;border-radius:4px;background:transparent;color:#fff;display:grid;place-items:center;cursor:pointer}
.vst__pause svg{width:16px;height:16px;fill:currentColor}
.vst__i-pl{display:none}.vst__ph.is-paused .vst__i-pl{display:block}.vst__ph.is-paused .vst__i-pa{display:none}
.vst__tap{position:absolute;top:44px;bottom:0;z-index:2;border:0;background:transparent;padding:0;cursor:pointer}
.vst__tap--l{left:0;width:35%}.vst__tap--r{right:0;width:65%}
.vst__tap:focus-visible,.vst__pause:focus-visible{outline:2px solid #F5480F;outline-offset:-2px}
.vst__cap{position:absolute;left:10px;right:10px;bottom:10px;z-index:3;margin:0;font:600 13px/1.3 'Switzer',sans-serif;pointer-events:none}
@container (max-height:225px){.vst__hl{gap:4px 8px}.vst__r{width:38px;height:38px}.vst__pl{margin-bottom:6px}.vst__cap{font-size:12px}}
@media (prefers-reduced-motion:reduce){.vst__im{transition:none}.vst__ph.is-run .vst__im.is-on{animation:none}}// Verhalen met voortgangsbalkjes · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.vst');if(!w)return;
var ph=w.querySelector('.vst__ph'),ims=[].slice.call(w.querySelectorAll('.vst__im')),bars=[].slice.call(w.querySelectorAll('.vst__bars i')),hs=[].slice.call(w.querySelectorAll('.vst__h')),cap=w.querySelector('.vst__cap'),pb=w.querySelector('.vst__pause');
var T=['Om half zes gaat de steenoven aan.','Croissants met roomboter, vanaf 7 uur in de winkel.','Vandaag extra: speltbrood met walnoot, € 4,95.','Bezorging in Utrecht-Oost. Bestel voor 10 uur, morgen bij je thuis.'];
var rm=matchMedia('(prefers-reduced-motion: reduce)').matches,N=ims.length,cur=0,el=0,DUR=5000,paused=rm,hold=false,vis=false,last=0,raf=0;
function show(i){cur=(i+N)%N;el=0;ims.forEach(function(m,k){m.classList.toggle('is-on',k===cur);});ph.classList.remove('is-run');void ph.offsetWidth;if(!paused&&!rm)ph.classList.add('is-run');
bars.forEach(function(b,k){b.style.transform='scaleX('+(k<cur?1:0)+')';});cap.textContent=T[cur];
hs.forEach(function(h,k){if(k===cur){h.setAttribute('aria-current','true');h.classList.add('is-seen');}else h.removeAttribute('aria-current');});}
function setPause(p){paused=p;ph.classList.toggle('is-paused',p);pb.setAttribute('aria-label',p?'Afspelen':'Pauzeren');if(!p&&!rm&&!ph.classList.contains('is-run'))ph.classList.add('is-run');}
function tick(now){raf=requestAnimationFrame(tick);var dt=last?Math.min(100,now-last):0;last=now;if(paused||hold||!vis||document.hidden)return;el+=dt;bars[cur].style.transform='scaleX('+Math.min(1,el/DUR).toFixed(4)+')';if(el>=DUR){if(cur===N-1){bars[cur].style.transform='scaleX(1)';setPause(true);}else show(cur+1);}}
w.querySelector('.vst__tap--l').addEventListener('click',function(){show(cur-1);});
w.querySelector('.vst__tap--r').addEventListener('click',function(){if(cur===N-1&&paused){show(0);setPause(false);}else show(cur+1);});
pb.addEventListener('click',function(){if(paused&&cur===N-1&&el>=DUR)show(0);setPause(!paused);});
hs.forEach(function(h,i){h.addEventListener('click',function(){show(i);if(!rm)setPause(false);});});
var ht=0;ph.addEventListener('pointerdown',function(e){if(e.target.closest('.vst__pause'))return;ht=setTimeout(function(){hold=true;ph.classList.add('is-hold');},180);});
function rel(e){clearTimeout(ht);if(hold){hold=false;ph.classList.remove('is-hold');if(e&&e.type==='pointerup'){var stop=function(ev){ev.stopPropagation();ev.preventDefault();ph.removeEventListener('click',stop,true);};ph.addEventListener('click',stop,true);setTimeout(function(){ph.removeEventListener('click',stop,true);},50);}}}
ph.addEventListener('pointerup',rel);ph.addEventListener('pointerleave',rel);ph.addEventListener('pointercancel',rel);
ph.addEventListener('keydown',function(e){var k=e.key;if(k==='ArrowRight'){show(cur+1);}else if(k==='ArrowLeft'){show(cur-1);}else if(k==='Home'){show(0);}else if(k===' '&&e.target.classList.contains('vst__tap')){e.preventDefault();setPause(!paused);}else return;if(k!==' ')e.preventDefault();});
new IntersectionObserver(function(es){es.forEach(function(e){vis=e.isIntersecting;});},{threshold:.4}).observe(ph);
setPause(rm);show(0);raf=requestAnimationFrame(tick);
}
init(document.querySelector('.jouw-container'));
More components
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 →