Creating Nested Layouts in Next.js
Nested layouts are one of the App Router's most powerful features for building scalable UIs. A layout wraps all pages in its folder and subfolders, persisting during navigation so shared UI like sidebars and headers don't unmount or lose state. You can nest layouts infinitely, each adding a new layer of structure and behavior. This article teaches you how to architect multi-level layouts that keep your code organized and your apps performant.
What Are Nested Layouts?
A nested layout is a layout.tsx file in a subfolder of your app. Each folder can have its own layout, which wraps all routes inside it. Layouts nest—a child layout wraps the content of a parent layout—creating a composition tree of shared UI components.
When you navigate from /dashboard to /dashboard/users, the root layout and dashboard layout persist; only the page component swaps. This is fundamentally different from traditional page-based routing where every navigation re-renders everything. Persistent layouts preserve scroll position, form state, and animations, creating a snappier, more app-like feel.
Visualizing Layout Nesting
Imagine a Dashboard app with this structure:
app/
├── layout.tsx # Global header, footer (all pages)
├── page.tsx # Home page
└── dashboard/
├── layout.tsx # Dashboard sidebar (all /dashboard/* routes)
├── page.tsx # /dashboard overview
├── users/
│ └── page.tsx # /dashboard/users list
├── settings/
│ └── page.tsx # /dashboard/settings
└── projects/
├── layout.tsx # Projects sub-section layout
├── page.tsx # /dashboard/projects list
└── [id]/
└── page.tsx # /dashboard/projects/:id detail
When you visit /dashboard/users, the rendering tree is:
RootLayout
└── DashboardLayout
└── UsersPage
When you navigate to /dashboard/projects/123, the tree becomes:
RootLayout
└── DashboardLayout
└── ProjectsLayout
└── ProjectDetailPage
The root and dashboard layouts persist; the projects layout and page swap in. No re-mounting, no state loss.
Building a Real-World Dashboard
Let's build a multi-level dashboard with a global header, a sidebar for dashboard routes, and a nested layout for a projects section.
Step 1: Create the Root Layout
The root layout wraps everything. Include your global header and footer, and set up any global providers (like theme providers or Redux stores).
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<title>My Dashboard</title>
</head>
<body className="bg-gray-50">
<header className="bg-white shadow">
<nav className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-2xl font-bold">MyApp</h1>
<div className="flex gap-4">
<a href="/" className="hover:underline">
Home
</a>
<a href="/dashboard" className="hover:underline">
Dashboard
</a>
</div>
</nav>
</header>
<main>{children}</main>
</body>
</html>
);
}
Step 2: Create the Dashboard Layout
The dashboard layout adds a sidebar. It wraps all routes under /dashboard/*.
// app/dashboard/layout.tsx
"use client";
import { useState } from "react";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div className="flex h-screen">
<aside
className={`${
sidebarOpen ? "w-64" : "w-20"
} bg-slate-800 text-white transition-all duration-300`}
>
<nav className="p-4 space-y-2">
<a
href="/dashboard"
className="block px-4 py-2 rounded hover:bg-slate-700"
>
Dashboard
</a>
<a
href="/dashboard/users"
className="block px-4 py-2 rounded hover:bg-slate-700"
>
Users
</a>
<a
href="/dashboard/projects"
className="block px-4 py-2 rounded hover:bg-slate-700"
>
Projects
</a>
<a
href="/dashboard/settings"
className="block px-4 py-2 rounded hover:bg-slate-700"
>
Settings
</a>
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="block w-full text-left px-4 py-2 rounded hover:bg-slate-700 mt-8"
>
{sidebarOpen ? "Collapse" : "Expand"}
</button>
</nav>
</aside>
<main className="flex-1 overflow-auto p-8">{children}</main>
</div>
);
}
This layout persists across all /dashboard/* routes. When you toggle the sidebar state (via useState), it persists even when you navigate to /dashboard/users or /dashboard/projects. The parent root layout (header and footer) also persists.
Step 3: Create Pages Inside Dashboard
Now add pages for each section:
// app/dashboard/page.tsx
export default function DashboardPage() {
return (
<div>
<h2 className="text-3xl font-bold mb-4">Dashboard Overview</h2>
<p className="text-gray-600">
Welcome. Select a section from the sidebar.
</p>
</div>
);
}
// app/dashboard/users/page.tsx
export default function UsersPage() {
return (
<div>
<h2 className="text-3xl font-bold mb-4">Users</h2>
<ul className="space-y-2">
<li className="p-2 border rounded">Alice Johnson</li>
<li className="p-2 border rounded">Bob Smith</li>
<li className="p-2 border rounded">Carol White</li>
</ul>
</div>
);
}
Step 4: Add a Third-Level Layout for Projects
For the projects section, add another nested layout that provides additional UI (like a breadcrumb or filter bar).
// app/dashboard/projects/layout.tsx
export default function ProjectsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<div className="mb-6 p-4 bg-blue-100 rounded">
<h3 className="font-semibold">Projects Section</h3>
<p className="text-sm text-gray-600">Manage and organize all projects</p>
</div>
{children}
</div>
);
}
// app/dashboard/projects/page.tsx
export default function ProjectsPage() {
return (
<div>
<h2 className="text-3xl font-bold mb-4">Projects</h2>
<div className="grid grid-cols-3 gap-4">
<div className="p-4 border rounded">
<h4 className="font-bold">Project Alpha</h4>
<p className="text-sm text-gray-600">Status: Active</p>
</div>
<div className="p-4 border rounded">
<h4 className="font-bold">Project Beta</h4>
<p className="text-sm text-gray-600">Status: Planning</p>
</div>
<div className="p-4 border rounded">
<h4 className="font-bold">Project Gamma</h4>
<p className="text-sm text-gray-600">Status: Complete</p>
</div>
</div>
</div>
);
}
Navigate from /dashboard/users to /dashboard/projects. You'll see the sidebar persist, but the main content swaps. The projects layout banner also appears—a third UI layer added by the nested layout.
Layout Isolation and Composition
Each layout can manage its own state, fetch data (via Server Components), and provide context to its children. Layouts don't re-mount on child navigation, so state persists. This is critical for apps with complex state.
Using Context with Layouts
You can use React Context to share data across a layout and its children without prop drilling:
// app/dashboard/context.tsx
"use client";
import React, { createContext, useState } from "react";
export const DashboardContext = createContext<{
userId: string | null;
setUserId: (id: string) => void;
} | null>(null);
export function DashboardProvider({
children,
}: {
children: React.ReactNode;
}) {
const [userId, setUserId] = useState<string | null>(null);
return (
<DashboardContext.Provider value={{ userId, setUserId }}>
{children}
</DashboardContext.Provider>
);
}
// app/dashboard/layout.tsx
import { DashboardProvider } from "./context";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<DashboardProvider>
<div className="flex h-screen">
{/* sidebar */}
<main className="flex-1">{children}</main>
</div>
</DashboardProvider>
);
}
Now all child pages can access userId via the context, and it persists across navigation.
Key Takeaways
- Nested layouts wrap child routes and persist during navigation, keeping shared UI (sidebars, headers) mounted and state intact.
- Each folder can have its own
layout.tsx, creating a composition tree where child layouts wrap parent layouts. - Layouts don't re-mount, preserving form state, scroll position, and animation progress—ideal for interactive dashboards.
- Context and state in layouts propagate to all child pages without re-rendering the layout on navigation.
- Layout nesting is infinitely composable; architect your UI as a tree of concerns (global, section, subsection).
Frequently Asked Questions
Do layouts re-render when I navigate?
No. Layouts persist during navigation within their scope. The layout component itself doesn't re-render; only the children prop swaps. This is why state in useState persists and DOM elements (like iframes) don't reset. If you want re-render behavior, use template.tsx instead.
What if I have state in a layout and navigate away from its routes?
The layout unmounts when you navigate outside its scope. For example, if /dashboard/layout.tsx has state and you navigate to /, the layout unmounts and state is lost. If you re-enter /dashboard, the layout re-mounts with fresh state. Use external state management (Redux, Zustand) if state needs to survive layout unmounting.
Can I have multiple layouts at the same level?
Not in a single folder. Each folder can have one layout.tsx. However, you can use route groups (folders wrapped in parentheses) to have multiple layout trees at the same level. See the route groups article for this pattern.
How deep can I nest layouts?
Arbitrarily deep. A common pattern is: root layout → section layout → subsection layout → page. Performance is excellent because only pages render per navigation; layouts are lightweight containers.
Should I use a layout or a component wrapper?
Use a layout if the UI should persist across navigation within a folder. Use a component if the UI is local to a single page or re-renders on navigation. Layouts are more efficient and provide better perceived performance.
Further Reading
- Next.js Layouts Documentation – official guide to layout composition.
- React Context API – sharing data across layouts and pages without prop drilling.
- Server vs Client Components in Layouts – best practices for layout rendering modes.