Dashboards, modals and multiple root layouts need routing patterns beyond simple folders.
Route groups: a folder in parentheses, like (marketing), organises routes without affecting the URL. app/(marketing)/about/page.tsx is /about. Groups let different sections have different layouts, and even multiple root layouts (each group with its own layout.tsx containing <html>).
Private folders: _lib or _components are excluded from routing entirely.
Parallel routes render several pages in the same layout simultaneously. Named slots are folders starting with @: app/dashboard/@team/page.tsx and @analytics/page.tsx are passed to app/dashboard/layout.tsx as team and analytics props next to children. Each slot has its own loading and error states and can be shown conditionally (e.g. based on the user's role). Add default.tsx to a slot as the fallback when the slot has no match for the current URL after a full page load; otherwise you get a 404.
Intercepting routes load a route inside the current layout during client navigation, while a direct visit shows the full page. The convention mirrors relative paths: (.)photo intercepts a sibling segment, (..) one level up, (...) from the root. Combined with a parallel @modal slot, this creates a URL-backed modal: clicking a photo opens a modal with a shareable URL; refreshing shows the photo page; back closes it.
`template.tsx` is like a layout but remounts on every navigation, for enter animations or per-page effects.
Example
// app/
// ├── (marketing)/about/page.tsx -> /about (group not in URL)
// ├── layout.tsx receives children + modal slot
// ├── @modal/default.tsx -> return null when no modal
// ├── @modal/(.)photos/[id]/page.tsx intercepts /photos/[id] on client navigation
// └── photos/[id]/page.tsx full page on direct visit or refresh
// app/layout.tsx
export default function RootLayout({ children, modal }: LayoutProps<'/'>) {
return (
<html lang="en">
<body>{children}{modal}</body> {/* modal = the @modal parallel slot */}
</html>
)
}
// app/@modal/(.)photos/[id]/page.tsx
export default async function PhotoModal({ params }: PageProps<'/photos/[id]'>) {
const { id } = await params
return <dialog open>Photo {id} (press back to close)</dialog>
}
// app/@modal/default.tsx
// export default function Default() { return null }Remember
(group)folders organise routes and layouts without changing URLs@slotfolders are parallel routes passed to the layout as props; adddefault.tsx(.),(..),(...)intercept routes for URL-backed modals
Watch out for
- Forgetting
default.tsxin a parallel slot, so a hard refresh of another URL renders a 404.