Route transitions significantly impact the user experience of your web application. Smooth, informative transitions keep your users engaged and informed. Today, we'll explore a practical aspect of improving this UX in Next.js applications: building a route change loading overlay using next/router events.

Keep in mind, this tutorial uses the next/router package, not the newer next/navigation. That being said, this will only work with the old (still in heavy use) pages router, not the new app router. A tutorial on loaders/routing events for the app router is forthcoming.

There is also a GitHub repo and demo companion site for this tutorial. Links to both are that the bottom of this article. So, without further ado, let's get coding!

Setting Up Environment

The quickest way to get up and running would be to clone my repo. But if you want to start from scratch, just spin up a new Next.js project using create-next-app@latest with the following settings:

Typescript? Yes
ESLint? Yes
Tailwind CSS? Yes
src/ directory? Yes
App Router? No
import alias? Yes
leave default import alias

This will install all our dependencies. You shouldn't need to install anything else.

Our Loader Component

Now create a folder src/components and in it, a file called RouteLoader.tsx:

import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
const Loader = () => (
    <div className="fixed top-0 left-0 w-screen h-screen z-[99999999999999] flex items-center justify-center bg-black/40">
        <div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-white"></div>
export default function RouteLoader() {
    const router = useRouter();
    const [loading, setLoading] = useState<boolean>(false);
    useEffect(() => {
        const handleStart = (url: string) => setLoading(true);
        const handleComplete = (url: string) => setLoading(false);'routeChangeStart', handleStart);'routeChangeComplete', handleComplete);'routeChangeError', handleComplete);
        return () => {
  'routeChangeStart', handleStart);
  'routeChangeComplete', handleComplete);
  'routeChangeError', handleComplete);
    }, [router]);
    return loading ? <Loader /> : null;

Should be pretty self-explanatory here. We bind the routeChangeStart event to handleStart, and routeChangeComplete and routeChangeError to handleComplete. We also want to remove these listeners when the component is unmounted, so we return a function that unbinds the events. Basically, what will happen is as soon as a route change is detected, loading state will be set to true. This will show our full-screen loading overlay. Once the page is done loading, the handleComplete event will fire (on success or error) and loading will be set to false.

Finally, in our _app.tsx file, we'll modify it to import our loader component:

import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import RouteLoader from '@/components/RouteLoader'
import Header from '@/components/Header'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function App({ Component, pageProps }: AppProps) {
  const classes = [inter.className, 'min-h-screen'];
  return <main className={classes.join(' ')}>
    <RouteLoader />
    <Header />
    <div className="flex flex-col">
      <Component {...pageProps} />

That's it! This is a very basic example. You could create a much fancier loader using an animated SVG or GIF, but it's generally good practice to make your loader very lightweight. Otherwise you'd need a loader for your loader! 🤣

GitHub Repo

Demo Site

Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the Designly Blog. Also, please leave your comments! I love to hear thoughts from my readers.

I use Hostinger to host my clients' websites. You can get a business account that can host 100 websites at a price of $3.99/mo, which you can lock in for up to 48 months! It's the best deal in town. Services include PHP hosting (with extensions), MySQL, Wordpress and Email services.

Looking for a web developer? I'm available for hire! To inquire, please fill out a contact form.