Story Progress Bars — Instagram Stories HTML CSS JS

Story Progress Bars · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Transform-based fill
Bars fill via transform: scaleX with a left origin, so progress animates on the compositor and stays smooth, not via layout-bound width.
animationend auto-advance
The viewer advances when the active fill animation ends, tying slide changes to the visible bar so the timer and indicator never desync.
Clean animation restart
show resets the fill with the animation:none + reflow + restore trick, so re-entering a slide always replays its progress from zero.
Tap-side navigation
Transparent left/right hit zones call prev/next, matching the native story gesture without extra buttons.
Hold-to-pause
Press-and-hold toggles animation-play-state: paused on the active fill, freezing both the bar and the countdown until release.
Configurable duration
A single --dur custom property controls each slide's length, so changing pacing is one value, not code edits.
Crossfading slides
Slides stack absolutely and transition opacity, a compositor-friendly crossfade rather than an abrupt swap.
Done/active/upcoming states
Completed bars are full, the active bar animates, and upcoming bars sit empty — the instantly-readable story progress model.

About this UI Snippet

Story Progress Bars — Auto-Advancing Segments, Tap Navigation & Hold-to-Pause

Screenshot of the Story Progress Bars snippet rendered live

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

  1. 1
    Paste HTML, CSS, and JSA phone-style story viewer appears with four segmented bars on top; the first fills automatically over five seconds.
  2. 2
    Watch 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.
  3. 3
    Tap to navigateTap the right side to skip ahead and the left side to go back, just like Instagram stories.
  4. 4
    Hold to pausePress and hold anywhere — the active bar freezes mid-fill and resumes the moment you release.
  5. 5
    Change the durationSet the --dur custom property (e.g. to 3s or 8s) to control how long each slide stays before advancing.
  6. 6
    Swap 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

Social story viewers
The core use — Instagram/Snapchat-style story playback. Combine with a carousel for a feed of multiple authors' stories.
Product tours and onboarding
Walk new users through features as auto-advancing slides; pair with an onboarding tour for in-app spotlights.
Marketing and campaign stories
Embed a tappable story unit on a landing page to showcase a launch, with hold-to-pause for readers who want longer.
Tutorial and how-to sequences
Step-by-step visual guides where each segment is a step; users can tap back to review or hold to study a frame.
Event and recap highlights
Replay a sequence of moments from an event as a story, swapping the gradient slides for photos or short clips.
Mobile-first showcases
Any vertical, tap-through media experience; combine with an image accordion elsewhere on the page for expandable galleries.

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.