Silent Route Transitions Are Breaking Your User Experience: Build a Custom Next.js Progress Bar Component

Silent Route Transitions Are Breaking Your User Experience: Build a Custom Next.js Progress Bar Component

By Reggi, 12 Jan 2023

When a client transitions between routes in a Next.js application, the browser skips the standard hard refresh. It only swaps the relevant content chunks. If your architecture relies heavily on static site generation via getStaticProps or server-side pre-rendering via getServerSideProps, this seamless transition works flawlessly on a pristine connection. But the moment network latency creeps in or a connection hiccups, the interface enters a silent, frozen state. The user clicks a link, nothing moves, and the UI provides zero visual feedback.

To solve this state ambiguity, you need an active visual indicator that communicates progress immediately upon user interaction. While ready-made packages like nextjs-progressbar exist, building an in-house, fully configurable component gives you absolute control over styling, shallow route observation, custom delays, and lifecycle cleanup.

Why Visual Progress Indicators Matter

A progress bar gives the user an immediate visual representation of task completion. It helps them understand how much work has completed and how much remains, allowing them to gauge the approximate time left for an operation to finish. When transitions rely on asynchronous data retrieval or background bundle fetching, an immediate top-level indicator prevents users from repeatedly clicking elements or abandoning the page.

The Architectural Blueprint: Custom NextNProgress

We construct the component by wiring nprogress directly into the Next.js Router.events lifecycle. The component manages route changes, supports shallow route filtering, applies dynamic CSS with optional Content Security Policy (CSP) nonces, and handles cleanup when the component unmounts.

Here is the complete implementation for components/progressbar.tsx:

ts
import Router from 'next/router'; import * as NProgress from 'nprogress'; import * as PropTypes from 'prop-types'; import * as React from 'react'; export interface NextNProgressProps { /** * The color of the bar. * @default "#29D" */ color?: string; /** * The start position of the bar. * @default 0.3 */ startPosition?: number; /** * The stop delay in milliseconds. * @default 200 */ stopDelayMs?: number; /** * The height of the bar. * @default 3 */ height?: number; /** * Whether to show the bar on shallow routes. * @default true */ showOnShallow?: boolean; /** * The other NProgress configuration options to pass to NProgress. * @default null */ options?: Partial<NProgress.NProgressOptions>; /** * The nonce attribute to use for the `style` tag. * @default undefined */ nonce?: string; /** * Use your custom CSS tag instead of the default one. * This is useful if you want to use a different style or minify the CSS. * @default (css) => <style nonce={nonce}>{css}</style> */ transformCSS?: (css: string) => JSX.Element; } const NextNProgress = ({ color = '#003865 linear-gradient(71.18deg, rgb(0, 34, 255)-27.32%, rgb(0, 34, 255)-16.39%, rgb(81, 121, 254)-7.38%, rgb(165, 237, 182) 30.59%, rgb(250, 232, 90) 46.06%, rgb(253, 172, 62) 62.61%, rgb(255, 92, 0) 75.82%);', startPosition = 0.3, stopDelayMs = 200, height = 3, showOnShallow = true, options, nonce, transformCSS = (css) => <style nonce={nonce}>{css}</style>, }: NextNProgressProps) => { let timer: NodeJS.Timeout | null = null; React.useEffect(() => { if (options) { NProgress.configure(options); } Router.events.on('routeChangeStart', routeChangeStart); Router.events.on('routeChangeComplete', routeChangeEnd); Router.events.on('routeChangeError', routeChangeError); return () => { Router.events.off('routeChangeStart', routeChangeStart); Router.events.off('routeChangeComplete', routeChangeEnd); Router.events.off('routeChangeError', routeChangeError); }; }, []); const routeChangeStart = ( _: string, { shallow, }: { shallow: boolean; } ) => { if (!shallow || showOnShallow) { NProgress.set(startPosition); NProgress.start(); } }; const routeChangeEnd = ( _: string, { shallow, }: { shallow: boolean; } ) => { if (!shallow || showOnShallow) { if (timer) clearTimeout(timer); timer = setTimeout(() => { NProgress.done(true); }, stopDelayMs); } }; const routeChangeError = ( _err: Error, _url: string, { shallow, }: { shallow: boolean; } ) => { if (!shallow || showOnShallow) { if (timer) clearTimeout(timer); timer = setTimeout(() => { NProgress.done(true); }, stopDelayMs); } }; return transformCSS(`#nprogress{pointer-events:none}#nprogress .bar{background:${color};position:fixed;z-index:9999;top:0;left:0;width:100%;height:${height}px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px ${color},0 0 5px ${color};opacity:1;-webkit-transform:rotate(3deg) translate(0,-4px);-ms-transform:rotate(3deg) translate(0,-4px);transform:rotate(3deg) translate(0,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:${color};border-left-color:${color};border-radius:50%;-webkit-animation:nprogresss-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}`); }; NextNProgress.propTypes = { color: PropTypes.string, startPosition: PropTypes.number, stopDelayMs: PropTypes.number, height: PropTypes.number, showOnShallow: PropTypes.bool, options: PropTypes.object, nonce: PropTypes.string, transformCSS: PropTypes.func, }; export default React.memo(NextNProgress);

Component Breakdown and Configuration Options

The NextNProgressProps interface gives fine-grained control over both the visual presentation and the execution logic during routing events.

PropertyTypeDefault ValueDescription
colorstringMulti-stop gradientSets the CSS background property for the loading bar and peg shadows.
startPositionnumber0.3The initial percentage value where the progress bar starts running.
stopDelayMsnumber200Delay in milliseconds before executing NProgress.done() to prevent flickers.
heightnumber3Height of the progress bar in pixels.
showOnShallowbooleantrueDetermines whether the progress indicator triggers on shallow route updates.
optionsPartial<NProgress.NProgressOptions>undefinedCustom configuration payload passed directly into NProgress.configure().
noncestringundefinedNonce attribute applied to the generated <style> tag for strict CSP environments.
transformCSS(css: string) => JSX.ElementInjected <style> tagFunction allowing custom CSS transformations, minification, or external injection.

Integration into the Application Layout

Once the component is built, integrate it at the layout level so it persists across page navigations. In your layout structure (such as pages/layout/index.tsx), mount the component high in the tree.

js
import Header from "../../components/header" import Footer from "../../components/footer" //import nextprogresss compoonent import NextNProgress from '../../components/nprogress'; const Layout = ({ categories, children }: Props) => { return ( <> <div className="relative h-screen bg-gradient-to-b-white dark:bg-gradient-to-b lg:h-[140vh]"> <NextNProgress options={{ showSpinner: false }} /> <Header categories={categories} /> <main>{children}</main> <Footer /> </div> </> ) } export default Layout;

With NextNProgress placed at the top of your layout container, navigating to any route immediately triggers the top bar. The component binds to Router.events, starts the animation at 0.3, watches for completion or errors, and clears timers gracefully on unmount. This small architectural addition eliminates dead air between client transitions and delivers unambiguous feedback on every route change.


Popular Reads