How to Create a Global Custom 404 Page for Route Groups in Next.js

React
ui-image How to Create a Global Custom 404 Page for Route Groups in Next.js
Image generated by ChatGPT (DALL·E)

Implementing a custom 404 page in Next.js versions <=16 can be surprisingly tricky when using route groups. By default, Next.js expects a not-found.jsx file to be placed next to the layout of a route. But what happens when you have multiple route groups with different layouts? Let’s dive in.

Suppose we have two route groups, each with its own layout and not-found.jsx:

app
├── (admin)
│   ├── admin
│   ├── layout.jsx
│   └── not-found.jsx
├── (main)
│   ├── foo
│   ├── page.jsx
│   ├── not-found.jsx
│   └── layout.jsx
└── favicon.ico

If a user requests a page that exists within a route group (e.g., /foo/404), Next.js will render the custom 404 page for that group—exactly as expected.

However, if the user requests a page that doesn’t match any group (e.g., /404), the app falls back to Next.js’ default 404 page, which may not match your app’s styling.

You might try placing a not-found.jsx in the root (/src/app). Next.js will throw a build error:

Build error: not-found.jsx doesn’t have a root layout. To fix this error, make sure every page has a root layout.

You could attempt a minimal “mock” root layout:

// src/app/layout.jsx
export default function RootLayout({ children }) {
  return children;
}

But this triggers a runtime error:

Runtime Error: Missing and tags in the root layout. Read more at https://nextjs.org/docs/messages/missing-root-layout-tags

I tried many variations without success—until I discovered a workaround from this post: https://x.com/huozhi/status/1921690791694446919.

In your next.config.mjs, enable the experimental globalNotFound option:

// next.config.mjs
export default {
  experimental: {  
    globalNotFound: true
  }
};

Then, remove any root layout.jsx or not-found.jsx and create a global-not-found.jsx instead:

app
├── (admin)
│   ├── admin
│   ├── layout.jsx
│   └── not-found.jsx
├── (main)
│   ├── foo
│   ├── page.jsx
│   ├── not-found.jsx
│   └── layout.jsx
├── favicon.ico
└── global-not-found.jsx

Here’s a fully rendered server-side example for global-not-found.jsx:

export default function GlobalNotFound() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="robots" content="noindex" />
        <title>404 - Page Not Found</title>
        <style dangerouslySetInnerHTML={{ __html: `
          body {
            margin: 0;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: Arial, sans-serif;
            background: #f0f0f0;
            color: #333;
            text-align: center;
          }
          h1 { font-size: 6rem; margin: 0; color: #ff4d4f; }
          p { font-size: 1.25rem; margin: 0.5rem 0 1.5rem; }
          a {
            display: inline-block;
            padding: 0.5rem 1rem;
            text-decoration: none;
            color: white;
            background: #1890ff;
            border-radius: 5px;
            transition: background 0.2s;
          }
          a:hover { background: #40a9ff; }
        `}} />
      </head>
      <body>
        <div>
          <h1>404</h1>
          <p>Page not found</p>
          <a href="/">Go to homepage</a>
        </div>
      </body>
    </html>
  );
}

This approach is fully server-side and framework-agnostic, but you cannot include client components like headers or footers from your route groups.

To include a 404 page from a specific route group, you can create a standard page inside a group, for example: src/app/(main)/404/page.jsx. Then, import it in global-not-found.jsx:

import NotFoundPage from './(main)/404/page';
import MainLayout from './(main)/layout'; 

export default function GlobalNotFound() {
  return (
    <MainLayout>
      <NotFoundPage />
    </MainLayout>
  );
}

This approach allows your global 404 page to reuse layouts and components from your route groups, keeping the design consistent across the app.

Conclusion

Creating a global 404 page in Next.js 16 with route groups requires a few extra steps due to the framework’s layout constraints. By enabling the globalNotFound experimental feature and combining it with route group-specific pages, you can build a fully consistent, reusable 404 page that matches your app’s design.

This method ensures that both unmatched routes and group-specific routes render properly styled 404 pages, giving your users a seamless experience across your app.