Vertical Story-Style Slider — HTML CSS JS Snippet
Vertical Story-Style Slider · Mobile · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="vst-phone" id="vstPhone">
<div class="vst-bars" id="vstBars"></div>
<div class="vst-header">
<img class="vst-avatar" src="https://i.pravatar.cc/60?img=32" alt="">
<div class="vst-who"><strong>webdevpuneet</strong><span>2h ago</span></div>
</div>
<div class="vst-slides" id="vstSlides">
<div class="vst-slide" style="background:linear-gradient(160deg,#6366f1,#4338ca)"><h2>New tool just dropped 🚀</h2><p>Build sitemaps in one click.</p></div>
<div class="vst-slide" style="background:linear-gradient(160deg,#ec4899,#9d174d)"><h2>1788+ snippets</h2><p>Copy-paste HTML, CSS & JS, free.</p></div>
<div class="vst-slide" style="background:linear-gradient(160deg,#0ea5e9,#0369a1)"><h2>Export anywhere</h2><p>React, Vue, Angular & Tailwind.</p></div>
<div class="vst-slide" style="background:linear-gradient(160deg,#10b981,#047857)"><h2>Swipe to see more</h2><p>Tap the sides to move — try it.</p></div>
</div>
<button class="vst-tap vst-tap-left" id="vstPrev" aria-label="Previous story"></button>
<button class="vst-tap vst-tap-right" id="vstNext" aria-label="Next story"></button>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f1117;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.vst-phone{position:relative;width:280px;height:500px;border-radius:26px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,.5)}
.vst-bars{position:absolute;top:10px;left:10px;right:10px;display:flex;gap:5px;z-index:3}
.vst-bar{flex:1;height:3px;border-radius:3px;background:rgba(255,255,255,.35);overflow:hidden}
.vst-bar-fill{height:100%;width:0%;background:#fff;border-radius:3px}
.vst-bar.done .vst-bar-fill{width:100%}
.vst-bar.active .vst-bar-fill{transition:width linear}
.vst-header{position:absolute;top:22px;left:12px;right:12px;display:flex;align-items:center;gap:8px;z-index:3}
.vst-avatar{width:28px;height:28px;border-radius:50%;border:1.5px solid rgba(255,255,255,.8)}
.vst-who{display:flex;flex-direction:column;color:#fff;font-size:11px;line-height:1.3}
.vst-who strong{font-size:12.5px}
.vst-who span{opacity:.75}
.vst-slides{position:relative;width:100%;height:100%}
.vst-slide{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:flex-end;padding:28px 22px 40px;opacity:0;transition:opacity .25s ease}
.vst-slide.active{opacity:1;z-index:1}
.vst-slide h2{color:#fff;font-size:21px;font-weight:800;margin-bottom:8px;text-shadow:0 2px 8px rgba(0,0,0,.3)}
.vst-slide p{color:rgba(255,255,255,.88);font-size:13.5px;line-height:1.5}
.vst-tap{position:absolute;top:0;bottom:0;width:34%;border:none;background:transparent;cursor:pointer;z-index:2}
.vst-tap-left{left:0}
.vst-tap-right{right:0}var slides = document.querySelectorAll('.vst-slide');
var barsWrap = document.getElementById('vstBars');
var current = 0;
var DURATION = 4000;
var timer = null;
slides.forEach(function () {
var bar = document.createElement('div');
bar.className = 'vst-bar';
bar.innerHTML = '<div class="vst-bar-fill"></div>';
barsWrap.appendChild(bar);
});
var bars = document.querySelectorAll('.vst-bar');
function render() {
slides.forEach(function (s, i) { s.classList.toggle('active', i === current); });
bars.forEach(function (b, i) {
var fill = b.querySelector('.vst-bar-fill');
fill.style.transition = 'none';
b.classList.toggle('done', i < current);
b.classList.toggle('active', i === current);
if (i < current) { fill.style.width = '100%'; }
else if (i > current) { fill.style.width = '0%'; }
else {
fill.style.width = '0%';
// force reflow so the transition restarts cleanly on repeated visits
void fill.offsetWidth;
fill.style.transition = 'width ' + DURATION + 'ms linear';
requestAnimationFrame(function () { fill.style.width = '100%'; });
}
});
}
function goTo(i) {
if (i < 0) i = 0;
if (i >= slides.length) { current = slides.length - 1; render(); return; }
current = i;
render();
restart();
}
function next() { if (current < slides.length - 1) goTo(current + 1); }
function prev() { if (current > 0) goTo(current - 1); }
function restart() {
clearTimeout(timer);
timer = setTimeout(function () {
if (current < slides.length - 1) next();
}, DURATION);
}
document.getElementById('vstNext').addEventListener('click', next);
document.getElementById('vstPrev').addEventListener('click', prev);
document.addEventListener('keydown', function (e) {
if (e.key === 'ArrowRight') next();
else if (e.key === 'ArrowLeft') prev();
});
render();
restart();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Vertical Story-Style Slider — Segmented Progress Bars & Tap-to-Advance

Stories replaced the carousel on every social app for one reason: the segmented bar at the top *shows you the pacing* before you even interact — how many slides remain, and roughly how much time is left on this one. This snippet rebuilds that exact mechanic from scratch: one thin bar per slide, the active one fills left-to-right over a fixed duration via a CSS width transition, and finishing a fill auto-advances to the next slide.
A CSS transition doing the timer's job
Instead of animating the fill with requestAnimationFrame on every tick, each active bar's fill gets transition: width 4000ms linear and then its width is set to 100% — the browser handles the actual animation, and a plain setTimeout matching that same duration handles advancing to the next slide. Re-visiting an already-finished slide resets its bar to full instantly (no transition), and resetting to a not-yet-reached slide snaps it back to empty — so scrubbing back and forth always shows an accurate state, not leftover animation frames.
Two invisible tap zones, not visible buttons
The left/right third of the phone frame are full-height, borderless buttons — tapping the right side advances, the left side goes back, exactly like every native stories implementation. Arrow keys do the same on desktop, so the pattern works with a keyboard too, not just a thumb.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA phone-framed story starts playing automatically — watch the first progress bar fill.
- 2Tap the right edgeAdvances to the next slide immediately and resets the timer for the new one.
- 3Tap the left edgeGoes back one slide — its bar refills from the start.
- 4Wait it outEach slide auto-advances when its bar finishes filling, just like a native story.
- 5Add a fifth slideAdd one more .vst-slide div — a matching progress bar segment is generated automatically.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Change the DURATION constant (in milliseconds) at the top of the JS. It drives both the progress bar's fill transition and the setTimeout that triggers auto-advance, so they always stay in sync.
Tapping either side or pressing an arrow key immediately jumps to a slide and restarts its timer via restart() — so interacting always resets the pacing rather than fighting the running timer.
Listen for pointerdown on the phone frame and clearTimeout(timer) plus pause the active bar's fill (set its width to its current computed value and remove the transition); on pointerup, resume by re-triggering the transition for the remaining width.
Yes — .vst-slide is just a flex container; replace the background gradient with a background-image, or add an <img>/<video> as its first child sized with object-fit: cover.
Yes — Left/Right arrow keys navigate exactly like the tap zones, and the tap zones themselves are real <button> elements with aria-labels.