Skip to content
Chapter 17Lesson 2

The Next.js root layout owns the document shell

How the Next.js App Router builds the HTML document around your React app, and the rules for what belongs in the root layout that wraps every page.

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>;
}

Now run pnpm dev, open the page, and choose “View Source” from the browser menu. You don’t see <h1>Welcome</h1> sitting alone. You see 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> is in there, buried near the bottom: inside a <body>, inside an <html lang="en">, under a <head> full of tags you never typed, next to a <script> you didn’t add. The question this lesson answers is who writes the rest of the page?

In the Next.js App Router , the document around your components has exactly three authors. Learning which one owns which piece is most of the lesson:

  • 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 that lives inside <body>.

That split is not bureaucracy. The document shell is a set of decisions read by machines you never see: a search crawler reads your <title>, a screen reader reads lang="en", and the browser reads <meta charset> before it can decode a single byte of your page. Get the shell right and those machines do their jobs. Get it wrong, or put the wrong thing in it, and you break SEO, accessibility, or rendering for every page at once, because this is the one file the whole app shares.

By the end you’ll be able to write a correct app/layout.tsx from memory, and to name what must never go in it and exactly what breaks when it does. You’ll also meet children as a real, load-bearing prop. In the last lesson it was a name on a list; here it’s the slot your entire app renders into.

Before we can talk about which file owns which part, we need a shared picture of the parts themselves. You met the DOM as a tree of nodes back in the DOM chapter; this is the document frame that sits on top of that tree.

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

Every page shares this skeleton. <head> holds metadata that machines read, and <body> holds what people see. The rest of the lesson is about which file writes which box.

Four pieces carry all the weight here. Learn what each is for once and the names stop being noise:

  • <!DOCTYPE html> is the standards-mode switch. It isn’t an element and it has no closing tag; it’s a one-line instruction that tells the browser “render this page by modern web standards.” Without it, browsers fall back to quirks mode and emulate decades-old bugs. Next.js emits this line for you, so you will never write it.
  • <html lang="en"> is the root element, the single box everything else lives inside. Its lang attribute declares the document’s language, and three different consumers read it: a screen reader uses it to pick the right pronunciation, the browser uses it for hyphenation, and translation tools use it to know what they’re translating from. You always set it.
  • <head> holds metadata, meaning information about the page that isn’t drawn on the page itself: the <title>, the character encoding, the viewport settings, the favicon link, social-share tags.
  • <body> holds everything visible. This is where your React tree mounts; your <h1> from a moment ago landed here.

One point here is worth carrying through the whole lesson. The contents of <head> look invisible because nothing in there renders on the page, but they are the most-read part of your document, just not by humans. Your <title> is the text on the browser tab and the blue heading of your search result on Google. Your <meta charset> tells the browser which character encoding to use to turn raw bytes into text, and if you get it wrong or omit it, your page renders mojibake . Your <meta name="description"> is the grey snippet under that search result. None of this is decoration: it’s a set of contracts with the browser and with every crawler that visits, and the root layout is where you sign them.

This is the structural core of the lesson, so it’s worth going slowly.

In the App Router, a file named app/layout.tsx is the root layout . It’s the outermost component in your app, and Next.js renders it once, wrapping it around every single page. It is also required: Next.js refuses to build an App Router project without one. There is no app without this file.

Here is the whole of 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>
);
}

The very first line imports globals.css. Notice there’s no name on the left: you’re not importing a value, you’re importing the file for its side effect, which is pulling the app’s single global stylesheet (the Tailwind entry point) into the build. It lives here, in the root layout, so those styles apply to every page. We’ll come back to it.

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

The component is a default export named RootLayout. This course uses named exports almost everywhere, and this is one of the handful of exceptions, because the App Router finds this component by its default export. The framework dictates it, and the rule still holds everywhere else. Its only prop is children, typed React.ReactNode.

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

children is the prop the last lesson named and left as a forward reference, and here it pays off. Next.js injects your page into this slot through the framework convention, so RootLayout never imports it. The type React.ReactNode covers anything React can render: JSX, a string, a number, an array of those, even null. The full story of children as a component API comes in a later chapter. For now, just read it 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"> is where the layout renders the root element and sets the document language. No other file in your app 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> places the children slot inside the body. This single line is where your entire visible app mounts. Read top to bottom, this tiny file is the document shell: it opens <html>, opens <body>, drops the page in, and closes both.

1 / 1

Three facts about this file matter more than the rest. They’re the difference between a layout an experienced engineer trusts and one that quietly breaks.

It’s a Server Component, and that shapes everything else about the file. Notice what’s absent: no 'use client' line at the top, no hooks, no window or localStorage. By default, every component in the App Router is a Server Component , meaning it runs on the server, renders to HTML, and ships zero JavaScript to the browser for itself. You opt a component into the browser with a directive you’ll meet shortly, and the root layout never does. The full Server/Client model gets a whole chapter later on. For now, hold one sentence: the layout runs on the server and sends down HTML.

It owns <html> and <body>, and nothing else may render them. This is a hard rule. As your app grows you’ll add nested layouts, which are layouts for a specific section, like everything under /dashboard, and those are covered in a later chapter. The thing to know now is that a nested layout returns JSX that slots inside <body> through its own children; it never renders <html> or <body> again. The reason is simple: two <html> tags in one document is invalid HTML. The root layout is the one and only place these two tags exist.

lang belongs here, and forgetting it is a real bug. For a single-language SaaS, hardcode lang="en" exactly as shown. (When you later build a multi-language app, that value gets rendered dynamically from the URL or the user’s locale; a later chapter handles it, so ignore it for now.) Leaving lang off isn’t a style nit. A screen reader then has to guess the language, and it often guesses wrong, reading English content with, say, Spanish pronunciation rules. So set lang on every root layout: it costs one attribute, and skipping it is an accessibility regression.

A quick map of where this file sits and who its neighbors are:

  • 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)

So the layout writes <html> and <body>. But look back at the document from the start of the lesson: there was a whole <head> full of tags, and the layout we just wrote doesn’t contain a single one of them. Where does <head> come from?

You might expect to write <head><title>Acme</title></head> inside the layout’s JSX, the way you would in a plain HTML file. You don’t. Next.js gives you a different, better tool for <head>: the metadata API . Instead of writing tags, you export a description of them, and Next.js renders the actual <head> tags for you.

It looks like this: a metadata object, typed with Metadata from next, exported right 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' },
};

That object isn’t markup, but Next.js turns it into markup. Each field becomes 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.

Those are the three fields almost every SaaS sets at the root: a title, a description, and icons for the favicon. (title can also be a template object so every page gets a ”— Acme” suffix automatically, but that’s a refinement for a later chapter, and one string is the right start.)

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

  • metadata, a plain constant object as shown above, is for when the values are fixed at build time: your app’s name, your default description. This is what the root layout uses.
  • generateMetadata, an async function that returns a Metadata object, is for when the values depend on the route’s data. An invoice page can’t hardcode its title; it has to fetch invoice #4019 and return { title: 'Invoice #4019' }. You’ll reach for generateMetadata constantly once you’re building real pages, and its full surface (social-share cards, dynamic preview images) is a later chapter. For now, just know the name and that it’s the dynamic sibling of metadata.

You might have noticed two tags in that first document, <meta charset> and <meta viewport>, that aren’t in our metadata object at all. Next.js emits both automatically. UTF-8 charset and the standard responsive viewport are there on every page without you lifting a finger, and you almost never override them. (There’s a separate viewport export for the rare case where you need per-page variation, but reaching for it is unusual, and a later chapter covers it.)

That leaves the question of why a declarative object instead of letting you write <head> tags yourself. The metadata API does two things hand-written tags can’t. It deduplicates and orders the head tags across your whole app, and it lets a page override the layout’s defaults, so a specific page can export its own title and have it cleanly win over the layout’s. Hand-write <title> as raw JSX in two places and you get two <title> tags in conflict, with no rule for which one a crawler honors. The API is the single source of truth precisely so that conflict can’t happen. That’s the reasoning behind a rule you’ll see again at the end of this lesson: never put <head> tags in your JSX.

There is exactly one honest exception, named here so you recognize it later. A performance hint like <link rel="preconnect"> that the typed fields don’t directly cover goes through the metadata object’s other field: still the API, still not raw JSX.

To lock in the split, sort each of these into the author responsible for it in the document.

Each item below is part of the rendered HTML document. Sort it into the author responsible for it. Drag each item into the bucket it belongs to, then press Check.

Metadata API writes the head
Root layout JSX writes the html and body tags
The page <title>
The <meta name="description">
The favicon <link rel="icon">
The <meta charset="utf-8"> (framework’s job, never hand-written JSX)
The lang attribute
The <body> element
Wrapping the page in {children}

The layout we wrote put just one thing inside <body>: {children}. In a real app, a few other things legitimately live there, wrapped around the page. There are exactly three patterns, and it’s worth knowing all three so you recognize them, along with the discipline that keeps the list short.

The first you’ve already met: the {children} slot itself. It’s always present, and it’s where the current page renders. Every root layout has it.

The second is global providers wrapping {children}. Some things every page needs aren’t UI you can see; they’re shared context, such as the current theme (light or dark), a data cache, or the active language. In React these are supplied by a provider component that wraps the tree, so anything rendered beneath it can read that context. Because every page needs them, providers wrap {children} at the root. You’ll wire real ones in later chapters, like a theme provider, a query client, or an i18n provider, but the shape is always the same: a wrapper around the children.

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

Putting those together, a realistic <body> looks like this, still tiny:

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

That <Providers> wrapper is doing real work, and the next section is entirely about why it has to be its own component. For now, read it as “the box the providers live in.”

What about a navigation bar? A site-wide navbar is a candidate for the root layout, but the same discipline applies. The root layout is shared by every route: your marketing homepage, your sign-in screen, your logged-in dashboard. A navbar that only makes sense inside the app doesn’t belong in a layout the sign-in page also renders. Section-specific UI like that belongs in a nested layout (a later chapter), not the root.

That points at a reflex experienced engineers build, and it underpins several of the warnings later in this lesson: keep the root layout lean. Everything in it runs on every navigation, so everything in it is a cost paid globally, on every page transition. Put the shell, the providers, and genuinely global chrome here, and push everything route-specific down into nested layouts. When in doubt, leave it out.

Two integrations have the root layout as their natural home, for the same reason <html lang> does: they load once and apply everywhere. You won’t learn either subsystem in depth here, just where they live and why.

The first is next/font, Next.js’s font loader. You want a font to apply to the whole document, so you load it in the root layout. The shape is: import a font, call it once at the top of the module with some options, then apply the className it hands back to <html>. Here’s 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 globals.css import again: the single global stylesheet, the Tailwind entry point. There’s no name on the left because it’s imported purely for its side effect, so 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, with options. subsets: ['latin'] tells Next.js to ship only the Latin glyphs. The call returns an object, which we keep in inter. Because it runs at the module top level, Next.js self-hosts and preloads the font at build time, so there’s no request to Google at runtime 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>. That’s the same inter from step 2, so the font and the class are one connected thing. With the class on the root element, the font cascades down to the entire document.

1 / 1

The payoff of next/font, worth naming even though the depth is a later chapter, is that Next.js self-hosts and subsets the font at build time and preloads it. Subsetting shrinks the file, self-hosting removes the runtime round-trip to Google’s servers, and preloading prevents the layout shift where text visibly reflows when a webfont finally arrives.

The second integration is globals.css, the side-effect import you’ve seen on line one of both versions. It’s the single global stylesheet for the whole app, and it’s the entry point for Tailwind, the styling system the next chapter is entirely about. You import it in the root layout for the same “apply everywhere” reason, and you import it for its side effect: it has no named export to grab, it just needs to be in the build. The next chapter unpacks what’s actually inside it.

Both belong at the root because both are document-wide. The font and the global stylesheet apply to every page, just like lang does, so they’re 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”

This is the rule whose mistakes cost the most, so it’s worth building as cause and effect rather than memorizing.

You’ve seen that the root layout is a Server Component: it runs on the server and ships no JavaScript. The escape hatch into the browser is a single line, 'use client' , at the top of a file. That directive marks the file, and everything it imports, as code that also runs in the browser. It’s how you opt a component into being interactive.

Here is where it goes wrong. When you need a provider that uses React state, like a theme provider, it’s tempting to put 'use client' at the top of app/layout.tsx and be done. That one line is costly because the directive is contagious downward. A 'use client' layout doesn’t just make the layout a Client Component; it turns every page beneath it, which is your entire app, into a client subgraph . One word at the top of one file ships your whole application to the browser as client JavaScript, forfeiting the Server Components default (server-only data access, zero-JS rendering) for every route at once.

The fix is the <Providers> pattern, and it rests on one fact that surprises most people: a Server Component can render a Client Component as a child and hand it children. The boundary isn’t all-or-nothing at the top of the tree. You put it exactly where it’s needed and no higher.

So the client concerns move into their own file. That file, app/_components/providers.tsx, carries 'use client' at its top, takes children, and wraps them in whatever providers need browser state. The root layout stays a pure Server Component and simply renders <Providers> around the page. The boundary lands on <Providers>, deep in the tree, and everything above it, including the entire layout, 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. Putting 'use client' on the layout turns every route beneath it into a client subgraph, so the entire app loses the Server Components default. The layout itself doesn’t even need browser state; only the provider does.

The repository convention is exactly this: one provider shell, mounted as a <Providers> Client Component in the root layout. When you add a theme later, or a data cache, or anything that needs React state, it goes inside that file, never on the layout itself.

The second failure is subtler, and it follows directly from “the layout runs on the server.” You don’t need the full theory yet, just enough to recognize it and reach for the fix.

The mechanism is short. The root layout renders to HTML on the server. Then that HTML arrives in the browser and React performs hydration : it walks the very same tree again, in the browser, attaching event handlers as it goes, and it expects the markup it produces to match the server’s exactly. When the two don’t match, you get a hydration mismatch , where React warns in the console and may discard the server HTML entirely.

Picture this line in your root layout:

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

The server renders it at, say, 14:30:01. The HTML ships. A few hundred milliseconds later the browser hydrates and runs the same line, but now the clock reads 14:30:02. The server produced one value, the browser computed another, and React can’t reconcile them. Anything that differs between the server render and the client render produces this: Date.now(), Math.random(), crypto.randomUUID(), or anything reading the current moment or per-request state. And because it’s in the root layout, which wraps every page, the mismatch is global.

There are two fixes, in order of preference.

The first choice is to scope the dynamic bit to a Client Component. Move the time, the random value, or the per-request read into a small 'use client' leaf that computes it in the browser (often after hydration, in an effect). Then the server render and the first browser render agree, and the changing value appears only afterward. The mismatch never happens because the server never tried to render the moving part.

The second, more surgical fix is suppressHydrationWarning on the one specific element whose mismatch is genuinely expected and harmless:

<html lang="en" suppressHydrationWarning>

This tells React “I know this one element’s content will differ between server and client, so don’t warn.” The textbook legitimate use is a theme library setting the dark class on <html> via a tiny inline script before React hydrates, so the page doesn’t flash the wrong theme. The server couldn’t have known the user’s saved theme, so a mismatch on <html> is expected and fine. You’ll see exactly this in a later chapter. Two cautions, though. It suppresses the warning for that one element only, not its whole subtree, and reaching for it to silence a real mismatch hides the bug rather than fixing it. Use it only where the mismatch is genuinely expected.

The full hydration-mismatch story is a later chapter. Here you just need to recognize the failure on sight and know the two ways out.

Everything above converges on a short checklist. The root layout is the one file shared by your whole app, so the cost of a mistake here is global, which is why it’s worth knowing what not to put in it. Each item below ties back to the section that explained why:

  • No 'use client'. It turns the entire tree into a client subgraph and forfeits Server Components for every route. Push client concerns into <Providers>.
  • No raw <head> JSX. It bypasses the metadata API’s deduplication, ordering, and override behavior. Use the metadata export; <title> especially is never inline JSX. (The lone exception: a <link rel="preconnect"> performance hint goes through the metadata other field.)
  • No per-request randomness or current time. It causes a hydration mismatch, globally. Scope it to a Client Component, or, only when the mismatch is expected and benign, add suppressHydrationWarning to that one 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 actually 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. This is the one thing people omit rather than misplace, and missing it is an accessibility regression, because the screen reader is left guessing the language.

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.

The mental model to walk away with is compact: the root layout is a Server Component that renders once around every page, owns <html lang> and <body> exclusively, delegates <head> to the metadata API, and pushes anything client-only into a <Providers> child. Three authors, one document. Get that split right and the machines that read the document, meaning crawlers, screen readers, the browser, and React’s hydration, all find what they need.

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. You don’t need any of them to write a correct root layout, but they’re worth a bookmark for the day you reach past what this lesson covered.