04 - Layouts
📋 Jump to TakeawaysWhat are Layouts?
Layouts wrap pages with shared UI (nav bars, sidebars, footers). They persist across navigation — only the page content inside changes. Layouts can be nested, and each level can load its own data.
How Rendering Works (Lifecycle)
Layouts render pages, not the other way around. When a request comes in, SvelteKit works top-down:
- Runs load functions: root layout → nested layout → page
- Renders components: root layout first, which calls
{@render children()}to render the next layout, which calls{@render children()}again to render the page
+layout.svelte (root: header, nav, footer)
└── [topic]/+layout.svelte (sidebar + content area)
└── [topic]/[lesson]/+page.svelte (the actual lesson)The layout is the parent. The page is the child. If a layout doesn't call {@render children()}, the page never appears.
Root Layout
Every SvelteKit app has a root layout that wraps all pages. The children snippet is where the page content renders:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
let { children } = $props(); // <!-- SvelteKit passes this automatically -->
</script>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
{@render children()} <!-- renders whatever +page.svelte matches the current URL -->
</main>
<footer>© 2026</footer>Every page in the app will have this header and footer.
Nested Layouts
A layout in a subfolder wraps only the pages in that folder. Layouts stack — the dashboard layout renders inside the root layout:
src/routes/
├── +layout.svelte ← Root layout (nav + footer)
├── +page.svelte ← Home page
└── dashboard/
├── +layout.svelte ← Dashboard layout (sidebar) — nested inside root
├── +page.svelte ← /dashboard
└── settings/
└── +page.svelte ← /dashboard/settings<!-- src/routes/dashboard/+layout.svelte -->
<script lang="ts">
let { children } = $props();
</script>
<div class="dashboard">
<aside>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</aside>
<div class="content">
{@render children()} <!-- /dashboard or /dashboard/settings renders here -->
</div>
</div>Result: /dashboard/settings renders inside dashboard layout, which renders inside root layout.
Layout Data
Layouts can have their own load functions. The data is available to the layout and all child pages:
// src/routes/+layout.ts
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ fetch }) => {
const res = await fetch('/api/user');
const user = await res.json();
return { user }; // available in +layout.svelte AND all child pages
};<!-- src/routes/+layout.svelte -->
<script lang="ts">
let { data, children } = $props(); <!-- data = { user } from load -->
</script>
<header>
<p>Welcome, {data.user.name}</p>
</header>
{@render children()}Layout Server Data
Use +layout.server.ts for server-only data (DB, secrets, locals):
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return {
user: locals.user // set by hooks.server.ts (see lesson 08)
};
};Accessing Layout Data in Pages
Pages automatically receive data from parent layouts merged with their own data:
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
let { data } = $props();
// data includes both this page's load data AND parent layout data
</script>
<h1>Hello {data.user.name}</h1> <!-- user came from root layout's load -->Layout Groups
Parentheses create groups that share a layout without affecting URLs:
src/routes/
├── (marketing)/
│ ├── +layout.svelte ← simple layout (no sidebar)
│ ├── about/+page.svelte → /about
│ └── contact/+page.svelte → /contact
└── (app)/
├── +layout.svelte ← app layout (sidebar + auth)
└── dashboard/+page.svelte → /dashboard(marketing) and (app) don't appear in URLs — they just group routes under different layouts.
Breaking Out of Layouts
Use @ to skip parent layouts. Rarely needed but useful for special pages:
+page@.svelte— uses only the root layout (skips all intermediate layouts)+page@(app).svelte— uses the(app)group layout
Error Boundaries
+error.svelte catches errors from load functions and renders an error page. Use $page from $app/state to access the error details:
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/state';
</script>
<h1>{page.status}: {page.error.message}</h1>Each layout level can have its own +error.svelte — errors bubble up to the nearest one.
Loading States
Show a loading indicator during navigation using the navigating store:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { navigating } from '$app/stores';
let { children } = $props();
</script>
{#if $navigating}
<div class="loading">Loading...</div>
{/if}
{@render children()}Key Takeaways
- Layouts (
+layout.svelte) wrap pages with shared UI; uselet { children } = $props()and{@render children()}to render page content - Layouts nest automatically — a layout in a subfolder renders inside its parent layout
- Layout load functions (
+layout.tsor+layout.server.ts) provide data to the layout and all child pages - Pages automatically receive merged data from their own load function and all parent layout load functions
- Layout groups
(name)let you apply different layouts to different route groups without affecting URLs - Use
+page@.svelteto break out of intermediate layouts and render with only the root layout +error.sveltecatches load function errors at each layout level; access details viapage.statusandpage.errorfrom$app/state- Show loading indicators during navigation using
navigatingfrom$app/stores