The Problem
Pushed a dashboard build to staging on Monday and the QA team came back with the same bug from three different pages: clicking a sidebar link 404s, but a hard refresh on the same URL renders fine. The 404 was not from the route itself — the page existed, the data fetched, the layout rendered. Next.js was throwing a 404 from a parallel route slot we had set up for the modal stack.
The console gave the actual hint:
Error: No default component found for slot @modal at /dashboard/settings.
A default.tsx is required for parallel routes that do not match every URL.
This was a Next.js 15 codebase that worked fine until the upgrade to 16. Same @modal slot, same routes, same conventions. Something about how 16 resolves unmatched parallel route slots changed, and any soft navigation that lands on a URL that does not have a matching segment in the slot now blows up with a 404 instead of falling through.
If you are seeing intermittent 404s after upgrading to Next.js 16 only on routes that use @slot directories, this is the one.
Why It Happens
Parallel routes in App Router let you render multiple pages in the same layout simultaneously. A common pattern is @modal for a modal stack, @analytics for a side panel, or @nav for a context-aware navigation block:
app/
├── layout.tsx
├── page.tsx
├── @modal/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx
├── settings/
│ └── page.tsx
In Next.js 15, an unmatched slot during a soft (client-side) navigation would silently render null if no default.tsx existed. The runtime would log a warning but the page would still load. On a hard navigation (full page request), the 404 was correct because the server could not resolve every slot.
Next.js 16 unified the two paths. The router now treats soft and hard navigations identically: every parallel slot must resolve to either a matching segment or an explicit default.tsx, on every URL it can be rendered at. If a slot is unmatched and no default exists, the entire route returns 404 — even if the main page.tsx would render fine on its own.
The reason for the change is sane: it removes a class of subtle bugs where a soft nav leaves stale slot content from the previous URL because nothing tells the slot to clear. But the migration is painful because most apps only have default.tsx in the slot's root, not nested under every segment that the slot can be rendered alongside.
The Fix
Step 1: Add default.tsx at the slot root. This catches every URL that does not match any of the slot's nested segments:
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}
If your @modal slot is meant to render nothing when there is no modal, return null. Do not return an empty fragment — Next.js treats <></> as a renderable subtree and will hydrate it, which costs you a few ms per nav.
Step 2: Add default.tsx at every level the slot crosses. This is the gotcha. If your slot directory looks like this:
app/
├── @modal/
│ ├── default.tsx
│ ├── (.)photos/
│ │ └── [id]/
│ │ └── page.tsx
│ └── login/
│ └── page.tsx
And you navigate to /dashboard/photos/123, the router resolves the intercepted route (.)photos/[id]. But if you then navigate to /dashboard/photos/123/comments (a deeper URL), the slot has no matching segment past [id], so you also need a default.tsx inside the dynamic segment:
// app/@modal/(.)photos/[id]/default.tsx
export default function PhotoModalDefault() {
return null
}
The rule: anywhere a page.tsx exists inside a slot, you also need a sibling default.tsx to handle the case where the route resolves but the slot does not.
Step 3: Use the slot in layout.tsx correctly. I have seen this break on real codebases because the layout assumes the slot is always present. In Next.js 16, slot props are always passed but can be null:
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
{modal}
</body>
</html>
)
}
Render {modal} directly. Do not wrap it in a conditional like {modal && <Suspense>{modal}</Suspense>}. The default.tsx returning null is what makes this safe. If you wrap conditionally, React loses the slot's place in the tree and remounts every modal mid-flight, which kills the open-close animation.
Step 4: Verify with the App Router debug overlay. Next.js 16 ships an updated route boundaries inspector. Press Ctrl+Shift+P in dev mode and pick "Show parallel route boundaries." Slots without a matching segment or default.tsx are highlighted in red. Walk every interactive nav path and watch for red — those are your remaining gaps. The official parallel routes docs cover the edge cases for intercepting and route groups.
The Lesson
The Next.js 16 change is not a bug, it is a tightening. Soft and hard navigations now share the same slot resolution, which makes parallel routes more predictable but requires a default.tsx everywhere a slot can be rendered. Add default.tsx at the slot root, add it inside every segment that contains a page.tsx, and render the slot unconditionally in your layout.
If a Next.js 16 upgrade has surfaced 404s in your App Router build and you would rather hand the migration off than chase every nested slot yourself, that is what I do — see my services, or check the Next.js async params TypeError fix for another 16 migration trap on dynamic routes.