You Might Also Like
Page Transition Progress Bar — Route-Change Loading Bar in HTML CSS JS
Page Transition Progress Bar · Loaders · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Page Transition Progress Bar — Tied to a Simulated Navigation Lifecycle

This is the thin bar that slides across the top of the page during a client-side route change — the same family as a top loading bar, but built around a different mechanic: instead of a continuous rAF-eased trickle toward 90%, this snippet drives the bar through three discrete, explicit phases of one simulated navigation — climb, hold, complete — using plain CSS width transitions with different easing per phase, triggered by clicking a route link in the demo.
How this differs from a continuous trickle bar
A trickle-style bar (see the top loading bar snippet) uses a setInterval to keep nudging a target value upward and a requestAnimationFrame loop to ease the displayed width toward it continuously, so the width is recalculated every frame for as long as the load takes. This snippet takes a simpler, phase-based approach on purpose: a single navigate() call sets the width straight to 82% with one CSS transition, waits a fixed hold period (standing in for "request in flight"), then transitions straight to 100% with a second, different easing curve. There is no per-frame JavaScript loop at all — every motion is a CSS width transition, and the "curve" comes from choosing three different states and three different transition timings rather than continuously recomputing a target.
Three explicit phases, not one continuous animation
navigate(path) resets the bar to 0% (with the transition disabled and a forced reflow so the reset isn't animated), then transitions to 82% quickly with a snappy cubic-bezier, holds unchanged for 900ms while nothing animates (simulating the actual request being in flight), then transitions the remaining distance to 100% with a softer ease-out — visibly two different speeds for the "climb" and "complete" segments, which is what a realistic route change looks like: fast optimistic progress, a pause for the real work, then a quick finish.
Route demo, not a generic trigger
Clicking one of the three route buttons calls navigate() with that path and, once the bar completes and fades, updates a "Now at" label — tying the whole animation explicitly to a simulated navigation event rather than a standalone play button, closer to how you'd wire this to real router lifecycle events (beforeEach/afterEach, routeChangeStart/routeChangeComplete).
Wiring it to a real router
Call navigate(path) — or split it into a start()/finish() pair if your router's start and complete events fire far apart — from your router's navigation hooks. Pair it with a skeleton loader for the incoming page's content placeholder, or an indeterminate bar for in-page async work that isn't a full navigation.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Ask an AI coding assistant like Claude to walk through why this bar's navigate() function uses three separate CSS width transitions with different durations and easing curves instead of one continuous requestAnimationFrame-driven trickle like a typical NProgress-style bar, and why the forced reflow (void bar.offsetWidth) before starting the climb transition matters for getting a clean reset on repeated navigations. It's also worth asking how you'd adapt the fixed 900ms hold into a hold that lasts until a real navigation promise resolves, rather than a fixed timeout — which is the main thing standing between this demo and a production router integration. For extending it, ask for a version that shows a different color or duration profile for slow versus fast navigations, or that falls back to an indeterminate hold if the real request takes far longer than the demo's fixed timing assumes. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a top-of-page navigation progress bar in plain HTML, CSS, and JavaScript that is explicitly tied to a simulated route-change lifecycle with three distinct phases — no requestAnimationFrame loop, no continuous trickle interval.
Requirements:
- A thin, fixed-position bar pinned to the very top of the viewport with a high z-index, hidden (zero opacity) until a navigation is triggered.
- A demo UI with at least two clickable "route" links/buttons that each trigger a navigate(path) function representing a simulated client-side route change.
- Phase 1 (climb): on navigate(), reset the bar's width to 0% without animating the reset (disable the transition, force a layout reflow, then re-enable the transition), then transition the width up to roughly 80-85% using a relatively fast, snappy easing curve.
- Phase 2 (hold): after reaching roughly 80-85%, the bar must hold at that width completely unchanged for a realistic pause (several hundred milliseconds to around a second), representing the point where a real request is in flight and no more optimistic progress can be faked.
- Phase 3 (complete and fade): after the hold, transition the width the remaining distance to 100% using a different, softer easing curve than the climb phase, then fade the bar out via opacity, and only after the fade finishes reset its width back to 0% (with the transition disabled again) so it's ready for the next navigation.
- After the bar completes and fades, update a piece of demo UI (such as a "current route" label) to reflect the path that was navigated to, so the whole sequence reads as a real navigation completing, not just a bar animating in isolation.
- Structure the code so it is clear where you would call the climb-and-hold portion from a router's "navigation started" event and the complete portion from its "navigation finished" event in a real app.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA mock app window with three route buttons renders below a hidden top bar.
- 2Click a route buttonThe bar appears and climbs quickly to 82% with a snappy ease.
- 3Watch it holdThe bar pauses at 82% for ~900ms — the simulated in-flight request.
- 4Watch it completeIt eases the rest of the way to 100%, then fades out and resets.
- 5See the route updateOnce the bar fades, the "current route" label updates to the clicked path.
- 6Wire to a real routerCall navigate() from your router's navigation start hook with the target path.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The top loading bar uses a continuous requestAnimationFrame loop plus a setInterval "trickle" that keeps nudging a moving target toward 90% for as long as the load takes, recalculating width every frame. This snippet instead defines three fixed phases — climb to 82%, hold for a set duration, complete to 100% — each driven by a single CSS width transition with its own easing curve, and there is no per-frame JavaScript at all. It trades the trickle bar's open-ended "keep faking progress" behavior for a fixed, three-beat curve tied to one simulated navigation.
The hold represents the request actually being in flight — the point where the app has no more information to fake progress with and is genuinely waiting. Freezing the bar there for a realistic pause (900ms in the demo) rather than continuing to inch upward makes the eventual jump to 100% read as a real completion event rather than an animation simply running out.
Call navigate(path) when your router's navigation starts (e.g. Vue Router's beforeEach, Next.js's routeChangeStart, React Router's navigation start). If your router exposes separate start and complete events, split navigate() into two functions — one that climbs to 82% and holds indefinitely, and one that completes to 100% and fades — and call the second when the real navigation actually resolves instead of using a fixed timeout.
Because this bar's motion is a fixed, known sequence of states (0% to 82% to 100%) rather than an open-ended approach toward an uncertain endpoint, a CSS transition per phase is simpler and needs no per-frame JavaScript. The trickle bar's rAF loop exists specifically because its target keeps moving unpredictably; this bar's targets are fixed values, so CSS transitions are sufficient and lighter-weight.
Wrap navigate(path) in a small hook or service and call it from your router's navigation guards — a useEffect tied to route changes in React, a beforeEach/afterEach pair in Vue Router, or a Router event subscription in Angular. Keep the bar as a single fixed element outside your route-specific components so it persists across navigations.