Layouts
Layouts wrap pages with shared UI that persists across navigations. They never remount — use templates when you need fresh state.
Recipe
Quick-reference recipe card — copy-paste ready.
// app/layout.tsx — Root layout (required, exactly one)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<header>Site Header</header>
<main>{children}</main>
<footer>Site Footer</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx — Nested layout for /dashboard/*
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<nav className="w-64">Sidebar</nav>
<section className="flex-1">{children}</section>
</div>
);
}When to reach for this: Whenever multiple pages share the same wrapper UI — navigation bars, sidebars, providers, or structural containers.
Working Example
// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: { default: "My App", template: "%s | My App" },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<nav className="border-b px-6 py-3">
<a href="/">Home</a>
<a href="/dashboard" className="ml-4">Dashboard</a>
</nav>
{children}
</body>
</html>
);
}// app/dashboard/layout.tsx
import Link from "next/link";
const sidebarLinks = [
{ href: "/dashboard", label: "Overview" },
{ href: "/dashboard/analytics", label: "Analytics" },
{ href: "/dashboard/settings", label: "Settings" },
];
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<aside className="w-64 border-r p-4">
<h2 className="mb-4 font-bold">Dashboard</h2>
<ul className="space-y-2">
{sidebarLinks.map((link) => (
<li key={link.href}>
<Link href={link.href} className="text-blue-600 hover:underline">
{link.label}
</Link>
</li>
))}
</ul>
</aside>
<main className="flex-1 p-6">{children}</main>
</div>
);
}// app/dashboard/page.tsx
export default function DashboardPage() {
return <h1>Dashboard Overview</h1>;
}
// app/dashboard/analytics/page.tsx
export default function AnalyticsPage() {
return <h1>Analytics</h1>;
// Navigating here keeps the sidebar mounted — no remount, no state loss
}Deep Dive
How It Works
- Root layout is mandatory. It must contain
<html>and<body>tags. Next.js will error if this file is missing. - Layouts receive
childrenonly. Thechildrenprop is the current page or the next nested layout in the tree. - Layouts do not remount on navigation. React reconciles them — component state, effects, and DOM persist. This is why sidebars and nav bars stay interactive.
- Layouts nest automatically.
app/layout.tsxwrapsapp/dashboard/layout.tsxwhich wrapsapp/dashboard/page.tsx. You never manually compose them. - Layouts are Server Components by default. Add
"use client"only when the layout itself needs hooks or browser APIs. - Metadata can be defined per layout. Each layout (or page) can export a
metadataobject orgenerateMetadatafunction that merges with parent metadata.
Variations
// Template — remounts on every navigation (useful for enter/exit animations)
// app/dashboard/template.tsx
"use client";
import { motion } from "framer-motion";
export default function DashboardTemplate({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}// Layout with params — accessing route params in a layout
// app/blog/[slug]/layout.tsx
export default async function BlogPostLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<article>
<div className="text-sm text-gray-500">Post: {slug}</div>
{children}
</article>
);
}// Conditional layout — different UI based on auth status
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session) redirect("/login");
return (
<div>
<p>Welcome, {session.user.name}</p>
{children}
</div>
);
}TypeScript Notes
// Layout props type
interface LayoutProps {
children: React.ReactNode;
params: Promise<Record<string, string>>; // async in Next.js 15+
}
// Template has the same props as layout
interface TemplateProps {
children: React.ReactNode;
}
// Metadata types
import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
return { title: slug };
}Gotchas
- You cannot pass data from layout to children via props. Layouts only receive
children. Use React Context, cookies, or a shared data-fetch instead. - Root layout cannot be a Client Component without
<html>and<body>. Even as a Client Component, it must render those tags. - Templates and layouts can coexist. The render order is
layout.tsx>template.tsx>page.tsx. The template sits between. - Layouts cannot access
searchParams. Onlypage.tsxreceivessearchParams. If a layout needs query params, useuseSearchParams()in a Client Component child. - Removing a layout affects all child routes. If you delete
app/dashboard/layout.tsx, all dashboard pages lose that wrapper instantly. paramsare async in Next.js 15+. Alwaysawait paramsin layouts — the synchronous destructuring pattern is deprecated.
Alternatives
| Approach | When to Use |
|---|---|
template.tsx | Need component remount on each navigation (animations, logging) |
| Route Groups with separate layouts | Different layouts for different sections at the same URL depth |
| Client-side context provider | Sharing state across pages without a visual wrapper |
Parallel Routes (@slot) | Rendering multiple pages side-by-side in one layout |
Real-World Example
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
Root Layout with Provider Nesting
// Production example: Root layout with providers and metadata
// File: app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/theme-provider';
import { ToastProvider } from '@/components/toast-provider';
import { ProjectStoreInitializer } from '@/components/project-store-initializer';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: { default: 'SystemsArchitect', template: '%s | SystemsArchitect' },
description: 'Cloud architecture learning platform',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ToastProvider>
<ProjectStoreInitializer>
{children}
</ProjectStoreInitializer>
</ToastProvider>
</ThemeProvider>
</body>
</html>
);
}Async Home Page
// Production example: Home page with server data
// File: app/page.tsx
import { getFeaturedContent } from '@/lib/content';
export default async function HomePage() {
const featured = await getFeaturedContent();
return (
<main>
<h1>Welcome to SystemsArchitect</h1>
{featured.map((item) => (
<article key={item.id}>{item.title}</article>
))}
</main>
);
}What this demonstrates in production:
- Provider nesting order matters.
ThemeProviderwraps everything because toast notifications and the store initializer both need theme context.ToastProvidercomes beforeProjectStoreInitializerbecause store initialization errors need to display toast messages. suppressHydrationWarningon the<html>tag is required when using theme providers that set aclassordata-themeattribute on the server. Without it, the mismatch between server-rendered and client-rendered class triggers a React warning.- Layouts never re-mount on navigation. This means the
ThemeProvider,ToastProvider, andProjectStoreInitializerare initialized once and persist across all page transitions. Any state in these providers is preserved. - The
metadata.title.templatepattern ('%s | SystemsArchitect') enables child pages to set just their title (e.g.,export const metadata = { title: 'Dashboard' }) and it automatically renders as "Dashboard | SystemsArchitect". - The home page is an async Server Component. It fetches data at request time with zero client JavaScript. This pattern replaces
getServerSidePropsfrom the Pages Router.
FAQs
Why is the root layout required?
The root app/layout.tsx must exist and must contain <html> and <body> tags. Next.js errors if this file is missing because it defines the HTML document structure for the entire app.
Do layouts remount when navigating between child routes?
No. Layouts persist across navigations. React reconciles them so component state, effects, and DOM are preserved. This is why sidebars and nav bars stay interactive.
What is the difference between layout.tsx and template.tsx?
layout.tsxpersists and does not remount on navigationtemplate.tsxremounts on every navigation, giving fresh state- Render order is:
layout.tsx>template.tsx>page.tsx - They can coexist in the same segment
Gotcha: Can you pass data from a layout to its children via props?
No. Layouts only receive children as a prop. Use React Context, cookies, or a shared data-fetch to share data between a layout and its child pages.
Can layouts access searchParams?
No. Only page.tsx receives searchParams. If a layout needs query params, use useSearchParams() in a Client Component child.
How do you access route params in a layout?
// app/blog/[slug]/layout.tsx
export default async function BlogLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<article>
<p>Post: {slug}</p>
{children}
</article>
);
}How does the metadata title template work?
Set title: { default: "My App", template: "%s | My App" } in the root layout. Child pages export metadata = { title: "Dashboard" } and it renders as "Dashboard | My App".
Gotcha: What happens if params are destructured synchronously in Next.js 15+?
It breaks. params is now a Promise in Next.js 15+. You must await params in layouts. The synchronous pattern is deprecated.
What are the TypeScript types for layout and template props?
interface LayoutProps {
children: React.ReactNode;
params: Promise<Record<string, string>>;
}
interface TemplateProps {
children: React.ReactNode;
}How do you type generateMetadata in a layout?
import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
return { title: slug };
}Can the root layout be a Client Component?
Yes, but it must still render <html> and <body> tags. Even as a Client Component, these tags are required.
How do layouts nest automatically?
app/layout.tsx wraps app/dashboard/layout.tsx which wraps app/dashboard/page.tsx. You never manually compose them. Next.js handles the nesting based on the file-system hierarchy.
Related
- App Router Basics — file conventions overview
- Route Groups — multiple layouts at the same level
- Parallel Routes — @slot segments in layouts
- Loading & Error — loading and error boundaries within layouts