Layouts & Navigation in Next.js: Shared UI and Client Components
Step 3 of 31 — Next.js Tutorial Series | Source code for this step
What You Will Build
By the end of this step you will have a persistent navigation bar that stays on screen as you move between pages. You will understand layouts, the Link component for client-side navigation, and why some components must be Client Components.
Goal: Click links in a header to navigate between Home, Posts, and Login — instantly, without page reloads.
Table of Contents
- The Problem: Repeated UI
- Core Concept: Layouts
- Update the Root Layout
- Simplify Every Page
- Core Concept: Client Components vs Server Components
- Create the Header Component
- Core Concept: The Link Component
- Wire the Header into the Layout
- Core Concept: usePathname — Highlighting the Active Link
- Run and Test
- Summary & Key Takeaways
The Problem: Repeated UI
Look at your current pages. Every single one (page.tsx, posts/page.tsx, posts/new/page.tsx) has a duplicated header:
<header className="bg-white shadow-sm">
<div className="mx-auto max-w-4xl px-6 py-4">
<h1 className="text-2xl font-bold text-gray-900">Superblog</h1>
</div>
</header>
If you need to change the header (add a logo, change the color, add navigation links), you would have to edit every single page. This violates the DRY principle (Don't Repeat Yourself).
In plain React, you would solve this with a wrapper component or a React Router <Outlet>:
// Plain React solution
function App() {
return (
<>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts" element={<Posts />} />
</Routes>
</>
)
}
Next.js solves this more elegantly with layouts.
Core Concept: Layouts
A layout is a component that wraps one or more pages. It is defined in a file called layout.tsx.
You already have one — the Root Layout at app/layout.tsx. It wraps your entire application with <html> and <body> tags.
Here is the key insight: Layouts receive {children} as a prop. The children is the page.tsx component (or a nested layout) of the current route. When the user navigates between pages, only the {children} part re-renders — the layout stays mounted.
┌─────────────────────────────────┐
│ layout.tsx │ ← stays mounted
│ ┌───────────────────────────┐ │
│ │ {children} │ │ ← swapped when navigating
│ │ (page.tsx content) │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
This means if you put a header inside the layout, it will persist across all page navigations. No duplication needed.
Layout nesting rules:
| File location | What it wraps |
|---|---|
app/layout.tsx | All pages in the entire app |
app/posts/layout.tsx | Only pages inside /posts and its children |
app/posts/[id]/layout.tsx | Only the /posts/[id] page and its children |
You can have layouts at any level. They nest automatically:
Root Layout
└── Posts Layout (optional)
└── page.tsx
For now, we only use the Root Layout.
Update the Root Layout
Replace the entire contents of app/layout.tsx with:
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})
export const metadata: Metadata = {
title: 'Superblog',
description: 'A full-stack blog built with Next.js, Prisma, and NextAuth.js',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<div className="flex min-h-screen flex-col bg-gray-50">
{/* Header will go here in the next section */}
<main className="mx-auto w-full max-w-4xl flex-1 px-6 py-12">
{children}
</main>
<footer className="mx-auto w-full max-w-4xl px-6 py-8 text-center text-sm text-gray-400">
Built with Next.js 16, React 19, Prisma & NextAuth.js
</footer>
</div>
</body>
</html>
)
}
What changed and why:
-
Added structure around
{children}— We wrapped the body content in a flex column (flex flex-col) with a minimum height of the full screen (min-h-screen). This pushes the footer to the bottom. -
Added
<main>wrapper — The{children}is now inside a<main>element with consistent max-width and padding. Every page automatically gets this container without having to define it themselves. -
Moved the footer here — The footer is shared across all pages, so it belongs in the layout. No more duplicating it in each page.
-
Left a comment for the header — We will add it in the next section as a separate component.
Key concept: Everything outside
{children}in the layout is shared and persistent. It does not re-render when navigating between pages. This is fundamentally different from plain React, where the entire component tree re-renders on route changes.
Simplify Every Page
Now that the layout handles the outer structure, header, and footer, each page only needs its unique content. Update all your pages:
app/page.tsx — Home Page
Replace the entire file with:
export default function Home() {
return (
<>
<h2 className="mb-4 text-4xl font-bold text-gray-900">
Welcome to Superblog
</h2>
<p className="mb-8 text-lg text-gray-600">
A full-stack blog application built with Next.js, Prisma, and
NextAuth.js. We are building this step by step.
</p>
<div className="grid gap-6 md:grid-cols-2">
<div className="rounded-lg bg-white p-6 shadow-md">
<h3 className="mb-2 text-lg font-semibold text-gray-800">
📝 Blog Posts
</h3>
<p className="text-gray-600">
Create, read, and delete blog posts. Coming once we connect to the
database.
</p>
</div>
<div className="rounded-lg bg-white p-6 shadow-md">
<h3 className="mb-2 text-lg font-semibold text-gray-800">
🔐 Authentication
</h3>
<p className="text-gray-600">
User registration and login with NextAuth.js. Coming in a later
step.
</p>
</div>
</div>
</>
)
}
Notice: no <header>, no <footer>, no min-h-screen, no max-w-4xl. The layout provides all of that. The page only defines what is unique to the home page.
We use a React Fragment (<>...</>) because a React component can only return a single element, and here all the tags (<h1>, <p>, etc.) are siblings — a Fragment groups them without adding an extra <div> to the DOM.
app/posts/page.tsx — Posts List
Replace the entire file with:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'All Posts — Superblog',
description: 'Browse all blog posts on Superblog',
}
export default function PostsPage() {
return (
<>
<h2 className="mb-4 text-3xl font-bold text-gray-900">All Posts</h2>
<p className="mb-8 text-gray-600">
Posts will appear here once we connect to the database.
</p>
<div className="rounded-lg bg-white p-6 shadow-md">
<p className="italic text-gray-400">
No posts yet. Check back after Step 8!
</p>
</div>
</>
)
}
app/posts/new/page.tsx — Create Post
Replace the entire file with:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Create New Post — Superblog',
description: 'Write and publish a new blog post',
}
export default function NewPostPage() {
return (
<>
<h2 className="mb-4 text-3xl font-bold text-gray-900">Create New Post</h2>
<p className="mb-8 text-gray-600">
The form to create a post will be here once we add Server Actions in
Step 15.
</p>
<div className="rounded-lg bg-white p-6 shadow-md">
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Title
</label>
<div className="h-10 w-full rounded border border-gray-300 bg-gray-100" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Content
</label>
<div className="h-32 w-full rounded border border-gray-300 bg-gray-100" />
</div>
<button
disabled
className="cursor-not-allowed rounded bg-gray-300 px-4 py-2 text-gray-500"
>
Create Post (coming soon)
</button>
</div>
</div>
</>
)
}
app/login/page.tsx — Login
Replace the entire file with:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Sign In — Superblog',
description: 'Sign in to your Superblog account',
}
export default function LoginPage() {
return (
<div className="flex items-center justify-center py-12">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900">
Sign In
</h2>
<p className="mb-6 text-center text-gray-500">
Authentication will be added in Step 12.
</p>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Email
</label>
<div className="h-10 w-full rounded border border-gray-300 bg-gray-100" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Password
</label>
<div className="h-10 w-full rounded border border-gray-300 bg-gray-100" />
</div>
<button
disabled
className="w-full cursor-not-allowed rounded bg-gray-300 py-2 text-gray-500"
>
Sign In (coming soon)
</button>
</div>
<p className="mt-4 text-center text-sm text-gray-500">
Don't have an account?{' '}
<span className="text-blue-500">Register</span>
</p>
</div>
</div>
)
}
Notice that the login page no longer has min-h-screen or bg-gray-50 — the layout provides those. The page just centers its card within the available space.
Core Concept: Client Components vs Server Components
Before we build the Header, we need to understand when and why you use a Client Component.
Server Components (the default) run on the server:
- ✅ Can query databases directly
- ✅ Can read files from the filesystem
- ✅ Send zero JavaScript to the browser
- ❌ Cannot use
useState,useEffect,useContext - ❌ Cannot use event handlers (
onClick,onChange) - ❌ Cannot use browser APIs (
window,localStorage)
Client Components (marked with "use client") run in the browser:
- ✅ Can use
useState,useEffect,useContext - ✅ Can use event handlers and browser APIs
- ❌ Cannot query databases directly
- Send JavaScript to the browser (increases bundle size)
The decision rule is simple:
Does this component need interactivity (state, effects, event handlers)?
- Yes → add
"use client"at the top- No → leave it as a Server Component (default)
Our Header needs to highlight the active navigation link. To know which link is active, we need the usePathname() hook — a React hook that only works in Client Components. Therefore, our Header must be a Client Component.
Important architecture pattern: Keep Client Components as small and specific as possible. The layout itself stays a Server Component. Only the Header (which needs interactivity) becomes a Client Component. This way, the browser only downloads the minimal JavaScript needed.
layout.tsx ← Server Component (no JS sent to browser)
├── Header.tsx ← Client Component ("use client" — JS sent to browser)
├── {children} ← page.tsx — Server Component
└── <footer> ← part of layout — Server Component
Create the Header Component
Create the file app/Header.tsx:
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export default function Header() {
const pathname = usePathname()
const links = [
{ href: '/', label: 'Home' },
{ href: '/posts', label: 'Posts' },
{ href: '/posts/new', label: 'New Post' },
{ href: '/login', label: 'Sign In' },
]
return (
<header className="bg-white shadow-sm">
<nav className="mx-auto flex w-full max-w-4xl items-center justify-between px-6 py-4">
<Link href="/" className="text-xl font-bold text-gray-900">
Superblog
</Link>
<div className="flex items-center space-x-4">
{links.map((link) => {
const isActive = pathname === link.href
return (
<Link
key={link.href}
href={link.href}
className={`rounded px-3 py-2 text-sm font-medium transition ${
isActive
? 'bg-blue-500 text-white'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`}
>
{link.label}
</Link>
)
})}
</div>
</nav>
</header>
)
}
Let's break down every part of this component:
"use client" Directive
'use client'
This is the very first line in the file (before any imports). It tells Next.js: "This component and everything it imports should run in the browser."
Without this directive, Next.js would try to run this component on the server, and usePathname() would fail because it is a browser-only hook.
Rules for "use client":
- It must be the first line (before imports)
- It only needs to be in the entry point — all components imported by a Client Component automatically become Client Components too
- It creates a boundary — Server Components above can still be Server Components
usePathname() Hook
const pathname = usePathname()
usePathname() is a Next.js hook from "next/navigation". It returns the current URL path as a string:
| Current URL | pathname value |
|---|---|
http://localhost:3000 | "/" |
http://localhost:3000/posts | "/posts" |
http://localhost:3000/posts/new | "/posts/new" |
http://localhost:3000/login | "/login" |
We use it to determine which navigation link should be highlighted.
Active Link Logic
const isActive = pathname === link.href
We use a simple exact match — the link is highlighted only when the current path matches it exactly. This ensures that visiting /posts/new highlights only "New Post" and not "Posts" as well.
Core Concept: The Link Component
We used Link briefly in Step 2 for the 404 page. Now let's understand it fully.
import Link from 'next/link'
;<Link href="/posts">Posts</Link>
How Link Works Under the Hood
When Next.js renders a <Link>, it outputs a regular <a> tag in the HTML. But it attaches a JavaScript click handler that:
- Prevents the default browser navigation (which would cause a full page reload)
- Fetches only the new page's content from the server
- As each link here use the same layout, Swaps the
{children}in the layout with the new page's content - Updates the browser URL using the History API
The result: the layout (header, footer) stays mounted. Only the page content changes. The transition is instant.
Link vs anchor tag comparison
// ❌ Full page reload — downloads everything again
<a href="/posts">Posts</a>
// ✅ Client-side navigation — instant, layout preserved
<Link href="/posts">Posts</Link>
Prefetching
Link also prefetches pages in the background. When a <Link> is visible in the viewport, Next.js automatically fetches that page's data ahead of time. So when the user clicks, the page is already loaded — the navigation feels instant.
You can disable this with <Link href="/posts" prefetch={false}>, but the default behavior is what you want most of the time.
Wire the Header into the Layout
Now import the Header component into the Root Layout. Update app/layout.tsx:
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import Header from './Header'
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})
export const metadata: Metadata = {
title: 'Superblog',
description: 'A full-stack blog built with Next.js, Prisma, and NextAuth.js',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<div className="flex min-h-screen flex-col bg-gray-50">
<Header />
<main className="mx-auto w-full max-w-4xl flex-1 px-6 py-12">
{children}
</main>
<footer className="mx-auto w-full max-w-4xl px-6 py-8 text-center text-sm text-gray-400">
Built with Next.js 16, React 19, Prisma & NextAuth.js
</footer>
</div>
</body>
</html>
)
}
What is happening architecturally:
layout.tsx (Server Component)
│
├── <Header /> ← Client Component (has "use client")
│ Only this part sends JS to the browser
│
├── <main>
│ └── {children} ← page.tsx (Server Component)
│ Renders as pure HTML, zero JS
│
└── <footer> ← Part of layout (Server Component)
Renders as pure HTML
The Root Layout is still a Server Component. It imports the Client Component Header, but that does not make the layout itself a Client Component. Next.js handles the boundary automatically:
- The layout renders on the server and produces HTML
- The Header renders on the server initially (for the first HTML) but also hydrates on the client so
usePathname()can work - The page renders purely on the server
This is called the "island architecture" pattern — most of the page is static server-rendered HTML, with small interactive "islands" (the Header) that hydrate with JavaScript.
Core Concept: usePathname — Highlighting the Active Link
Let's visualize what happens as you navigate:
When you visit /:
usePathname()returns"/"- Home link:
"/" === "/"→ active (blue) - Posts link:
"/" === "/posts"→ false (gray) - New Post link:
"/" === "/posts/new"→ false (gray) - Sign In link:
"/" === "/login"→ false (gray)
When you visit /posts/new:
usePathname()returns"/posts/new"- Home link:
"/posts/new" === "/"→ false (gray) - Posts link:
"/posts/new" === "/posts"→ false (gray) - New Post link:
"/posts/new" === "/posts/new"→ active (blue) - Sign In link:
"/posts/new" === "/login"→ false (gray)
Only "New Post" highlights when you are on /posts/new. Each link highlights only when you are on that exact page.
Why is
usePathnamea Client Component hook? The pathname can change without a full page reload (via client-side navigation). A Server Component renders once on the server and cannot react to URL changes. A Client Component runs in the browser and can re-render when the URL changes, which is exactly what we need for active link highlighting.
Run and Test
Start the dev server:
pnpm dev
Visit http://localhost:3000 and test:
| Action | What should happen |
|---|---|
| See the header | "Superblog" logo on the left, nav links on the right |
| "Home" is highlighted blue | You are on / |
| Click "Posts" | Page content swaps instantly. "Posts" link turns blue. Header stays. |
| Click "New Post" | Shows the create post form mockup. Only "New Post" is blue. |
| Click "Sign In" | Shows the login card. "Sign In" is blue. |
| Click "Superblog" logo | Returns to home. "Home" is blue. |
Visit /anything | Custom 404 page appears. No nav link is highlighted. |
| Check transitions | No page flicker. The header and footer never disappear. |
Your final folder structure:
app/
├── globals.css
├── layout.tsx ← Root Layout (with Header + footer)
├── Header.tsx ← Client Component (navigation)
├── not-found.tsx ← Custom 404
├── page.tsx ← / (simplified — just content)
├── login/
│ └── page.tsx ← /login (simplified)
└── posts/
├── page.tsx ← /posts (simplified)
└── new/
└── page.tsx ← /posts/new (simplified)
Summary & Key Takeaways
| Concept | What it means |
|---|---|
Layout (layout.tsx) | A persistent wrapper around pages. Stays mounted during navigation. |
{children} in layouts | The slot where the current page renders. Only this part swaps. |
| Server Component | Default. Runs on server. No JS sent to browser. Cannot use hooks/events. |
| Client Component | Marked with "use client". Runs in browser. Can use hooks and events. |
"use client" directive | Must be the first line. Creates a client boundary. |
Link component | Client-side navigation. No page reload. Layout stays mounted. |
| Prefetching | Link automatically fetches pages before the user clicks. |
usePathname() | Hook that returns the current URL path. Client Component only. |
| Island architecture | Most of the page is server-rendered; small interactive parts hydrate. |
Design Decision: Where to Put Components
In this tutorial, we put Header.tsx directly in the app/ folder next to layout.tsx. This is fine for small projects. As projects grow, some teams create a components/ folder. Both approaches work — Next.js does not enforce a component organization pattern. The only rule is that page.tsx, layout.tsx, and other special files must follow the naming conventions.
What is Next
In Step 4, we will introduce Prisma ORM — the tool that connects our application to a database. We will install Prisma, understand what an ORM does, and set up the database configuration. No more placeholder content — real data is coming.