nprogress, react-loader-spinner, and react-top-loading-bar are all npm packages designed to provide visual feedback during asynchronous operations in web applications, but they serve different UI patterns and integration models. nprogress is a lightweight, framework-agnostic library that renders a slim progress bar at the top of the page, commonly used to indicate page navigation or data loading. react-loader-spinner offers a collection of animated spinner components built specifically for React, suitable for inline or modal loading states. react-top-loading-bar is a React-specific implementation of a top-of-page progress indicator, providing a declarative API and integration with React's component lifecycle.
When users wait for content, a well-designed loading indicator reduces perceived latency and improves UX. The three libraries — nprogress, react-loader-spinner, and react-top-loading-bar — all address this need but with fundamentally different approaches: global top bar, inline spinners, and React-native top bar, respectively. Let’s compare them in real engineering contexts.
nprogress renders a thin, colored bar fixed at the very top of the viewport (position: fixed; top: 0). It mimics the native browser loading bar seen in Chrome.
// nprogress: Imperative global control
import NProgress from 'nprogress';
NProgress.start();
fetch('/api/data').then(() => NProgress.done());
react-loader-spinner provides reusable React components that render inline wherever placed in the JSX tree — inside a button, a card, or a full-screen overlay.
// react-loader-spinner: Declarative inline spinner
import { Oval } from 'react-loader-spinner';
function LoadingCard({ isLoading }) {
return (
<div>
{isLoading ? <Oval height={40} width={40} /> : <Content />}
</div>
);
}
react-top-loading-bar also renders a top-of-page bar like nprogress, but as a React component that lives in your app’s JSX hierarchy.
// react-top-loading-bar: Component-based top bar
import TopLoadingBar from 'react-top-loading-bar';
import { useRef } from 'react';
function App() {
const ref = useRef(null);
const startLoading = () => ref.current?.continuousStart();
const finishLoading = () => ref.current?.complete();
return (
<>
<TopLoadingBar color="#29D" ref={ref} />
<MainApp startLoading={startLoading} finishLoading={finishLoading} />
</>
);
}
nprogress is imperative and global. You call NProgress.start() and NProgress.done() directly. This works outside React but creates tight coupling to side effects.
// Common in route guards
router.beforeEach((to, from, next) => {
NProgress.start();
next();
});
router.afterEach(() => NProgress.done());
react-loader-spinner is purely declarative and local. You conditionally render the spinner based on React state. No global state or side effects.
function SubmitButton({ isSubmitting }) {
return (
<button disabled={isSubmitting}>
{isSubmitting ? <TailSpin height="20" width="20" /> : 'Submit'}
</button>
);
}
react-top-loading-bar blends both: it’s declarative in structure (you render the component once) but imperative in control (you call methods via a ref). This gives you the visual style of nprogress with React lifecycle safety.
// Safe for SSR — no direct DOM access in render
useEffect(() => {
loadingRef.current?.continuousStart();
fetchData().finally(() => loadingRef.current?.complete());
}, []);
nprogress manipulates the DOM directly on import. This breaks in SSR environments like Next.js unless you guard usage with typeof window !== 'undefined'.
// Risky in SSR
if (typeof window !== 'undefined') {
NProgress.start();
}
react-loader-spinner is fully compatible with SSR because it renders standard React elements. No DOM access during render.
// Safe everywhere
<Oval ariaLabel="loading-indicator" />
react-top-loading-bar avoids DOM access during initial render and uses refs for runtime control, making it SSR-safe by design.
// Renders empty div on server, activates on client
<TopLoadingBar color="#f00" ref={ref} />
nprogress allows limited customization via CSS overrides or a config object:
NProgress.configure({
minimum: 0.1,
showSpinner: false,
speed: 500
});
But you can’t change the core animation logic without forking.
react-loader-spinner offers multiple built-in spinner types (Oval, Circles, TailSpin, etc.), each accepting props like height, width, color, and ariaLabel. You can even pass custom SVG paths.
<Puff
visible={true}
height="80"
width="80"
color="#4fa94d"
ariaLabel="puff-loading"
/>
react-top-loading-bar supports color, height, and shadow via props, plus programmatic control over progress simulation (continuousStart, staticStart, complete).
<TopLoadingBar
color="#29D"
height={3}
shadow={true}
ref={ref}
/>
You’re using React Router and want a top bar during page loads.
react-top-loading-bar — integrates cleanly with React lifecycle and router hooks.nprogress — works but requires useEffect guards and isn’t React-idiomatic.react-loader-spinner — wrong visual pattern (inline, not top bar).A user clicks “Save” and you show a spinner inside the button.
react-loader-spinner — designed exactly for this.Each widget loads independently and shows its own loading state.
react-loader-spinner — per-widget spinners avoid visual noise from a single top bar.| Feature | nprogress | react-loader-spinner | react-top-loading-bar |
|---|---|---|---|
| Visual Style | Top bar | Inline spinners | Top bar |
| Framework | Vanilla JS | React-only | React-only |
| Control Model | Imperative (global) | Declarative (local) | Declarative + Ref imperative |
| SSR Safe | ❌ (requires guards) | ✅ | ✅ |
| Custom Animations | Limited (CSS only) | ✅ (multiple built-in types) | Limited (progress simulation) |
| Best For | Global nav loading | Localized loading states | React apps needing top bar |
nprogress.react-top-loading-bar.react-loader-spinner.Don’t mix top bars and inline spinners arbitrarily — pick one primary loading pattern per app to maintain visual consistency. And always ensure loading states are accessible (e.g., with aria-busy or aria-live regions), regardless of which library you choose.
Choose nprogress if you need a minimal, framework-agnostic top-loading bar that works across any JavaScript environment—not just React—and you prefer manual imperative control over its visibility. It’s ideal for simulating native browser-like loading behavior during route transitions or API calls without JSX overhead. However, it requires direct DOM manipulation and doesn’t integrate with React’s state model, which can complicate testing or SSR.
Choose react-loader-spinner when you need flexible, visually distinct inline loading indicators (like rotating circles, bars, or custom SVG animations) that fit within your UI layout rather than occupying the top edge of the viewport. It’s perfect for localized loading states—such as inside buttons, cards, or modals—and offers multiple animation types out of the box. Since it’s purely presentational and stateless, you’ll manage visibility via React state, making it predictable and testable.
Choose react-top-loading-bar if you’re building a React application and want a top-of-page progress bar with a declarative, component-based API that integrates cleanly with React’s rendering cycle. It supports programmatic control via refs and automatically handles mounting/unmounting, making it safer for SSR and easier to coordinate with React Router or data fetching hooks. It’s the best choice when you want the visual pattern of nprogress but with idiomatic React ergonomics.
Slim progress bars for Ajax'y applications. Inspired by Google, YouTube, and Medium.
Add nprogress.js and nprogress.css to your project.
<script src='nprogress.js'></script>
<link rel='stylesheet' href='nprogress.css'/>
NProgress is available via bower and npm and spm.
$ bower install --save nprogress
$ npm install --save nprogress
Simply call start() and done() to control the progress bar.
NProgress.start();
NProgress.done();
Using Turbolinks or similar? Ensure you're using Turbolinks 1.3.0+, and use this: (explained here)
$(document).on('page:fetch', function() { NProgress.start(); });
$(document).on('page:change', function() { NProgress.done(); });
$(document).on('page:restore', function() { NProgress.remove(); });
Add progress to your Ajax calls! Bind it to the jQuery ajaxStart and
ajaxStop events.
Make a fancy loading bar even without Turbolinks/Pjax! Bind it to
$(document).ready and $(window).load.
Percentages: To set a progress percentage, call .set(n), where n is a
number between 0..1.
NProgress.set(0.0); // Sorta same as .start()
NProgress.set(0.4);
NProgress.set(1.0); // Sorta same as .done()
Incrementing: To increment the progress bar, just use .inc(). This
increments it with a random amount. This will never get to 100%: use it for
every image load (or similar).
NProgress.inc();
If you want to increment by a specific value, you can pass that as a parameter:
NProgress.inc(0.2); // This will get the current status value and adds 0.2 until status is 0.994
Force-done: By passing true to done(), it will show the progress bar
even if it's not being shown. (The default behavior is that .done() will not
do anything if .start() isn't called)
NProgress.done(true);
Get the status value: To get the status value, use .status
minimumChanges the minimum percentage used upon starting. (default: 0.08)
NProgress.configure({ minimum: 0.1 });
templateYou can change the markup using template. To keep the progress
bar working, keep an element with role='bar' in there. See the default template
for reference.
NProgress.configure({
template: "<div class='....'>...</div>"
});
easing and speedAdjust animation settings using easing (a CSS easing string)
and speed (in ms). (default: ease and 200)
NProgress.configure({ easing: 'ease', speed: 500 });
trickleTurn off the automatic incrementing behavior by setting this to false. (default: true)
NProgress.configure({ trickle: false });
trickleRate and trickleSpeedYou can adjust the trickleRate (how much to increase per trickle) and trickleSpeed (how often to trickle, in ms).
NProgress.configure({ trickleRate: 0.02, trickleSpeed: 800 });
showSpinnerTurn off loading spinner by setting it to false. (default: true)
NProgress.configure({ showSpinner: false });
parentspecify this to change the parent container. (default: body)
NProgress.configure({ parent: '#container' });
Just edit nprogress.css to your liking. Tip: you probably only want to find
and replace occurrences of #29d.
The included CSS file is pretty minimal... in fact, feel free to scrap it and make your own!
Bugs and requests: submit them through the project's issues tracker.
Questions: ask them at StackOverflow with the tag nprogress.
Chat: join us at gitter.im.

NProgress © 2013-2014, Rico Sta. Cruz. Released under the MIT License.
Authored and maintained by Rico Sta. Cruz with help from contributors.
ricostacruz.com · GitHub @rstacruz · Twitter @rstacruz