Shared UI with +layout.svelte

In any multi-page application, many routes share structural elements: a navigation bar, a sidebar, a footer, a banner showing the logged-in user. The naive approach is to import and render these in every +page.svelte file. That works until you need to change something — then you’re touching dozens of files for what should be a one-line edit.

SvelteKit solves this with layouts — components that wrap their descendant routes with shared UI. A layout runs once when a user enters its subtree and stays mounted as they navigate between child routes. Pages come and go inside it; the layout stays.

Layouts are worth planning

A well-designed layout hierarchy can eliminate hundreds of lines of duplicate code and make global UI changes trivial. A poorly designed one forces awkward workarounds. Spend a few minutes mapping out your layout structure before building — it pays dividends.


The Root Layout

The most important layout is the root layout. It lives at the top level of your routes directory and wraps every single page in your application.

src/routes/
├── +layout.svelte Root layout (wraps everything)
├── +page.svelte Homepage
└── about/
    └── +page.svelte About page

Think of it as the shell of your website — the structure that never changes while the page content swaps out inside it.

<!-- src/lib/components/Navigation.svelte -->
<nav>
	<a href="/">Home</a>
	<a href="/about">About</a>
	<a href="/blog">Blog</a>
	<a href="/contact">Contact</a>
</nav>
<!-- src/routes/+layout.svelte -->
<script lang="ts">
	import Navigation from '$lib/components/Navigation.svelte'

	let { children } = $props()
</script>

<div class="app">
	<header>
		<Navigation />
	</header>

	<main>
		{@render children()}
	</main>

	<footer>
		<p>&copy; 2025 My Amazing Website</p>
	</footer>
</div>

<style>
	.app {
		display: flex;
		flex-direction: column;
		min-height: 100vh;
	}

	main {
		flex: 1;
		padding: 2rem;
	}
</style>

The children Snippet

If you’re coming from Svelte 4, you might be looking for <slot />. In Svelte 5, slots were replaced by snippets.

The children prop is a special snippet that represents the content “inside” the layout — the page component or nested layout that occupies this route. You render it with {@render children()}.

This is the most critical rule: you must always render children(). If you forget, no page content will ever appear:

<!-- WRONG: Pages will never show -->
<script lang="ts">
  let { children } = $props()
</script>
<nav>Navigation</nav>
<footer>Footer</footer>

<!-- CORRECT: Always render children -->
<script lang="ts">
  let { children } = $props()
</script>
<nav>Navigation</nav>
<main>{@render children()}</main>
<footer>Footer</footer>

Nested Layouts

Layouts can be nested. A layout inside a subdirectory wraps pages in that directory, but is itself wrapped by the parent layout above it.

Example: Blog Section Layout

Imagine a blog section that needs a sidebar, but the homepage doesn’t. Create a +layout.svelte inside the blog directory:

src/routes/
├── +layout.svelte Root layout (wraps everything)
├── +page.svelte Homepage (no sidebar)
└── blog/
    ├── +layout.svelte Blog layout (adds sidebar)
    ├── +page.svelte Blog index
    └── [slug]/
        └── +page.svelte Individual blog post
<!-- src/routes/blog/+layout.svelte -->
<script lang="ts">
	let { children } = $props()
</script>

<div class="blog-layout">
	<aside>
		<h3>Categories</h3>
		<ul>
			<li><a href="/blog?category=tech">Tech</a></li>
			<li><a href="/blog?category=life">Life</a></li>
			<li><a href="/blog?category=tutorials">Tutorials</a></li>
		</ul>
	</aside>

	<div class="content">
		{@render children()}
	</div>
</div>

<style>
	.blog-layout {
		display: grid;
		grid-template-columns: 250px 1fr;
		gap: 2rem;
	}
</style>

Visualizing the Hierarchy

When a user visits /blog/my-first-post, SvelteKit constructs the page as a stack of nested components, from the outside in:

Root Layout (+layout.svelte)
└── Blog Layout (blog/+layout.svelte)
    └── Blog Post Page (blog/[slug]/+page.svelte)

The resulting HTML structure reflects this nesting exactly:

<!-- From Root Layout -->
<div class="app">
	<header>...</header>
	<main>
		<!-- From Blog Layout -->
		<div class="blog-layout">
			<aside>...</aside>
			<div class="content">
				<!-- From Page -->
				<article>...</article>
			</div>
		</div>
	</main>
	<footer>...</footer>
</div>

Receiving Data in Layouts

Layouts can receive data from load functions, just like pages. The data comes from +layout.js or +layout.server.js in the same directory and is available alongside children in $props():

<!-- src/routes/+layout.svelte -->
<script lang="ts">
	import type { LayoutProps } from './$types'

	let { data, children }: LayoutProps = $props()
</script>

<header>
	{#if data.user}
		<span>Welcome, {data.user.name}</span>
		<a href="/logout">Logout</a>
	{:else}
		<a href="/login">Login</a>
	{/if}
</header>

<main>
	{@render children()}
</main>

We’ll cover layout data loading in the next articles: +layout.js and +layout.server.js.


Layout Groups

As your application grows, you’ll hit a common need: different sections require completely different layouts, but you don’t want the section name to appear in the URL. A marketing homepage and a user dashboard both live at the top level — / and /dashboard — but they look nothing alike. The homepage has a hero image and a big CTA button; the dashboard has a sidebar navigation bar.

One root layout can’t serve both without becoming a conditional mess. But without some organizational tool, you’d have to use URL prefixes like /marketing/ and /app/ just to get separate layout hierarchies.

SvelteKit’s solution: layout groups. Wrap a directory name in parentheses and it creates a separate layout hierarchy without adding anything to the URL. The (marketing) folder exists for SvelteKit’s routing logic only — users never see it in their address bar.

src/routes/
├── (marketing)/
   ├── +layout.svelte Marketing layout (big headers, CTAs)
   ├── +page.svelte / (homepage)
   ├── about/
   └── +page.svelte /about
   └── pricing/
       └── +page.svelte /pricing
├── (app)/
   ├── +layout.svelte App layout (sidebar, compact)
   ├── dashboard/
   └── +page.svelte /dashboard
   └── settings/
       └── +page.svelte /settings
└── +layout.svelte Root layout (wraps both groups)

The parentheses tell SvelteKit: use this for organization, not for routing. A visitor sees /about, not /(marketing)/about.

Layout Groups for Organization

Layout groups are perfect for maintaining drastically different designs within the same app — like a public marketing site and a private dashboard — without URL clutter. They also let you apply authentication protection to an entire section with a single +layout.server.js.

Error Isolation: A Hidden Superpower

One often-overlooked benefit of nested layouts is error isolation. When something goes wrong in a nested route, SvelteKit finds the nearest +error.svelte and renders the error there — the parent layouts stay alive.

<!-- src/routes/dashboard/widget/+error.svelte -->
<script lang="ts">
	import { page } from '$app/state'
</script>

<div class="error-container">
	<h2>Widget Error</h2>
	<p>Something went wrong loading this widget, but the rest of your dashboard still works!</p>
	<pre>{JSON.stringify(page.error, null, 2)}</pre>
</div>

A failing dashboard widget doesn’t take down the sidebar or the navigation. Users can keep working while you fix the issue.


Breaking Out of Layouts

Sometimes a page needs to step outside its layout hierarchy. The classic example: a report that needs to be embedded in an iframe should render without any navigation chrome.

Use the @ syntax to reset to a specific layout ancestor:

src/routes/
├── +layout.svelte Root layout
├── (app)/
   ├── +layout.svelte App layout
   └── dashboard/
       └── reports/
           ├── +page.svelte Uses full app layout
           └── [id]/
               └── embed/
                   └── +page@.svelte Resets to root layout only
  • +page@.svelte — Renders inside root layout only (skips all intermediate layouts)
  • +page@(app).svelte — Renders inside the (app) group layout (skips deeper layouts)

The embed page shows the report content without any dashboard chrome — perfect for iframes. The same data, a completely different presentation context.


Layout Persistence

One of the most important things to understand about layouts is that they do not remount during navigation between sibling routes. When a user clicks from /blog/post-a to /blog/post-b, the blog layout stays alive — SvelteKit swaps only the +page.svelte content inside it.

This is why layout state survives page transitions within a section. A sidebar scroll position, an open/closed toggle, an animation in progress — all of these persist because the layout component never unmounts:

<!-- src/routes/blog/+layout.svelte -->
<script lang="ts">
	let { children } = $props()

	// Persists when navigating between blog posts.
	// Resets only when leaving /blog entirely.
	let sidebarOpen = $state(true)
</script>

<div class="blog-layout">
	<button onclick={() => (sidebarOpen = !sidebarOpen)}> Toggle Sidebar </button>

	{#if sidebarOpen}
		<aside>Sidebar content</aside>
	{/if}

	<main>
		{@render children()}
	</main>
</div>

The layout remounts only when the user navigates to a route outside its subtree — for example, leaving /blog entirely and going to /about. This means expensive layout setup (fetching navigation categories, initialising libraries, establishing WebSocket connections) happens once per section visit, not once per page.


Common Patterns

Conditional Layout Content

Use the page object from $app/state to conditionally render UI based on the current route:

<script lang="ts">
	import { page } from '$app/state'
	let { children } = $props()
</script>

<header>
	<nav>
		<a href="/">Home</a>
		{#if page.url.pathname.startsWith('/admin')}
			<a href="/admin/users">Users</a>
			<a href="/admin/settings">Settings</a>
		{/if}
	</nav>
</header>

{@render children()}

Show a progress bar during navigation. In Svelte 5, navigating comes from $app/state and is accessed directly — no $ prefix, not a store:

<script lang="ts">
	import { navigating } from '$app/state'
	let { children } = $props()
</script>

{#if navigating}
	<div class="loading-bar"></div>
{/if}

{@render children()}

<style>
	.loading-bar {
		position: fixed;
		top: 0;
		left: 0;
		right: 0;
		height: 3px;
		background: var(--brand);
		animation: loading 1s ease-in-out infinite;
	}

	@keyframes loading {
		0% {
			transform: translateX(-100%);
		}
		100% {
			transform: translateX(100%);
		}
	}
</style>

Conclusion

The +layout.svelte file represents SvelteKit’s solution to one of web development’s most persistent challenges: sharing UI structure without repetition or complexity. By defining layouts as nested components that wrap their descendants, SvelteKit creates a natural hierarchy where shared concerns live at appropriate levels — global navigation in the root, section-specific sidebars in nested layouts, and page-unique content in pages themselves.

The {@render children()} syntax is the mechanism that composes your entire application — every route renders inside the chain of layout wrappers above it, and layouts persist across navigation within their subtree so expensive setup happens only once.

Combined with layout groups for different site sections, error boundaries that contain failures without taking down the whole UI, and the ability to reset layouts when a page truly needs to stand alone, this system provides the flexibility to build everything from simple blogs to complex applications while staying firmly within the framework’s conventions.

Key Takeaways

  • +layout.svelte wraps all descendant routes — shared UI that persists across navigation, with nested layouts forming a composable component hierarchy
  • {@render children()} is mandatory — it renders the page component or nested layout that occupies this route; omitting it makes all pages invisible
  • Root layout wraps everythingsrc/routes/+layout.svelte is the HTML shell for the entire app; errors in its load function fall back to src/error.html
  • Layouts receive data via $props() in Svelte 5, populated by +layout.js or +layout.server.js — both data and children come from $props()
  • Layouts persist within their subtree — they only remount when the user navigates outside their route scope, preserving state and avoiding redundant setup
  • Layout groups organize routes without URL pollution(authenticated) and (marketing) folders create separate layout hierarchies; the parentheses are invisible in the final URL
  • navigating from $app/state is a plain reactive value (not a store) — use it without a $ prefix to show loading indicators during navigation
  • Break layout inheritance with @ reset+page@.svelte renders under the root layout only, skipping all intermediate layouts when a page needs to stand alone

See Also