More Animations Snippets
Story Progress Bars — Instagram Stories HTML CSS JS
Story Progress Bars · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
transform: scaleX with a left origin, so progress animates on the compositor and stays smooth, not via layout-bound width.show resets the fill with the animation:none + reflow + restore trick, so re-entering a slide always replays its progress from zero.prev/next, matching the native story gesture without extra buttons.animation-play-state: paused on the active fill, freezing both the bar and the countdown until release.--dur custom property controls each slide's length, so changing pacing is one value, not code edits.opacity, a compositor-friendly crossfade rather than an abrupt swap.About this UI Snippet
Story Progress Bars — Auto-Advancing Segments, Tap Navigation & Hold-to-Pause

Stories — the tap-through, auto-advancing slideshow popularised by Snapchat and Instagram — are now everywhere: social apps, product tours, onboarding, and marketing. The defining UI element is the row of segmented progress bars across the top: one segment per slide, the current one filling in real time, completed ones full, upcoming ones empty. This snippet implements a complete story viewer in plain HTML, CSS, and vanilla JavaScript: auto-advancing segmented bars, tap-left/right navigation, hold-to-pause, and crossfading slides.
Segmented bars driven by one CSS animation
Each segment is a track containing a .st-fill that is scaled horizontally. The fill uses transform: scaleX (not width) with transform-origin: left, so the bar appears to fill left-to-right — and because it is a transform, the animation runs on the compositor and stays perfectly smooth. Completed bars get .done (scaleX(1)); the active bar gets .st-active, which runs the st-grow keyframe over a --dur duration (default 5s, linear). Upcoming bars stay at scaleX(0).
Auto-advance via animationend
Rather than juggling setTimeout timers (which drift and are hard to pause), the viewer advances when the active fill's animation finishes. A single animationend listener on the bars container checks for the st-grow animation and calls next(). This ties progression directly to the visible progress bar, so the slide always changes exactly when its bar completes — no desync between the timer and the indicator.
Restarting the fill cleanly
When show(i) activates a bar, it must restart the fill animation from zero even if that bar was animated before. It does this with the classic reset trick: set animation: none, force a reflow by reading offsetWidth, then clear the inline animation so the class-based one runs fresh. Without this, re-entering a slide would not replay its progress.
Tap navigation and hold-to-pause
Two transparent .st-nav buttons cover the left and right ~40% of the viewer — tapping them calls prev() and next(), exactly like the native gesture. Holding anywhere (mousedown/touchstart) adds a .paused class that sets animation-play-state: paused on the active fill, freezing both the bar and the countdown; releasing resumes it. This is the behaviour users expect when they press and hold to read a slide longer.
Crossfading slides
Slides stack absolutely and crossfade with an opacity transition (compositor-friendly), so switching feels smooth rather than abrupt. The slides here use gradient backgrounds with emoji, but each is a normal element you can swap for an image or video.
Pair this with a carousel for swipeable galleries, an onboarding tour for guided flows, or an image accordion for expandable media.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA phone-style story viewer appears with four segmented bars on top; the first fills automatically over five seconds.
- 2Watch it auto-advanceWhen the first bar completes, the viewer crossfades to the next slide and that bar starts filling — looping back to the start after the last.
- 3Tap to navigateTap the right side to skip ahead and the left side to go back, just like Instagram stories.
- 4Hold to pausePress and hold anywhere — the active bar freezes mid-fill and resumes the moment you release.
- 5Change the durationSet the
--durcustom property (e.g. to 3s or 8s) to control how long each slide stays before advancing. - 6Swap in real mediaReplace each
.st-slide's gradient background with an image or a muted autoplay video for real stories.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Replace each .st-slide's gradient with background-image: url(...) or put an <img>/<video muted autoplay> inside it. For video, sync the bar duration to the clip length by setting that slide's --dur to the video duration, and call next() on the video's ended event instead of (or in addition to) the bar animation.
Track touch start/move on the viewer: a downward swipe past a threshold closes the story; horizontal swipes past the edges move to the previous/next author's story set. Keep the tap zones for within-set navigation and reserve swipes for set-level and dismiss gestures, as the native apps do.
Tying progression to the bar's animationend guarantees the slide changes exactly when its progress bar completes, even after pausing — animation-play-state pauses both together. A separate setTimeout can drift from the visual bar and is awkward to pause/resume in sync, leading to a timer that finishes before or after the bar visibly fills.
Provide visible, focusable Previous/Next controls (the snippet uses real <button>s with aria-labels) so keyboard users can navigate, and support Left/Right arrow keys mapped to prev/next. Offer a pause control and respect prefers-reduced-motion by disabling auto-advance for users who set it, since auto-moving content can be an accessibility barrier.
In React, hold current in useState, render bars from the slide array, key the active fill so it remounts (auto-restarting the animation), and advance in an onAnimationEnd handler. In Vue, use a ref and :key on the active fill with @animationend. In Angular, track current and bind [class.active]/(animationend). The scaleX keyframe and pause CSS port unchanged.