Vercel Logo

Nested Layouts

Your dashboard has a sidebar. Every page under /dashboard/* needs it. You copy-paste the sidebar into each page. Then the design changes. Now you're updating 12 files.

Nested layouts solve this: define the sidebar once, and every child route inherits it automatically. They persist across route changes and compose automatically.

VIDEO PLACEHOLDER · 3-4 min · EVERGREEN · Medium Priority

Layout Composition: How Nested Layouts Stack

Animated visualization of how layouts compose - root layout wraps section layout wraps page. Shows what persists vs re-renders during navigation, and why this matters for state preservation and performance.

Outcome

A nested layout for a section that overrides root layout chrome.

Fast Track

  1. Add a section layout.tsx in a nested folder.
  2. Move relevant chrome to the section layout.
  3. Verify composition and slotting (how children are placed within layout wrappers).

Hands-On Exercise 2.4

Build nested layouts that compose persistent UI across route segments.

Requirements:

  1. Create a nested layout under a route group or segment.
  2. Show different header/footer than root.
  3. Keep child routing intact.
  4. Demonstrate persistence across route changes within the section.

Implementation hints:

  • layout.tsx in each segment: Creates nested composition automatically.
  • Layouts persist: Don't re-render when navigating between child routes.
  • Automatic deduplication: Next.js ensures layouts only render once per navigation.
  • Children prop pattern: Layout receives children and wraps it with section-specific UI.
  • Layouts with params: If your layout needs params (rare but possible), remember params are async Promises. Layout must be async and await params.
  • Avoid duplicating providers if not needed.
  • Keep the layout lean; minimize data fetching here.
Layout Persistence

Layouts persist across route changes within their segment. State is maintained when navigating between child pages, improving performance and user experience.

Try It

  • Navigate within the section; verify persistent chrome.

Commit & Deploy

git add -A
git commit -m "feat(core): add nested layout for section chrome"
git push -u origin feat/core-nested-layout

Done-When

  • Navigate from /dashboard to /dashboard/analytics: sidebar remains visible and does not flash/reload
  • Navigate from /dashboard/analytics to /dashboard/settings: sidebar remains visible (verify by watching for any flicker)
  • Navigate from / to /about: marketing header/footer persists without reload
  • Compare / (marketing) vs /dashboard: different chrome visible (header/footer vs sidebar)
  • Visit /about (not /marketing/about): route group does not add segment to URL
  • Open DevTools Network tab, navigate between dashboard pages: layout.tsx JavaScript does not re-fetch

Solution

Solution

File Structure

apps/web/src/app/
├── layout.tsx                 # Root layout (html, body, global providers)
├── (marketing)/
│   ├── layout.tsx             # Marketing layout (header, footer, nav)
│   ├── page.tsx               # Home page
│   ├── about/page.tsx
│   └── pricing/page.tsx
└── dashboard/
    ├── layout.tsx             # Dashboard layout (sidebar, different chrome)
    ├── page.tsx               # Dashboard overview
    ├── analytics/page.tsx
    └── settings/page.tsx

How Layout Composition Works

┌─────────────────────────────────────────────────────────────┐
│  Root Layout (layout.tsx)                                   │
│  ├── <html>, <body>, global providers                       │
│  │                                                          │
│  │  ┌─────────────────────────────────────────────────────┐│
│  │  │  Marketing Layout ((marketing)/layout.tsx)          ││
│  │  │  ├── Header with nav                                ││
│  │  │  ├── {children} ← Page content renders here         ││
│  │  │  └── Footer                                         ││
│  │  └─────────────────────────────────────────────────────┘│
│  │                                                          │
│  │  ┌─────────────────────────────────────────────────────┐│
│  │  │  Dashboard Layout (dashboard/layout.tsx)            ││
│  │  │  ├── Sidebar (persists across routes)               ││
│  │  │  └── {children} ← Page content renders here         ││
│  │  └─────────────────────────────────────────────────────┘│
│  │                                                          │
└─────────────────────────────────────────────────────────────┘

Root Layout

apps/web/src/app/layout.tsx
// Root layout: wraps ALL pages, provides html/body structure
// Keep this minimal: no section-specific chrome here
 
import type { Metadata } from "next";
import "./globals.css";
 
export const metadata: Metadata = {
  title: "Next.js Foundations",
  description: "Learning Next.js patterns",
};
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="min-h-screen bg-white text-gray-900">
        {/* Global providers would go here (theme, auth, etc.) */}
        {children}
      </body>
    </html>
  );
}

Marketing Layout (Route Group)

apps/web/src/app/(marketing)/layout.tsx
// Marketing section layout: header, footer, nav
// (marketing) is a route group - doesn't affect URL structure
// Routes: /, /about, /pricing (NOT /marketing/about)
 
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="min-h-screen">
      {/* Marketing header - persists across all marketing pages */}
      <header className="mb-8 border-b py-4">
        <nav className="mx-auto flex max-w-4xl gap-4 px-4">
          <a href="/" className="font-semibold">
            Home
          </a>
          <a href="/about" className="text-gray-600 hover:text-gray-900">
            About
          </a>
          <a href="/pricing" className="text-gray-600 hover:text-gray-900">
            Pricing
          </a>
          {/* Link to dashboard (different layout) */}
          <a
            href="/dashboard"
            className="ml-auto text-blue-600 hover:text-blue-800"
          >
            Dashboard →
          </a>
        </nav>
      </header>
 
      {/* Page content renders here */}
      <main className="mx-auto max-w-4xl px-4">{children}</main>
 
      {/* Marketing footer - persists across all marketing pages */}
      <footer className="mx-auto mt-8 max-w-4xl border-t px-4 py-4 text-gray-500 text-sm">
        © 2026 Next.js Foundations
      </footer>
    </div>
  );
}

Dashboard Layout (Nested Segment)

apps/web/src/app/dashboard/layout.tsx
// Dashboard layout: sidebar navigation, different chrome than marketing
// This layout PERSISTS when navigating between /dashboard/* routes
// The sidebar doesn't re-render - only the main content area changes
 
import Link from "next/link";
 
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen">
      {/* Sidebar - persists across all dashboard routes */}
      <aside className="w-64 border-r bg-gray-50 p-4">
        <h2 className="mb-4 font-bold text-lg">Dashboard</h2>
        <nav className="space-y-2">
          <Link
            href="/dashboard"
            className="block rounded px-3 py-2 text-gray-700 hover:bg-gray-200"
          >
            Overview
          </Link>
          <Link
            href="/dashboard/analytics"
            className="block rounded px-3 py-2 text-gray-700 hover:bg-gray-200"
          >
            Analytics
          </Link>
          <Link
            href="/dashboard/settings"
            className="block rounded px-3 py-2 text-gray-700 hover:bg-gray-200"
          >
            Settings
          </Link>
        </nav>
        <div className="mt-8 border-t pt-4">
          {/* Link back to marketing section */}
          <Link
            href="/"
            className="text-gray-500 text-sm hover:text-gray-700"
          >
            ← Back to site
          </Link>
        </div>
        <div className="mt-4">
          <p className="text-gray-400 text-xs">
            This sidebar persists when navigating between dashboard pages.
          </p>
        </div>
      </aside>
 
      {/* Main content area - children change on navigation */}
      <main className="flex-1 p-8">{children}</main>
    </div>
  );
}

Dashboard Pages

apps/web/src/app/dashboard/page.tsx
// Dashboard overview page
// When navigating here from /dashboard/analytics, only this component re-renders
// The sidebar layout persists (no flicker, state preserved)
 
export default function DashboardPage() {
  return (
    <div>
      <h1 className="mb-4 font-bold text-3xl">Dashboard Overview</h1>
      <p className="mb-6 text-gray-600">
        Welcome to your dashboard. Navigate between pages using the sidebar.
        Notice how the sidebar persists across route changes.
      </p>
 
      <div className="grid grid-cols-3 gap-4">
        <div className="rounded-lg border bg-blue-50 p-4">
          <h3 className="font-semibold text-blue-800">Users</h3>
          <p className="font-bold text-2xl text-blue-600">1,234</p>
        </div>
        <div className="rounded-lg border bg-green-50 p-4">
          <h3 className="font-semibold text-green-800">Revenue</h3>
          <p className="font-bold text-2xl text-green-600">$45,678</p>
        </div>
        <div className="rounded-lg border bg-purple-50 p-4">
          <h3 className="font-semibold text-purple-800">Orders</h3>
          <p className="font-bold text-2xl text-purple-600">567</p>
        </div>
      </div>
    </div>
  );
}
apps/web/src/app/dashboard/analytics/page.tsx
// Analytics page - demonstrates layout persistence
// Try adding console.log in the layout to verify it doesn't re-run
 
export default function AnalyticsPage() {
  return (
    <div>
      <h1 className="mb-4 font-bold text-3xl">Analytics</h1>
      <p className="mb-6 text-gray-600">
        This is the analytics page. The sidebar layout persisted during
        navigation.
      </p>
 
      <div className="rounded-lg border p-6">
        <h2 className="mb-4 font-semibold text-xl">Traffic Overview</h2>
        <div className="flex h-64 items-center justify-center rounded bg-gray-100">
          <span className="text-gray-400">[Chart placeholder]</span>
        </div>
      </div>
    </div>
  );
}
apps/web/src/app/dashboard/settings/page.tsx
// Settings page - form state would persist across sibling navigations
// If you had a form here with unsaved changes, navigating to /analytics
// and back would preserve those changes (because the layout persists)
 
export default function SettingsPage() {
  return (
    <div>
      <h1 className="mb-4 font-bold text-3xl">Settings</h1>
      <p className="mb-6 text-gray-600">
        Configure your dashboard settings. The sidebar persists during
        navigation.
      </p>
 
      <div className="space-y-6">
        <div className="rounded-lg border p-4">
          <h2 className="mb-2 font-semibold">Notifications</h2>
          <label className="flex items-center gap-2">
            <input type="checkbox" className="rounded" />
            <span>Email notifications</span>
          </label>
        </div>
 
        <div className="rounded-lg border p-4">
          <h2 className="mb-2 font-semibold">Theme</h2>
          <select className="rounded border px-3 py-2">
            <option>Light</option>
            <option>Dark</option>
            <option>System</option>
          </select>
        </div>
      </div>
    </div>
  );
}

Testing Layout Persistence

  1. Navigate Dashboard: Click between Overview → Analytics → Settings
  2. Watch Network Tab: Layout CSS/JS doesn't reload between dashboard pages
  3. Add State Test: Add a counter to the sidebar, verify it persists across navigation
  4. Compare Layouts: Visit / (marketing) vs /dashboard to see different chrome
  5. Route Group: Confirm /about works (NOT /marketing/about)

Key Patterns

  • Route Groups: (marketing) folder organizes files without affecting URLs
  • Segment Layouts: dashboard/layout.tsx applies to all /dashboard/* routes
  • Persistence: Layouts don't re-render on child navigation (state preserved)
  • No Provider Duplication: Put providers in root layout, not every nested layout

References