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.svelteowns the visual layer for its route. Data loading belongs in+page.jsor+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 RunesFor 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:
- The Request: The user’s browser asks the server for the
/aboutpage. - 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.
- 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.
- 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:
- First on the server (you see it in your terminal)
- 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.
SnapshotsSnapshots 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.sveltedefines 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.jsor+page.server.jsload 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
browserconstant for environment checks - from$app/environmentto conditionally run browser-only code safely during SSR - Wrapped by layout hierarchy - renders inside parent
+layout.sveltecomponents, inheriting shared UI structure - Automatic client-side navigation - when users navigate between pages, SvelteKit swaps page components without full page reloads
See Also
- Official SvelteKit Documentation - +page.svelte
- Svelte 5 Runes - Reactivity with
$state,$derived,$effect - SSR and Hydration — Server-side rendering concepts
browserconstant — Environment detection