What the page Object Is
The previous two articles established that load functions return data before a page renders, and that layout load functions share that data across every child route in a segment. What they left open was a practical question that comes up immediately in real applications: once a page is rendered, how does a component that is not the page component itself read the current route, the current URL, or the current user?
SvelteKit answers this with the page object, exported from $app/state. It is a reactive store that reflects the current state of the application’s navigation at all times. Any component in the tree can import it and read from it, without any prop-drilling and without subscribing to a custom store. When the user navigates to a different route, every value in the page object updates automatically, and any component that reads those values re-renders accordingly.
Understanding the page object is not optional knowledge. It appears in navigation components that highlight the active link, in root layouts that set the document title dynamically, in error boundaries that read page.status, and in forms that read page.form for server-side action results. This article is a complete reference for what it contains, how its reactivity model works, and the situations where each property is useful.
Why the page Object Exists
Consider a standard navigation bar. It renders at the root layout level, wrapping every page in the application. Each nav link should appear highlighted when the user is on its corresponding route. The nav component does not receive any props; it has no natural channel to know which route is currently active.
Without page, the options are limited. You could pass the current URL down as a prop from the root layout, which means threading it through every intermediate component. You could create a Svelte context that the root layout writes to and the nav reads from, which is the right pattern for some use cases but adds boilerplate for something the framework already knows. You could derive the active route from the browser’s location object, but that does not work during SSR because there is no browser location on the server.
The page object solves all three problems at once. It is available everywhere, works on the server during SSR with realistic values derived from the incoming HTTP request, and updates reactively whenever the route changes.
<!-- src/lib/components/Nav.svelte -->
<!-- This works everywhere: SSR, client-side navigation, any component -->
<script lang="ts">
import { page } from '$app/state'
const links = [
{ href: '/', label: 'Home' },
{ href: '/blog', label: 'Blog' },
{ href: '/about', label: 'About' }
]
</script>
<nav>
{#each links as link}
<a href={link.href} class:active={page.url.pathname === link.href}>
{link.label}
</a>
{/each}
</nav>
<style>
.active {
font-weight: 600;
border-bottom: 2px solid currentColor;
}
</style> The class:active directive re-evaluates every time page.url.pathname changes. No subscription, no cleanup, no manual store handling. The reactivity is implicit, exactly as it is with $state in Svelte 5 component code.
How page Reactivity Works
The page object from $app/state is a plain reactive object backed by Svelte 5’s fine-grained reactivity system. Reading any property of it during a component’s render creates a dependency on that property. When navigation occurs and SvelteKit updates the values inside page, only the components that read the changed properties re-render.
This is distinct from $app/stores, the older Svelte 4 API, which exposed page as a Svelte store that required explicit subscription with $page. The $app/state version works without any subscription syntax because it is built on runes. If you are reading documentation or code that uses $page with a dollar-sign prefix and imports from $app/stores, that is the older pattern. This series uses $app/state exclusively.
During server-side rendering, the page object is populated from the incoming HTTP request. page.url reflects the request URL, page.params reflects the resolved route parameters, and page.data reflects the merged output of all load functions that ran for that request. The component renders once, using that snapshot of state, and the result is sent as HTML. When the browser takes over and hydrates the page, the page object is initialised with the same values and the component behaviour remains consistent.
During client-side navigation, SvelteKit updates the page object before re-rendering affected components. The sequence is: navigation triggered, load functions run for the new route, page updated with new values, affected components re-render. Components do not receive explicit notification that navigation happened; they simply read from page and Svelte’s reactivity system handles the rest.
Every Property Explained
page.url
page.url is a standard Web API URL object representing the current page’s full URL. It exposes all the properties you would expect from a URL: pathname, search, searchParams, hash, origin, href, and the rest.
<!-- src/lib/components/Breadcrumb.svelte -->
<script lang="ts">
import { page } from '$app/state'
let segments = $derived(
page.url.pathname
.split('/')
.filter(Boolean)
.map((segment, i, arr) => ({
label: segment,
href: '/' + arr.slice(0, i + 1).join('/')
}))
)
</script>
<nav aria-label="Breadcrumb">
<a href="/">Home</a>
{#each segments as segment}
<span aria-hidden="true"> / </span>
<a href={segment.href}>{segment.label}</a>
{/each}
</nav> One important server-side caveat: page.url.hash is always an empty string during SSR. The browser strips the hash fragment before sending the HTTP request, so the server never sees it. Any logic that depends on the hash must run only in the browser. The correct pattern is to check page.url.hash inside $effect() or inside an event handler, never during component initialisation that runs during SSR.
page.url.searchParams is a URLSearchParams instance. It is read-only through page.url; to change search params you navigate programmatically using SvelteKit’s goto() function from $app/navigation. Reading from it in a $derived creates a dependency that causes the derived value to update whenever the search params change, which is the correct pattern for things like reactive filtering UI.
page.params
page.params is a plain object whose keys correspond to the dynamic segments in the active route’s directory name. For a route at src/routes/shop/[category]/[productId]/+page.svelte, the object will have category and productId as string-typed keys.
<!-- src/routes/shop/[category]/[productId]/+page.svelte -->
<script lang="ts">
import { page } from '$app/state'
import type { PageProps } from './$types'
let { data }: PageProps = $props()
// page.params mirrors what was available in the load function
let breadcrumb = $derived(`${page.params.category} / ${page.params.productId}`)
</script>
<p>Viewing: {breadcrumb}</p><h1>{data.product.name}</h1> In most cases, page.params and data from the load function are the right places to get this information, and they contain the same param values. The difference is that data requires a load function to exist and return something, whereas page.params is always available regardless of whether there is a load function.
page.route
page.route is an object with a single property: id. The id is the route’s pattern as a string in the bracketed notation that mirrors the directory structure, for example /blog/[slug] or /shop/[category]/[productId]. Crucially, it is the pattern rather than the resolved value. Two different URLs like /blog/introduction-to-svelte and /blog/advanced-runes both produce a route.id of /blog/[slug].
This makes page.route.id useful for logic that needs to know which route template is active, regardless of the specific values. Analytics code that tracks page-view events by route type, conditional rendering that differs between routes in a section, and logging middleware that classifies requests all benefit from the route pattern rather than the resolved URL.
<!-- src/lib/components/Nav.svelte -->
<script lang="ts">
import { page } from '$app/state'
// Highlight the Blog section for any route under /blog
let inBlogSection = $derived(page.route.id?.startsWith('/blog') ?? false)
</script>
<nav>
<a href="/" class:active={page.route.id === '/'}>Home</a>
<a href="/blog" class:active={inBlogSection}>Blog</a>
<a href="/about" class:active={page.route.id === '/about'}>About</a>
</nav> page.data
page.data is the merged output of all active load functions for the current route. It includes data from the root layout load, any intermediate layout loads, and the page load itself. It is the same object that +page.svelte receives through its data prop, but accessible from anywhere, including from parent layouts that need to read data from a child page they are wrapping.
The canonical use case is a root layout setting the document <title> dynamically based on what the current page’s load function returned:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { page } from '$app/state'
let { children } = $props()
</script>
<svelte:head>
<title>{page.data.title ? `${page.data.title} | My Site` : 'My Site'}</title>
<meta name="description" content={page.data.description ?? 'A great website.'} />
</svelte:head>
{@render children()} // src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`)
const post = await res.json()
return {
post,
title: post.title,
description: post.excerpt
}
} The root layout has no way to receive title and description through the normal data prop, because data flows downward and layouts only see their own load function’s return value merged with their ancestors. page.data is the clean, framework-supported way to flow information upward from a page to a parent layout.
The TypeScript type for page.data is determined by App.PageData in your application’s type declarations. SvelteKit does not automatically infer this as a union of all possible page data shapes, because at runtime the root layout simply does not know which page is active. You typically declare App.PageData with optional properties that any page might provide, and handle their absence with nullish coalescing as shown above.
page.status
page.status is the HTTP status code of the current response as a number. For a normally rendered page it is 200. For a page rendered because a load function called error(404, 'Not found'), it is 404. For a redirect, the page at the redirect destination will have a 200 status; the redirect response itself is not a page render.
page.status is most useful in the +error.svelte component, where it lets you conditionally render different messaging for different error types without duplicating the error page into multiple files:
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/state'
</script>
{#if page.status === 404}
<h1>Page not found</h1>
<p>The page you were looking for does not exist.</p>
<a href="/">Back to home</a>
{:else if page.status === 403}
<h1>Access denied</h1>
<p>You do not have permission to view this page.</p>
{:else}
<h1>Something went wrong</h1>
<p>An unexpected error occurred. Please try again later.</p>
<p class="code">Error {page.status}</p>
{/if} You can also read page.status from regular page components and layouts, which is less common but occasionally useful for UI that should behave differently on error states.
page.error
page.error is populated when SvelteKit is rendering an error page because of a thrown error() call or an unexpected exception. It is an object with a message string. For expected errors thrown with error(status, message), it will have the message you provided. For unexpected errors, it will contain the message from your handleError hook, or a generic fallback.
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/state'
</script>
<h1>Error {page.status}</h1><p>{page.error?.message}</p> Outside of error pages, page.error is null. There is no need to guard against it in normal page components.
page.form
page.form is the result returned by the most recently executed form action. It is populated after a form submission to a +page.server.ts action function and contains whatever that action returned. When no form action has run, or when the user navigates away and back, page.form is null.
This property is how progressively enhanced forms surface server-side validation results to the component without a page reload:
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { page } from '$app/state'
import type { PageProps } from './$types'
let { data }: PageProps = $props()
</script>
<form method="POST">
<label>
Name
<input name="name" type="text" value={page.form?.name ?? ''} />
{#if page.form?.errors?.name}
<span class="error">{page.form.errors.name}</span>
{/if}
</label>
<label>
Email
<input name="email" type="email" value={page.form?.email ?? ''} />
{#if page.form?.errors?.email}
<span class="error">{page.form.errors.email}</span>
{/if}
</label>
<button type="submit">Send</button>
{#if page.form?.success}
<p class="success">Message sent successfully.</p>
{/if}
</form> // src/routes/contact/+page.server.ts
import type { Actions } from './$types'
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData()
const name = String(data.get('name') ?? '')
const email = String(data.get('email') ?? '')
const errors: Record<string, string> = {}
if (!name.trim()) errors.name = 'Name is required'
if (!email.includes('@')) errors.email = 'Valid email required'
if (Object.keys(errors).length > 0) {
return { success: false, name, email, errors }
}
// process the form...
return { success: true }
}
} The action returns a plain object. SvelteKit makes that object available as page.form in the page component. If the user refreshes the page, page.form resets to null. If JavaScript is disabled, the same action runs and SvelteKit re-renders the page server-side with the action result, making the validation messages appear without any client-side JavaScript.
page.state
page.state is the value of the current history entry’s state object, set using SvelteKit’s pushState() or replaceState() from $app/navigation. This is a shallow routing mechanism: it lets you update what a component renders, and optionally what URL appears in the browser’s address bar, without triggering a full navigation and without running load functions again.
<!-- src/routes/gallery/+page.svelte -->
<script lang="ts">
import { page } from '$app/state'
import { pushState } from '$app/navigation'
import type { PageProps } from './$types'
let { data }: PageProps = $props()
function openModal(imageId: string) {
pushState('', { selectedImageId: imageId })
}
function closeModal() {
history.back()
}
</script>
<div class="gallery">
{#each data.images as image (image.id)}
<button onclick={() => openModal(image.id)}>
<img src={image.thumbnailUrl} alt={image.alt} />
</button>
{/each}
</div>
{#if page.state.selectedImageId}
{@const selected = data.images.find((i) => i.id === page.state.selectedImageId)}
{#if selected}
<div class="modal" role="dialog">
<button class="close" onclick={closeModal}>Close</button>
<img src={selected.fullUrl} alt={selected.alt} />
</div>
{/if}
{/if} When pushState is called with a state object, SvelteKit updates page.state reactively. The modal appears. When the user clicks the browser Back button, the history entry changes and page.state returns to its previous value, so the modal disappears. This pattern is the correct way to implement modals, drawers, and other overlays that should be dismissible with the browser’s back button, without full page navigation overhead.
Common Mistakes and Anti-Patterns
Importing from $app/stores Instead of $app/state
Svelte 4 exposed the page object through $app/stores as a Svelte store, requiring import { page } from '$app/stores' and $page (with the auto-subscription prefix) to read values inside a component.
<!-- AVOID: Svelte 4 pattern -->
<script lang="ts">
import { page } from '$app/stores'
</script>
<p>Path: {$page.url.pathname}</p> <!-- PREFERRED: Svelte 5 / $app/state pattern -->
<script lang="ts">
import { page } from '$app/state'
</script>
<p>Path: {page.url.pathname}</p> The $app/stores API still works in SvelteKit for backward compatibility, but it requires the store subscription syntax that is inconsistent with Svelte 5’s runes model. Use $app/state in all new code.
Using page.url.hash During SSR
page.url.hash is always an empty string on the server because HTTP requests do not include hash fragments. Reading it during SSR and expecting a meaningful value causes silent failures that are only apparent when testing in a real browser.
<!-- AVOID: Assumes hash is available during SSR -->
<script lang="ts">
import { page } from '$app/state'
// This will always be '' on the server, causing a flash
let activeSection = $state(page.url.hash.slice(1) || 'overview')
</script> <!-- PREFERRED: Read hash only in the browser -->
<script lang="ts">
import { page } from '$app/state'
let activeSection = $state('overview')
$effect(() => {
if (page.url.hash) {
activeSection = page.url.hash.slice(1)
}
})
</script> The $effect runs only in the browser, so the hash is always available when the code executes.
Comparing page.route.id Without Checking for null
page.route.id can be null on error pages, because errors can occur before a route is fully matched. Always guard against null when using it in logic.
<!-- AVOID: Throws if route.id is null on an error page -->
<script lang="ts">
import { page } from '$app/state'
let inBlog = $derived(page.route.id.startsWith('/blog'))
</script> <!-- PREFERRED: Safe null check -->
<script lang="ts">
import { page } from '$app/state'
let inBlog = $derived(page.route.id?.startsWith('/blog') ?? false)
</script> Performance and Scaling Considerations
Because page is a fine-grained reactive object, reading only the properties you need keeps components efficient. A navigation component that reads page.url.pathname only re-renders when the pathname changes. It does not re-render when page.data changes, or when a form action populates page.form. This precision is free; the reactivity system tracks property access automatically during render.
page.data deserves particular care in root layout components. Because page.data reflects the merged data of any currently active page, reading it broadly in a root layout creates a dependency that causes the layout to re-render whenever any page anywhere in the application changes its data. Prefer to read only the specific keys you need: page.data.title rather than a destructured spread of the whole object.
For page.form, note that the form action result is serialised and sent as part of the navigation response. Large objects returned from form actions become part of the page’s data payload. Return only the minimum necessary for the form’s UI, particularly field values and error messages; avoid returning large datasets from form actions.
When NOT to Use page
page from $app/state is designed for components that need to read current route state reactively. It is not a substitute for load function data.
Data that requires server-side processing, database access, authentication, or transformation before it is safe to expose should go through a load function and arrive as data through $props(). Fetching inside a component using page.url as the fetch parameter is the same anti-pattern as fetching inside a component in general: it runs after the component mounts, it does not benefit from SSR, and it creates a loading flash.
Similarly, page.params is useful for reading param values in a component that is not the page component itself. If you are inside +page.svelte and want the params, reading them from page.params and reading them from the load function return are equally correct, but using the typed data prop is usually cleaner because the type is precise and the value has often already been validated by the load function.
Conclusion
The page object from $app/state is the bridge between SvelteKit’s route system and your component tree. Every component in the application can read the current URL, the current route pattern, the current page’s data, the current form action result, and the current HTTP status from it, without props, without stores, and without manual subscriptions. It updates reactively on every navigation, and it is populated with meaningful values during SSR so there are no environment inconsistencies.
Understanding which property serves each use case saves a significant amount of architectural work. page.url.pathname for active-link detection. page.route.id for section-level logic. page.data for data flowing upward to parent layouts. page.form for progressive form enhancement. page.state for shallow routing with back-button support.
Key Takeaways
page is imported from $app/state and reflects the current navigation state as a fine-grained reactive object. In Svelte 5 SvelteKit applications, always use $app/state rather than $app/stores.
page.url is a URL object. page.url.hash is always empty on the server; read it only inside $effect. page.params mirrors what the load function receives. page.route.id is the route pattern, not the resolved URL; guard it for null on error pages.
page.data contains the merged output of all active load functions. It is the correct way to read page-level data from a parent layout, for instance to set the document <title>. page.form holds the most recent form action result and resets to null on navigation. page.state reflects history entry state set with pushState or replaceState.
What’s Next
With the page object understood, you have a complete picture of how load function output reaches components and how the application’s navigation state is exposed reactively. The next major decision in SvelteKit architecture is understanding which of the two load function file types to use for a given situation. The next article, Universal vs Server Load Functions, covers the distinction in full: where each runs, what each can access, and how to combine them when a route needs both.
Further Reading
- $app/state Documentation — official reference for the full
pageobject API including all properties and their types - Official SvelteKit Load Documentation — covers how load functions produce the data that
page.datareflects - SvelteKit Form Actions Documentation — how form actions work and produce the result that appears in
page.form - SvelteKit Shallow Routing —
pushState,replaceState, and thepage.statepatterns covered in this article