What is +page.svelte?

The +page.svelte file is the heart of any route in SvelteKit. It defines what users see when they visit a URL. It’s a standard Svelte component with a special name that SvelteKit recognizes as a page.

One file, one responsibility

+page.svelte owns the visual layer for its route. Data loading belongs in +page.js or +page.server.js, server mutations belong in form actions, and shared utilities belong in $lib. When each file has one clear job, pages stay readable and refactoring becomes mechanical rather than risky.


Your First Page: The Homepage

The most important route in any application is the root route, or “homepage”. In SvelteKit, this corresponds to the root of your src/routes directory.

If you create a file at src/routes/+page.svelte, you are defining the content for https://your-site.com/.

<!-- src/routes/+page.svelte -->

<h1>Welcome to My Website</h1>
<p>This is the homepage.</p>

That’s it. Visit http://localhost:5173/ in your browser, and you’ll see this content. No configuration files, no route registration—just a file in the right place.


Adding More Pages

Let’s expand your application with additional pages.

About Page

In routes folder add new folder named about and inside it create +page.svelte:

<!-- src/routes/about/+page.svelte -->
<h1>About Us</h1>
<p>We are a company that builds amazing things.</p>

<h2>Our Mission</h2>
<p>To make the web a better place, one component at a time.</p>

Contact Page

Now create another folder named contact with its own +page.svelte:

<!-- src/routes/contact/+page.svelte -->
<h1>Contact Us</h1>

<form>
	<label>
		Name:
		<input type="text" name="name" />
	</label>

	<label>
		Email:
		<input type="email" name="email" />
	</label>

	<label>
		Message:
		<textarea name="message"></textarea>
	</label>

	<button type="submit">Send Message</button>
</form>

Directory Structure

Your project directory structure now looks like:

src/routes/
├── +page.svelte /
├── about/
   └── +page.svelte /about
└── contact/
    └── +page.svelte /contact

Each folder becomes a URL segment. Each +page.svelte defines what renders at that URL.


Pages Are Just Svelte Components

A +page.svelte file is a regular Svelte component. You can use everything you know about Svelte:

State with Runes

You can use Svelte 5’s reactivity system directly in your pages. The $state rune allows you to create local state that updates the UI when changed.

<!-- src/routes/counter/+page.svelte -->
<script lang="ts">
	let count = $state(0)
</script>

<h1>Counter</h1>
<p>Count: {count}</p>
<button onclick={() => count++}>Increment</button>
Svelte 5 Runes

For a deep dive into Svelte 5 runes and reactivity, check out our comprehensive Svelte 5 Runes guide.

Importing Components

Pages can import and use other Svelte components. This allows you to build complex UIs by composing smaller, reusable pieces.

lets create two components: Button.svelte and Hero.svelte.

<!-- src/lib/components/Button.svelte -->
<script lang="ts">
	// Svelte 5: Use $props() to receive props
	let { onclick, children } = $props()
</script>

<button {onclick}>
	{@render children?.()}
</button>

<style>
	button {
		background-color: var(--brand);
		color: white;
		border: none;
		padding: 0.5rem 1rem;
		border-radius: 0.25rem;
		cursor: pointer;
		font-size: 1rem;
	}
</style>
<!-- src/lib/components/Hero.svelte -->
<script lang="ts">
	// Svelte 5: Use $props() to receive props
	let { title, subtitle } = $props()
</script>

<section class="hero">
	<h1>{title}</h1>
	<p>{subtitle}</p>
</section>

<style>
	.hero {
		padding: 2rem;
		background-color: var(--brand-light);
		color: var(--brand-dark);
		border-radius: 0.5rem;
		text-align: center;
	}
</style>

Now you can use these components in your +page.svelte files:

<!-- src/routes/+page.svelte -->
<script lang="ts">
	import Button from '$lib/components/Button.svelte'
	import Hero from '$lib/components/Hero.svelte'
</script>

<Hero title="Welcome" subtitle="To my amazing website" />

<Button onclick={() => alert('Clicked!')}>Click me</Button>

Styling

Styles in +page.svelte are scoped to the component by default. This means you can write CSS without worrying about it affecting other parts of your application.

<!-- src/routes/about/+page.svelte -->
<h1>About Us</h1>
<p class="intro">We build amazing things.</p>

<style>
	h1 {
		color: var(--brand);
		font-size: 2.5rem;
	}

	.intro {
		font-size: 1.25rem;
		color: var(--text-dimmed);
	}
</style>

Lifecycle: Pages Mount and Unmount

Since pages are just Svelte components, they follow the normal Svelte 5 lifecycle. When you navigate between pages, the old page unmounts and the new page mounts. The idiomatic Svelte 5 way to respond to these events is $effect, which runs after the component mounts and can return an optional cleanup function that runs before it unmounts:

<!-- src/routes/+page.svelte -->
<script lang="ts">
	$effect(() => {
		console.log('Home page mounted')

		// Return a cleanup function that runs when the page unmounts
		return () => console.log('Home page unmounting')
	})
</script>

<h1>Home</h1>

This pattern is useful for cancelling subscriptions, clearing timers, or removing event listeners that were added when the page loaded. The onMount and onDestroy functions still work in Svelte 5, but $effect is the unified approach that handles both setup and teardown in a single function.

Because layouts persist across navigation between pages that share a layout, only the page component itself mounts and unmounts — the wrapping layout stays alive. This matters when you’re cleaning up resources: side effects in a page component are page-scoped, while side effects in a layout component live as long as the user remains within that section of the app.


Understanding Server-Side Rendering (SSR)

If you’ve built Single Page Applications (SPAs) before, you might be familiar with the “white screen of death.” This happens because the browser downloads an essentially empty HTML file and has to wait for a large JavaScript bundle to download, parse, and execute before showing anything to the user. On a slow connection or low-end device, this gap can be seconds long.

SvelteKit solves this with Server-Side Rendering (SSR). The concept is straightforward: instead of sending a blank page and letting JavaScript build the UI, the server runs your Svelte components first, produces complete HTML, and sends that to the browser. The user sees real content immediately, before a single byte of JavaScript has executed.

How SSR Works

Here is what happens when a user visits your /about page:

  1. The Request: The user’s browser asks the server for the /about page.
  2. The Server Works: SvelteKit runs your load functions to fetch any data, then executes your Svelte components on the server, producing a complete HTML snapshot of the page.
  3. The Delivery: The server sends this full HTML to the browser. The user sees the content (text, images, layout) immediately — no JavaScript required for this first paint.
  4. The Handover (Hydration): A moment later, the JavaScript bundle loads and Svelte “hydrates” the page — it attaches event listeners and makes the page interactive. This process is invisible to the user; the HTML they were already seeing just gains interactivity.
sequenceDiagram
    participant Browser
    participant Server
    participant Database

    Browser->>Server: GET /about
    Server->>Database: Fetch data
    Database->>Server: Return data
    Server->>Server: Render Svelte component
    Server->>Browser: Send complete HTML
    Note over Browser: User sees content immediately
    Browser->>Browser: Download JavaScript
    Browser->>Browser: Hydrate (attach events)
    Note over Browser: Page becomes interactive

Why SSR Matters

The benefits compound in ways that matter for real applications. Search engines can crawl your content because it arrives as HTML — no JavaScript execution required. Slow connections still deliver a usable page immediately rather than a spinner. Users with JavaScript disabled or blocked (corporate firewalls, privacy extensions, script errors) see your content rather than a blank page.

You don’t have to configure anything for this to work. SSR is the default behavior in SvelteKit, and it applies to every +page.svelte automatically.

See SSR in Action

Want proof that your code runs on both server and client? Add a console.log to any page:

<!-- src/routes/+page.svelte -->
<script lang="ts">
	console.log('Hello from the page!')
</script>

<h1>Home</h1>

Refresh the page and check both your terminal (server) and your browser console (client). You’ll see “Hello from the page!” in both places. This proves the component renders twice:

  1. First on the server (you see it in your terminal)
  2. Then on the client during hydration (you see it in browser DevTools)

This dual execution is fundamental to understanding SvelteKit!


Receiving Data from Load Functions

While +page.svelte can work entirely on its own, its real power comes when combined with data loading. When you place a +page.js or +page.server.js file in the same directory as +page.svelte, SvelteKit automatically connects them: it runs the load function first, waits for it to finish, then passes its return value to the page component as the data prop.

The connection is made by naming convention — both files live in the same route directory, and SvelteKit knows they belong together. You don’t import one from the other. There’s no explicit wiring. The fact that +page.ts and +page.svelte share the same folder is enough:

src/routes/blog/
├── +page.ts       ← SvelteKit runs this, gets { posts: [...] }
└── +page.svelte   ← SvelteKit passes { posts: [...] } here as data
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
	import type { PageProps } from './$types'

	// data is everything returned from +page.ts — typed automatically
	let { data }: PageProps = $props()
</script>

<h1>Blog</h1>

{#each data.posts as post}
	<article>
		<h2>{post.title}</h2>
		<p>{post.excerpt}</p>
	</article>
{/each}

If you add a +page.js but access a field that it doesn’t return, TypeScript tells you immediately — there’s no runtime surprise. This is covered in depth in +page.js (for public data) and +page.server.js (for database queries and secrets).

Handling Form Actions

In addition to data, +page.svelte can also receive a form prop. This is used when you submit a form to a server action (defined in +page.server.js).

<script lang="ts">
	import type { PageProps } from './$types'

	let { data, form }: PageProps = $props()
</script>

{#if form?.success}
	<p class="success">Form submitted successfully!</p>
{/if}

<form method="POST">
	<!-- form fields -->
</form>

We’ll explore form actions in depth when we cover +page.server.js.


Managing the Head

<svelte:head>

Each page often needs its own title and meta tags for SEO. Svelte provides a special element <svelte:head> that allows you to insert elements into the <head> of your document.

<!-- src/routes/about/+page.svelte -->
<svelte:head>
	<title>About Us | My Website</title>
	<meta name="description" content="Learn more about our company and mission." />
</svelte:head>

<h1>About Us</h1>
Head Management

<svelte:head> content from nested routes is combined with parent layouts. The most specific (deepest) <title> wins, while other tags are merged.

Preserving State

Snapshot

Sometimes you want to preserve ephemeral state — like the text in a search input — when the user navigates away and then clicks “back”. SvelteKit makes this easy with snapshots. You define how to capture the state on the way out and how to restore it on the way back.

In Svelte 5, snapshot is exported from the regular instance <script> using export const. This is one of the non-prop exports that Svelte 5 still supports — export let for props was replaced by $props(), but export const for exposing values on the component instance remains valid. The capture and restore functions close over the reactive search state in the same script block:

<script lang="ts">
	import type { Snapshot } from './$types'

	let search = $state('')

	export const snapshot: Snapshot<string> = {
		capture: () => search,
		restore: (value) => {
			search = value
		}
	}
</script>

<input bind:value={search} placeholder="Search..." />

When capture() is called (on navigation away), it reads the current value of search. When restore(value) is called (on back-navigation), it writes back to search, triggering Svelte’s reactivity and updating the input. The closure over $state means this works correctly with Svelte 5’s fine-grained reactivity.

Snapshots

Snapshots are covered in detail in our Navigation article, including advanced patterns and use cases.

Data Preloading: Instant Navigation

Notice how navigation between pages feels instant? That’s because SvelteKit preloads data when you hover over links.

In your src/app.html, you’ll see:

<body data-sveltekit-preload-data="hover"></body>

This means when your mouse hovers over a link, SvelteKit starts fetching the data for that page before you click. By the time you click, the data is already loaded!

You can control this behavior:

  • "hover" — Preload on mouse hover (default, great for desktop)
  • "tap" — Preload on touchstart/mousedown (better for mobile)
  • "off" — No preloading

Common Patterns

In this section, we’ll explore some common patterns you’ll use in +page.svelte files.

Conditional Rendering Based on Data

We often want to show different content based on the data we receive. For example, displaying a list of posts or a message if there are none.

<script lang="ts">
	import type { PageProps } from './$types'

	let { data }: PageProps = $props()
</script>

{#if data.posts.length > 0}
	<ul>
		{#each data.posts as post}
			<li>{post.title}</li>
		{/each}
	</ul>
{:else}
	<p>No posts yet. Check back soon!</p>
{/if}

Accessing Global Page State

Sometimes you need access to the current URL, parameters, or user data from anywhere in your component tree (not just in +page.svelte). SvelteKit provides the page object for this.

<script lang="ts">
	import { page } from '$app/state'
</script>

<p>Current path: {page.url.pathname}</p>

{#if page.data.user}
	<p>Welcome back, {page.data.user.name}!</p>
{/if}

Nested Routes with Shared Layout

Pages can be nested inside folders to create URL paths. You can also use +layout.svelte files to share UI (like headers or sidebars) across multiple pages in a directory.

src/routes/
├── blog/
   ├── +layout.svelte Wraps all blog pages
   ├── +page.svelte /blog (list of posts)
   └── [slug]/
       └── +page.svelte /blog/my-post (individual post)

Learn more in our +layout.svelte guide.


Common Mistakes

1. Missing the + Prefix

src/routes/
├── page.svelte          WRONG: Ignored by SvelteKit
└── +page.svelte          CORRECT: This is your page

2. Expecting Data Without a Load Function

<!-- AVOID: data will be empty if there's no +page.ts! -->
<script lang="ts">
	let { data } = $props()
</script>

<p>{data.title}</p> <!-- Error: title is undefined -->

If you need data, create a +page.js or +page.server.js file (or their .ts equivalents if you prefer TypeScript).

3. Using Browser APIs During SSR

During SSR (Server-Side Rendering), your code runs in a Node.js environment where browser-specific objects like window, document, or localStorage do not exist. If you try to access them directly, you’ll get a runtime error on the server.

To safely use browser APIs, always check if you’re running in the browser using the browser boolean from $app/environment:

<script lang="ts">
	import { browser } from '$app/environment'

	// AVOID: window doesn't exist on the server!
	// const width = window.innerWidth

	// PREFERRED: Check for browser environment
	const width = browser ? window.innerWidth : 0
</script>

Conclusion

The +page.svelte file is where routing meets UI—the component that actually renders what users see when they visit a URL. As a standard Svelte component with full access to runes, imports, styles, and all of Svelte’s features, it represents the endpoint of SvelteKit’s routing system where data becomes interface. Whether receiving data from load functions, responding to user interactions, or rendering dynamic content, page components are where your application’s functionality becomes tangible.

Mastering +page.svelte means understanding its dual nature: during SSR it renders on the server to provide fast initial loads, then hydrates in the browser to become a fully interactive application. By properly handling data props with $props(), leveraging layout hierarchy for shared UI, and using the browser constant for environment-specific logic, you build pages that work everywhere while feeling fast and responsive. The + prefix convention and automatic prop passing from load functions make the connection between routing, data, and UI seamless and intuitive.

Key Takeaways

  • +page.svelte defines what renders at a route - it’s a standard Svelte component that becomes the page content users see
  • Receives data via $props()let { data }: PageProps = $props() is automatically populated from +page.js or +page.server.js load functions without any extra wiring
  • SSR enabled by default - pages render on the server first for fast initial loads, then hydrate to interactive components in the browser
  • Full Svelte component capabilities - use runes ($state, $derived, $effect), imports, styles, and all Svelte features
  • The + prefix is mandatory - SvelteKit only recognizes files starting with + as special route files
  • Use browser constant for environment checks - from $app/environment to conditionally run browser-only code safely during SSR
  • Wrapped by layout hierarchy - renders inside parent +layout.svelte components, inheriting shared UI structure
  • Automatic client-side navigation - when users navigate between pages, SvelteKit swaps page components without full page reloads

See Also