Layouts
Layouts wrap pages with shared UI that persists across navigations. They never remount - use templates when you need fresh state.
Search across all documentation pages
Layouts wrap pages with shared UI that persists across navigations. They never remount - use templates when you need fresh state.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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
}<html> and <body> tags. Next.js will error if this file is missing.children only. The children prop is the current page or the next nested layout in the tree.app/layout.tsx wraps app/dashboard/layout.tsx which wraps app/dashboard/page.tsx. You never manually compose them."use client" only when the layout itself needs hooks or browser APIs.metadata object or generateMetadata function that merges with parent metadata.// 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>
);
}// 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 };
}children. Use React Context, cookies, or a shared data-fetch instead.<html> and <body>. Even as a Client Component, it must render those tags.layout.tsx > template.tsx > page.tsx. The template sits between.searchParams. Only page.tsx receives searchParams. If a layout needs query params, use useSearchParams() in a Client Component child.app/dashboard/layout.tsx, all dashboard pages lose that wrapper instantly.params are async in Next.js 15+. Always await params in layouts - the synchronous destructuring pattern is deprecated.| 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 |
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// 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>
);
}// 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:
ThemeProvider wraps everything because toast notifications and the store initializer both need theme context. ToastProvider comes before ProjectStoreInitializer because store initialization errors need to display toast messages.suppressHydrationWarning on the <html> tag is required when using theme providers that set a class or data-theme attribute on the server. Without it, the mismatch between server-rendered and client-rendered class triggers a React warning.ThemeProvider, ToastProvider, and ProjectStoreInitializer are initialized once and persist across all page transitions. Any state in these providers is preserved.metadata.title.template pattern ('%s | SystemsArchitect') enables child pages to set just their title (e.g., export const metadata = { title: 'Dashboard' }) and it automatically renders as "Dashboard | SystemsArchitect".getServerSideProps from the Pages Router.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.
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.
layout.tsx persists and does not remount on navigationtemplate.tsx remounts on every navigation, giving fresh statelayout.tsx > template.tsx > page.tsxNo. 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.
No. Only page.tsx receives searchParams. If a layout needs query params, use useSearchParams() in a Client Component child.
// 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>
);
}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".
It breaks. params is now a Promise in Next.js 15+. You must await params in layouts. The synchronous pattern is deprecated.
interface LayoutProps {
children: React.ReactNode;
params: Promise<Record<string, string>>;
}
interface TemplateProps {
children: React.ReactNode;
}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 };
}Yes, but it must still render <html> and <body> tags. Even as a Client Component, these tags are required.
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.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥