Skip to content
Chapter 17Lesson 2

The Next.js root layout

How the Next.js App Router builds the HTML document around your React app, and what belongs in the root layout.

Every component you’ve written so far returns a fragment of UI: a heading, a list, a button. Your homepage might be nothing more than this:

app/page.tsx
export default function Home() {
return <h1>Welcome</h1>;
}

Run pnpm dev, open the page, and choose “View Source”. Your <h1> isn’t sitting alone. It’s a complete HTML document:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Acme</title>
</head>
<body>
<h1>Welcome</h1>
<script src="/_next/static/chunks/main.js"></script>
</body>
</html>

Your <h1> sits near the bottom, inside a <body>, inside an <html lang="en">, under a <head> full of tags you never typed. So who writes the rest of the page?

In the Next.js App Router , the document around your components has exactly three authors:

  • One file, app/layout.tsx, writes the <html> and <body> tags. This is the shell.
  • The metadata API writes everything inside <head>: the <title>, the <meta> tags, the favicon link.
  • A small <Providers> component writes the client-side machinery inside <body>.

The shell is shared by every page, and machines you never see read it: a search crawler reads your <title>, a screen reader reads lang="en", the browser reads <meta charset> before it can decode a byte. Put the wrong thing in it and you break SEO, accessibility, or rendering for every page at once.

Every web page ever served, in any framework, has the same outer skeleton: a <!DOCTYPE html> declaration, then a single <html> element wrapping exactly two children, a <head> and a <body>.

<!DOCTYPE html> not an element — a parser instruction
<html lang="en">
<head> metadata — not rendered on the page
<meta charset="utf-8"> <title>Acme</title> <link rel="icon">
<body> everything visible your React tree mounts here

<head> holds metadata that machines read; <body> holds what people see. This lesson is about which file writes which box.

Four pieces carry the weight:

  • <!DOCTYPE html> is the standards-mode switch: one line telling the browser to render by modern web standards. Without it, browsers fall back to quirks mode . Next.js emits it for you.
  • <html lang="en"> is the root element, the single box everything else lives inside. Its lang attribute declares the document’s language, and three consumers read it: a screen reader picks the right pronunciation, the browser hyphenates, and translation tools know what they’re translating from. You always set it.
  • <head> holds metadata, information about the page that isn’t drawn on it. It renders nothing yet is the most-read part of your document, just not by humans. The <title> is the browser-tab text and the blue heading of your Google result; <meta name="description"> is the grey snippet under it; <meta charset> picks the encoding that turns raw bytes into text, and getting it wrong renders mojibake . The root layout is where you sign these contracts with the browser and every crawler that visits.
  • <body> holds everything visible. This is where your React tree mounts, and where your <h1> landed.

In the App Router, app/layout.tsx is the root layout : the outermost component, rendered once around every page. It’s required; Next.js refuses to build an App Router project without one.

A minimal correct root layout, top to bottom:

import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

No name on the left of this import: you’re pulling in globals.css for its side effect, adding the app’s single global stylesheet (the Tailwind entry point) to the build. It lives in the root layout so the styles reach every page.

import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

RootLayout is a default export. The course uses named exports almost everywhere; this is an exception, because the App Router finds this component by its default export.

import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

children is the forward reference from the last lesson, paying off here. Next.js injects your page into this slot by convention, so RootLayout never imports it. React.ReactNode covers anything React can render: JSX, a string, a number, an array of those, even null. Read children as “the page goes here.”

import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

<html lang="en"> renders the root element and sets the document language. No other file renders this tag.

import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

<body>{children}</body> drops the page into the body, where your entire visible app mounts. Top to bottom, this file is the document shell: it opens <html>, opens <body>, places the page, and closes both.

1 / 1

Three facts about this file carry the most weight.

It’s a Server Component. Notice what’s absent: no 'use client', no hooks, no window or localStorage. By default, every component in the App Router is a Server Component : it runs on the server, renders to HTML, and ships zero JavaScript for itself. You opt a component into the browser with a directive you’ll meet shortly, and the root layout never does.

It owns <html> and <body>. As your app grows you’ll add nested layouts for specific sections, like everything under /dashboard. A nested layout slots its JSX inside <body> through its own children; it never renders <html> or <body> again, since two <html> tags in one document is invalid HTML. The root layout is the one place these tags exist.

lang belongs here, and omitting it is a real bug. For a single-language app, hardcode lang="en". Without it, a screen reader guesses the language and often guesses wrong, reading English content with, say, Spanish pronunciation. Setting it costs one attribute; skipping it is an accessibility regression.

Where this file sits among its neighbors:

  • Directoryapp/
    • layout.tsx the root layout, owns the html and body tags
    • page.tsx the homepage, rendered into children
    • globals.css the single global stylesheet
    • Directory_components/
      • providers.tsx the client wrapper (next section)

The layout writes <html> and <body>. The document from the start of the lesson also had a full <head>, and the layout has none of it. Where does <head> come from?

You might expect to write <head><title>Acme</title></head> in the layout’s JSX, as in plain HTML. Next.js gives you a better tool: the metadata API . You export a description of the head, and Next.js renders the tags.

It’s a metadata object, typed with Metadata from next, exported alongside RootLayout in the same file:

app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Acme',
description: 'Invoicing for small teams.',
icons: { icon: '/favicon.ico' },
};

Next.js turns each field into the matching <head> tag:

What you export
export const metadata = { title: 'Acme', description: 'Invoicing for small teams.', icons: { icon: '/favicon.ico' }, };
What Next.js renders into <head>
<head> <title>Acme</title> <meta name="description" content="Invoicing for small teams." /> <link rel="icon" href="/favicon.ico" /> </head>

You describe the head as a plain object; Next.js renders the tags. Same color, same tag.

These are the three fields almost every web app sets at the root: a title, a description, and icons for the favicon.

There are two ways to provide metadata, and the difference is when the values are known:

  • metadata, the plain constant object above, is for values fixed at build time: your app’s name, your default description. This is what the root layout uses.
  • generateMetadata, an async function returning a Metadata object, is for values that depend on the route’s data. An invoice page can’t hardcode its title; it fetches invoice #4019 and returns { title: 'Invoice #4019' }. You’ll reach for it constantly once you build real pages.

The first document also had <meta charset> and <meta viewport>, which aren’t in the object. Next.js emits both automatically, with UTF-8 charset and the standard responsive viewport, on every page.

Why a declarative object instead of hand-written tags? The API does two things raw tags can’t: it deduplicates and orders the head across your whole app, and it lets a page override the layout’s defaults, so a page’s own title cleanly wins. Hand-write <title> in two places and you get two conflicting tags, with no rule for which one a crawler honors. The API is the single source of truth, so that conflict can’t happen. This is why, at the end of this lesson, the rule is: never put <head> tags in your JSX.

A performance hint the typed fields don’t cover, like <link rel="preconnect">, goes through the object’s other field: still the API, not raw JSX.

Our layout put one thing inside <body>: {children}. A real app adds a few more, wrapped around the page, and they follow three patterns.

The first is the {children} slot itself, where the current page renders. Every root layout has it.

The second is global providers wrapping {children}. Some things every page needs aren’t visible UI but shared context: the current theme, a data cache, the active language. A provider supplies this by wrapping the tree, so anything beneath it can read that context. Because every page needs them, they wrap {children} at the root.

The third is persistent UI, the chrome that should survive navigation instead of remounting on every page change. The classic example is a portal target for toasts: an empty <div id="toast-root" /> that stays mounted so a “Saved” toast can render into it from anywhere.

Together, a realistic <body> stays tiny:

app/layout.tsx
<body>
<Providers>{children}</Providers>
<div id="toast-root" />
</body>

<Providers> does real work, and the next section explains why it has to be its own component.

What about a navigation bar? The root layout is shared by every route: the marketing homepage, the sign-in screen, the logged-in dashboard. A navbar that only makes sense inside the app doesn’t belong in a layout the sign-in page also renders, so section-specific UI like that goes in a nested layout instead.

That points at the discipline: keep the root layout lean. Everything in it runs on every navigation, so it’s a cost paid on every page transition. Put the shell, the providers, and genuinely global chrome here, and push everything route-specific down. When in doubt, leave it out.

Two integrations belong in the root layout for the same reason <html lang> does: they load once and apply everywhere.

The first is next/font, the Next.js font loader. To apply a font to the whole document, import it in the root layout, call it once at the top of the module with options, then apply the className it returns to <html>. Here is the same app/layout.tsx, grown to load a font and the global stylesheet:

import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}

The single global stylesheet and Tailwind entry point. There’s no name on the left because it’s imported purely for its side effect: its styles join the build and apply to every page. It always sits first.

import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}

Import the Inter font, then call it once at module scope, not inside the component. subsets: ['latin'] ships only the Latin glyphs. Running at the module top level lets Next.js self-host and preload the font at build time, so there’s no runtime request to Google and no flash of the wrong font.

import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}

Apply inter.className to <html> so the font cascades down to the entire document.

1 / 1

That build-time work is the payoff. Subsetting shrinks the file, self-hosting removes the round-trip to Google’s servers, and preloading prevents the layout shift where text reflows when a webfont finally arrives.

Both integrations belong at the root because both are document-wide, authored once in the one file every page shares.

Client code belongs in a Providers child, not the root

Section titled “Client code belongs in a Providers child, not the root”

The root layout is a Server Component: it runs on the server and ships no JavaScript. The one escape hatch into the browser is 'use client' at the top of a file, which marks that file and everything it imports as code that also runs in the browser.

When you need a provider that uses React state, like a theme provider, it is tempting to drop 'use client' at the top of app/layout.tsx. That one line is expensive, because the directive is contagious downward. A 'use client' layout turns every page beneath it, your entire app, into a client subgraph , forfeiting server-only data access and zero-JS rendering for every route at once.

The fix rests on one fact: a Server Component can render a Client Component as a child and pass it children. The boundary is not all-or-nothing at the top of the tree; you place it exactly where it is needed and no higher.

So the client concerns move into their own file, app/_components/providers.tsx, which carries 'use client', takes children, and wraps them in whatever providers need browser state. The layout stays a Server Component and renders <Providers> around the page, so the boundary lands deep in the tree while everything above it stays on the server.

app/layout.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}

One directive ships the whole app to the browser. 'use client' on the layout turns every route beneath it into a client subgraph, losing the Server Components default. The layout needs no browser state; only the provider does.

The repository convention is exactly this: anything needing React state goes in <Providers>, never on the layout.

Because the layout runs on the server, one more failure follows.

The root layout renders to HTML on the server. That HTML arrives in the browser, and React performs hydration : it walks the same tree again in the browser, attaching event handlers, and expects its markup to match the server’s exactly. When the two disagree, you get a hydration mismatch : React warns and may throw the server HTML away.

Picture this line in your root layout:

<p>{new Date().toLocaleTimeString()}</p>

The server renders 14:30:01 and ships the HTML. A few hundred milliseconds later the browser hydrates, runs the same line, and reads 14:30:02. The two can’t reconcile. Anything that differs between the server and client render does this: Date.now(), Math.random(), crypto.randomUUID(), anything reading the current moment or per-request state. Because the root layout wraps every page, the mismatch is global.

Two fixes, in order of preference.

First, scope the dynamic bit to a Client Component. Move the time, random value, or per-request read into a small 'use client' leaf that computes it in the browser, typically after hydration in an effect. The server and the first browser render now agree, and the changing value appears only afterward; the server never rendered the moving part, so nothing mismatches.

Second, more surgically, add suppressHydrationWarning to the one element whose mismatch is genuinely expected:

<html lang="en" suppressHydrationWarning>

This tells React the element’s content will differ between server and client, so it won’t warn. The standard case is a theme library setting the dark class on <html> via an inline script before React hydrates, so the page doesn’t flash the wrong theme; the server can’t know the user’s saved theme, so the mismatch is expected. Two cautions: it covers that one element only, not its subtree, and using it to silence a real mismatch hides the bug instead of fixing it.

The root layout is the one file your whole app shares, so a mistake here is global. The checklist below collects what to keep out.

  • No 'use client'. It turns the entire tree into a client subgraph, forfeiting Server Components for every route. Push client concerns into <Providers>.
  • No raw <head> JSX. It bypasses the metadata API’s deduplication, ordering, and overrides. Use the metadata export; <title> especially is never inline JSX. (The lone exception: a <link rel="preconnect"> hint, via the metadata other field.)
  • No per-request randomness or current time. It causes a global hydration mismatch. Scope it to a Client Component, or, only when the mismatch is expected and benign, add suppressHydrationWarning to that element.
  • No heavy or server-only data fetching. The layout runs on every navigation, so any fetch here is paid on every page transition. Fetch close to the page that needs the data.
  • No per-page UI or per-page metadata. Both belong to a nested layout or the page’s own metadata export. The root is shared by every route, including sign-in and marketing pages.
  • Don’t forget lang. The one thing people omit rather than misplace, and missing it leaves the screen reader guessing the language: an accessibility regression.

A quick self-check on the three that go wrong most often:

Each statement is about the Next.js root layout. Mark each statement True or False.

Adding 'use client' to the top of app/layout.tsx is a clean way to use a theme provider that needs React state.

False. The directive is contagious downward: a 'use client' layout turns the entire app beneath it into a client subgraph, forfeiting the Server Components default for every route. Put 'use client' on a <Providers> child instead — a Server Component can render a Client Component and pass it children.

The page <title> should be set through the exported metadata object, not written as a <title> tag in the layout’s JSX.

True. The metadata API deduplicates, orders, and lets a page override the layout’s head tags. Hand-written <title> JSX bypasses all of that and risks duplicate, conflicting tags with no rule for which one a crawler honors.

Rendering {new Date().toLocaleTimeString()} directly in the root layout is fine because it’s just a string.

False. The server renders one time, the browser hydrates a moment later and computes a different one — a hydration mismatch, and because it’s the root layout, it’s global. Scope the clock to a 'use client' Component that computes it in the browser.

One document, three authors: the root layout owns <html lang> and <body>, the metadata API owns <head>, and <Providers> owns anything client-only.

The references below are the canonical source for each piece of the document this lesson took apart: the layout file, the metadata API, the <head> it produces, and the Server/Client boundary the <Providers> rule rests on.