How to Create a Global Custom 404 Page for Route Groups in Next.js
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.