04 - Layouts
What 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.
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:
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/stores';
</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()}