Next.js Route Navigation Progress Bar: Enhancing User Experience with BProgress
This tutorial is also available as a video on YouTube 👉 【Next.js】Route Navigation Progress Bar
Hello, I'm Kaypen.
Let's start with a bad example.
In Dify.ai, when you click to navigate to another page, there's a waiting period before the page actually transitions.
However, during this waiting time, I don't know whether the navigation was successful or not, so I end up clicking multiple times until the page finally changes.
This is a poor user experience 👎
The solution is simple. Let's look at how GitHub handles navigation interactions.
As you can see, GitHub displays a progress bar during navigation, clearly telling users - "I'm navigating, please wait."
So how can we implement this effect in Next.js?
We can achieve this using the BProgress library.
BProgress is a lightweight progress bar component library that supports Next.js 15+, as well as other frameworks like Remix and Vue.
For using BProgress, I've created a demo project nextjs-progress-bar-demo. Let's clone this project first:
git clone git@github.com:wukaipeng-dev/nextjs-progress-bar-demo.git
Then enter the project directory:
cd nextjs-progress-bar-demo
First, install the dependencies:
npm install @bprogress/next
Start the project:
npm run dev
As you can see, this is a simple Next.js project with three pages: Home, Login, and Register.
The main
branch already has the progress bar configured. Let's switch to the without-progress-bar-demo
branch:
git checkout without-progress-bar-demo
In this branch, we haven't configured the progress bar, so no progress bar will be displayed during page navigation.
Next, let's import ProgressProvider
in the root layout app/layout.tsx
:
'use client';
import "./globals.css";
import { ProgressProvider } from '@bprogress/next/app';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<ProgressProvider
height="4px"
color="#4c3aed"
options={{ showSpinner: false }}
shallowRouting
>
{children}
</ProgressProvider>
</body>
</html>
);
}
Now, we can see that when navigating between the home page and login page, or between login and register pages, a progress bar will be displayed.
The ProgressProvider
parameters are:
height
: The height of the progress barcolor
: The color of the progress baroptions
: Progress bar configuration. HereshowSpinner
is set tofalse
, meaning no animated loading icon will be displayed.shallowRouting
: Whether to enable shallow routing. If enabled, when only the route's query parameters change (e.g.,?page=1
to?page=2
), the progress bar won't reload.
However, after successful login, clicking to navigate won't show the progress bar.
This is because navigation between the home page and login page, or between login and register pages, uses the <Link>
component.
The <Link>
component actually renders as an <a>
tag, and BProgress adds click events to all <a>
components to show the progress bar.
We can check in DevTools → Elements → <a>
→ Event Listeners whether a click event has been added:
But after successful login, we use router.push
for navigation.
BProgress doesn't add click events to router.push
, so naturally it won't show a progress bar.
Don't worry, BProgress provides us with a useRouter
method.
Replace Next.js's useRouter
with the useRouter
provided by BProgress:
// import { useRouter } from 'next/navigation';
import { useRouter } from '@bprogress/next/app';
Then use it as normal:
const router = useRouter();
router.push('/');
Now you can see that after successful login, when automatically navigating to the home page, the progress bar displays correctly.
But if your project has already wrapped its own useRouter
, you can pass the wrapped useRouter
as a parameter customRouter
for a second wrapping:
import { useRouter } from '@bprogress/next/app';
import { useRouter as useNextIntlRouter } from '@/i18n/navigation';
export default function Home() {
const router = useRouter({
customRouter: useNextIntlRouter,
});
return (
<button
onClick={() =>
router.push('/about', {
startPosition: 0.3,
locale: 'en',
})
}
>
Go to about page
</button>
);
}
Finally, let's go back to app/layout.tsx
, where we imported ProgressProvider
but turned app/layout
into a client component. Let's extract ProgressProvider
elsewhere and keep app/layout
as a server component.
// app/components/ProgressWrapper.tsx
'use client';
import { ProgressProvider } from '@bprogress/next/app';
interface ProgressWrapperProps {
children: React.ReactNode;
}
export function ProgressWrapper({ children }: ProgressWrapperProps) {
return (
<ProgressProvider
height="4px"
color="#0000ff"
options={{ showSpinner: false }}
shallowRouting
>
{children}
</ProgressProvider>
);
}
In app/layout.tsx
, we import ProgressWrapper
:
import { ProgressWrapper } from './components/ProgressWrapper';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<ProgressWrapper>
{children}
</ProgressWrapper>
</body>
</html>
);
}
Great job! You've completed the integration of a route navigation progress bar in Next.js.
That's all for this tutorial. I hope you found it helpful.
Thanks for reading! 👏