Top Loading Bar — Route Progress HTML CSS JS Snippet
Top Loading Bar · Loaders · Plain HTML, CSS & JS · Live preview
What's included
Features
startBar eases the target up by a shrinking step every 400ms, capped at 90%, faking confident progress for an unknown duration.finishBar sets the target to 100%; once eased past 99.5% the bar fades via opacity and resets for the next run.requestAnimationFrame loop eases displayed width toward the target each frame, so the bar glides between trickle steps.::after with layered box-shadows trails the leading edge, the signature NProgress light effect.position: fixed; top: 0 with a high z-index keeps the bar above all content during loads.simulate runs a full start-to-finish cycle; startBar/finishBar let you drive the phases independently.About this UI Snippet
Top Loading Bar — Trickle-to-90%, Complete-and-Fade & Glowing Peg

The thin progress bar that slides across the top of the page during navigation — popularised by YouTube, GitHub, and the NProgress library — has become the standard signal that "something is loading". It works precisely because the duration of a page load or fetch is unknown: instead of a fake percentage, it trickles quickly toward 90%, holds, and then snaps to 100% and fades once the real work finishes. This snippet implements that behaviour in plain HTML, CSS, and vanilla JavaScript.
The trickle model
A real loader can't know how long a request will take, so this bar fakes confident progress. startBar sets the target to 8% and starts a setInterval "trickle" that nudges the target upward by a shrinking amount every 400ms but never past 90% (target + (0.9 − target) × 0.2). This easing means it races early and slows as it approaches 90%, conveying activity without ever pretending to be done. When the actual work completes, finishBar clears the trickle and sets the target to 100%.
Smooth JS-driven width
A requestAnimationFrame loop eases the *displayed* width toward the target each frame (shown += (target − shown) × 0.14), so the bar glides rather than jumping between trickle steps. The width is set directly in JS rather than via a CSS width transition — deliberately, because utility frameworks like Tailwind don't animate raw width, so a JS tween guarantees identical smoothness across the React and Tailwind exports. Only the final fade uses an opacity transition (which frameworks do animate).
Complete and fade
When finishing and the bar has eased past 99.5%, the loop fades it out via opacity, then resets width to zero after the fade so the next navigation starts clean. The bar is position: fixed at the very top with a high z-index, and a glowing "peg" (::after with layered box-shadows) trails the leading edge — the signature NProgress detail that makes the bar feel like light moving across the screen.
Demo controls
A mock browser window with "Navigate (auto)", "Start", and "Finish" buttons lets you trigger the full cycle or drive the phases manually. simulate runs a realistic start-then-finish after 2.2 seconds.
In a real app you call startBar() when a route change or fetch begins and finishBar() when it resolves (router events, fetch interceptors, or a global request counter). Pair this with a skeleton loader for content placeholders, a progress bar for determinate tasks, or a download button for in-button progress.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Ask an AI coding assistant like Claude to trace through the trickle formula in startBar() — target + (0.9 - target) * 0.2 + 0.01 — with a few sample target values to see exactly why it races early and slows to a crawl approaching 90%, and why that specific shape is what makes an unknown-duration load feel confident rather than fake. It's worth asking about the architecture choice too: why width is tweened in JS via requestAnimationFrame instead of a CSS width transition, and what would actually break in the Tailwind export if it weren't. For extending it, ask for a version that reports real byte progress when it's available (falling back to the trickle only when it isn't), a color that shifts as the bar approaches completion, or multiple independent bars for concurrent requests that merge into one visible bar. 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 YouTube/GitHub-style top-of-page loading bar in plain HTML, CSS, and JavaScript that fakes confident progress for a request of unknown duration — no library, no CSS width transition.
Requirements:
- A thin, fixed-position bar pinned to the very top of the viewport with a high z-index, whose width is set directly via JavaScript on every frame, not via a CSS transition on the width property.
- A start function that immediately jumps the bar's target to a small initial value (such as 8%), then begins a repeating interval that nudges the target upward by a shrinking amount each tick — using a formula based on the remaining distance to 90% — so the target approaches but never reaches 90% on its own.
- A requestAnimationFrame loop that, every frame, eases the actually-displayed width a fraction of the way toward the current target (not snapping straight to it), so the bar visually glides between each trickle step rather than jumping.
- A finish function that stops the trickle interval and sets the target to 100%, letting the same easing loop carry the displayed width the rest of the way.
- Once the eased width crosses very close to 100% (such as above 99.5%), the loop must switch the bar to a CSS opacity fade-out, and only after that fade completes reset the width back to zero so the next run starts clean.
- Add a glowing highlight at the bar's leading edge using layered box-shadows on a pseudo-element, and provide simple manual "Start" and "Finish" trigger buttons plus a combined "simulate a full navigation" button for testing the whole cycle.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 browser window with controls appears, and a thin gradient bar sits ready at the very top of the page.
- 2Click "Navigate (auto)"The top bar slides in, trickles quickly toward 90% and holds, then after ~2.2s jumps to 100% and fades — a full load cycle.
- 3Watch the trickleNotice it races early and slows near 90%, never completing until the real work finishes — confident progress for an unknown duration.
- 4Drive it manuallyClick "Start" to begin trickling and "Finish" to complete and fade, mimicking a route change resolving.
- 5See the glowing pegA soft glow trails the bar's leading edge as it moves — the signature NProgress light effect.
- 6Wire it to your appCall
startBar()on route-change/fetch start andfinishBar()on resolve; the bar handles the rest.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Call startBar() when navigation/fetch begins and finishBar() when it resolves. For routers, use their start/complete events (e.g. Next.js routeChangeStart/routeChangeComplete, Vue Router beforeEach/afterEach). For fetch, wrap your client to increment a counter on request and decrement on response, calling startBar when the counter goes 0→1 and finishBar when it returns to 0.
Most navigations and fetches don't expose a reliable progress value, so a real percentage isn't available. Trickling toward 90% communicates "actively working" without lying about completion, then snapping to 100% on the real finish gives a satisfying, honest end. For downloads that DO report bytes, use a determinate bar like the download button instead.
The width changes in unpredictable steps (each trickle tick), so a fixed-duration CSS transition would stutter. A requestAnimationFrame ease toward the moving target stays smooth. It's also export-safe: Tailwind's transition utility doesn't animate raw width, so a CSS-transition bar would snap in the React + Tailwind build — the JS tween behaves identically everywhere.
Treat them as a finish: call finishBar() in your router's error/abort handler and your fetch's catch/finally so the bar always completes and fades rather than hanging at 90%. If you maintain a request counter, ensure failed requests still decrement it so the count can reach zero.
Keep the bar as a single fixed element and drive it from a small module exposing start/finish (the functions here). In React, call them from a router effect; in Vue from router guards in onMounted; in Angular from Router events in a root component. The trickle and rAF logic are framework-agnostic and port unchanged — or wrap them in a tiny store/service.