Dynamic & Parallel Routes
[slug], catch-all, route groups, parallel and intercepting.
Static routes get you to /about. Real apps need /blog/[slug], /shop/[...path], and modals that open over a list without losing the list's scroll position. Next has primitives for all of that, and they all come from folder naming conventions. No regex. No router config. Just brackets.
[slug]: one dynamic segment
A folder named with square brackets becomes a dynamic segment. Whatever the user types in that position is passed to your page as a parameter.
app/
āāā blog/
āāā [slug]/
āāā page.tsx // matches /blog/anythingparams prop is a Promise and must be awaited. Next 16 enforces this strictly. Forget the await and TypeScript will yell; runtime will yell louder.type Props = {
params: Promise<{ slug: string }>;
};
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return <article>{post.title}</article>;
}Multiple dynamic segments
Stack them. Each [name] folder becomes a key on the params object.
app/users/[userId]/posts/[postId]/page.tsx
// URL: /users/42/posts/7
// params: { userId: "42", postId: "7" }[...slug]: catch-all routes
Three dots before the name and Next captures the rest of the path as an array. Use this when the depth is variable: docs sites, wikis, file browsers.
app/docs/[...slug]/page.tsx
// /docs/getting-started params: { slug: ["getting-started"] }
// /docs/api/auth/sign-in params: { slug: ["api","auth","sign-in"] }
// /docs 404 (catch-all needs at least one segment)type Props = { params: Promise<{ slug: string[] }> };
export default async function Docs({ params }: Props) {
const { slug } = await params;
const path = slug.join("/");
const doc = await getDoc(path);
return <article>{doc.content}</article>;
}[[...slug]]: optional catch-all
Two pairs of brackets and the segment becomes optional. The same page now matches the parent URL too. Handy when you want one file to handle both /docs and /docs/anything/deep.
app/docs/[[...slug]]/page.tsx
// /docs params: { slug: undefined }
// /docs/intro params: { slug: ["intro"] }
// /docs/api/v2/users params: { slug: ["api","v2","users"] }Route groups: (parens) for organization
A folder with parentheses around its name is a route group. It does not appear in the URL. You use them to group routes that share a layout or to keep your app/ directory tidy without polluting URLs.
app/
āāā (marketing)/
ā āāā layout.tsx // shared marketing layout
ā āāā page.tsx // /
ā āāā pricing/page.tsx // /pricing
āāā (app)/
ā āāā layout.tsx // shared app shell
ā āāā dashboard/page.tsx // /dashboard
ā āāā settings/page.tsx // /settingsTwo completely different layouts (marketing site vs logged-in app) sit at the same URL level. Without route groups you'd have to pick one root layout and branch inside it.
layout.tsx inside the group). When the user crosses between groups, the layout fully reloads. Use this when marketing pages and the app need different fonts, themes, or shells.Parallel routes: @slot for multiple panes
A folder prefixed with @ is a named slot. The parent layout can render multiple slots side by side, each with its own loading and error states. The classic use case is a dashboard with independent panels.
app/dashboard/
āāā layout.tsx
āāā @analytics/
ā āāā default.tsx
ā āāā page.tsx
āāā @team/
ā āāā default.tsx
ā āāā page.tsx
āāā page.tsx // the main slot, available as { children }export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">{children}</div>
<aside>
{analytics}
{team}
</aside>
</div>
);
}default.tsx. When you navigate to a URL that doesn't have a match for that slot, Next renders default.tsx. Without it, hard refreshes on a sub-route will 404.Intercepting routes: (.) for modals
Intercepting routes let one route "take over" another for a single navigation. The canonical example: clicking a photo in a feed opens it in a modal, but if a friend visits the URL directly, they see the full photo page. Same URL, two renders.
The convention uses parentheses with dots:
(.)match siblings at the same level(..)match one level up(..)(..)match two levels up(...)match from the root
app/
āāā feed/
ā āāā page.tsx // /feed (list of photos)
ā āāā @modal/
ā āāā default.tsx // null
ā āāā (..)photo/[id]/
ā āāā page.tsx // intercepts /photo/[id]
āāā photo/
āāā [id]/
āāā page.tsx // full page at /photo/[id]From /feed, clicking a photo link to /photo/123 triggers the intercepting route and renders the modal version in the @modal slot. Refreshing the page or sharing the URL bypasses the intercept and shows the full page.
Combining everything
A real e-commerce site might use all of these:
app/
āāā (storefront)/ // route group: shared layout
ā āāā layout.tsx
ā āāā page.tsx // /
ā āāā search/
ā ā āāā @modal/
ā ā ā āāā default.tsx
ā ā ā āāā (..)product/[slug]/
ā ā ā āāā page.tsx // quick-view modal
ā ā āāā page.tsx // /search
ā āāā product/[slug]/page.tsx // /product/anything
āāā (admin)/ // separate root for admins
āāā layout.tsx
āāā dashboard/[[...path]]/page.tsx // /dashboard or /dashboard/foo/barQuiz
What's the params type in a Next.js 16 dynamic route?
Recap
[slug]dynamic,[...slug]catch-all,[[...slug]]optional catch-all.paramsis now a Promise. Alwaysawait params.(group)organizes without affecting URLs.@slotcreates parallel routes; each slot needsdefault.tsx.(.)intercepts let one route render in place of another, perfect for modal overlays that still share URLs.