Svelte 5 & SvelteKit Reference: Runes, Stores, Routing, Form Actions & Adapters
Svelte 5 compiles away the framework — no virtual DOM, no runtime diffing. The new Runes API ($state, $derived, $effect) replaces the old reactive let declarations and $: labels. SvelteKit is the full-stack layer: filesystem routing, SSR/SSG, server actions, and load functions. The mental model shift: Svelte is a compiler that turns your components into vanilla JS; SvelteKit is a framework built on Vite + that compiler.
1. Svelte 5 Runes — Reactivity
$state, $derived, $effect — the new reactive primitives (replaces let + $: in Svelte 4)
<!-- Counter.svelte (Svelte 5 runes) -->
<script>
// $state: fine-grained reactive signal
let count = $state(0);
let name = $state("World");
// $derived: computed from other state (replaces $: derived = ...)
let doubled = $derived(count * 2);
let greeting = $derived(`Hello, ${name}! Count: ${count}`);
// $derived.by for multi-line:
let stats = $derived.by(() => ({
doubled: count * 2,
isEven: count % 2 === 0,
label: count > 10 ? "high" : "low",
}));
// $effect: side effects (replaces $: { ... } blocks)
$effect(() => {
document.title = greeting; // runs when greeting changes
return () => {
document.title = "App"; // cleanup when component unmounts
};
});
// $effect.pre: runs before DOM update (for measuring DOM)
$effect.pre(() => {
const height = element.offsetHeight;
// runs before paint
});
</script>
<button onclick={() => count++}>{count}</button>
<p>Doubled: {doubled}</p>
<input bind:value={name} />
<!-- Class props with $props() rune: -->
<script>
// Props destructured from $props():
let { title, count = 0, onchange } = $props();
// ^required ^default ^callback prop
</script>
2. Component Patterns — Events, Stores, Lifecycle
Custom events, writable stores, onMount/onDestroy, and component composition
<!-- Emitting events (Svelte 5 uses callback props, not createEventDispatcher) -->
<script>
let { onselect } = $props(); // parent passes onselect={() => ...}
</script>
<button onclick={() => onselect({ id: 1, name: "Alice" })}>Select</button>
<!-- Svelte stores (still works in Svelte 5, good for shared state): -->
// stores/cart.ts:
import { writable, derived, get } from "svelte/store";
export const cartItems = writable([]);
export const cartTotal = derived(cartItems, ($items) =>
$items.reduce((sum, item) => sum + item.price * item.qty, 0)
);
export function addToCart(item) {
cartItems.update((items) => [...items, item]);
}
// In component — $ prefix auto-subscribes and unsubscribes:
<script>
import { cartItems, cartTotal, addToCart } from "$lib/stores/cart";
</script>
<p>{$cartItems.length} items — £{$cartTotal.toFixed(2)}</p>
<button onclick={() => addToCart(product)}>Add to cart</button>
<!-- Lifecycle (same as Svelte 4): -->
<script>
import { onMount, onDestroy, tick } from "svelte";
onMount(() => {
// DOM is ready — fetch data, init third-party libs
const observer = new IntersectionObserver(callback);
return () => observer.disconnect(); // cleanup = return function
});
// tick: wait for DOM update before reading:
async function toggleAndMeasure() {
open = true;
await tick(); // DOM has updated now
const height = panel.offsetHeight;
}
</script>
3. SvelteKit Routing & Load Functions
Filesystem routing, +page.ts load, +layout.svelte, and route params
# SvelteKit file conventions:
# src/routes/
# +page.svelte → /
# +layout.svelte → wraps all child routes
# +error.svelte → error page
# blog/
# +page.svelte → /blog
# [slug]/
# +page.svelte → /blog/:slug
# +page.ts → load function for /blog/:slug
# (auth)/ → route group (no URL segment)
# login/+page.svelte → /login
# register/+page.svelte
// +page.ts — load function (runs on server AND client):
export async function load({ params, fetch, cookies, url }) {
const res = await fetch(`/api/posts/${params.slug}`);
if (!res.ok) {
// SvelteKit error helper:
import { error } from "@sveltejs/kit";
error(404, "Post not found");
}
return { post: await res.json() };
}
// +page.server.ts — server-only load (DB access, secrets):
export async function load({ params, cookies }) {
const sessionId = cookies.get("session");
const user = await db.getUserBySession(sessionId);
if (!user) redirect(303, "/login");
const post = await db.getPost(params.slug);
return { post, user }; // typed via PageServerLoad
}
// +page.svelte — receive data from load:
<script lang="ts">
import type { PageData } from "./$types";
let { data } = $props(); // Svelte 5; Svelte 4: export let data
// data.post, data.user available
</script>
<h1>{data.post.title}</h1>
4. SvelteKit Form Actions & API Routes
+page.server.ts actions, use:enhance for progressive enhancement, and +server.ts API endpoints
// +page.server.ts — form actions (mutations without JavaScript requirement):
import { fail, redirect } from "@sveltejs/kit";
import type { Actions } from "./$types";
export const actions: Actions = {
// default action (single action on page):
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get("email") as string;
const password = data.get("password") as string;
if (!email || !password) {
return fail(400, { email, error: "Email and password required" });
}
const user = await db.login(email, password);
if (!user) return fail(401, { error: "Invalid credentials" });
cookies.set("session", user.sessionId, { path: "/", httpOnly: true });
redirect(303, "/dashboard");
},
// Named actions (multiple on same page):
logout: async ({ cookies }) => {
cookies.delete("session", { path: "/" });
redirect(303, "/login");
},
};
// +page.svelte — use:enhance for progressive enhancement:
<script>
import { enhance } from "$app/forms";
</script>
<form method="POST" use:enhance> <!-- works without JS, better with JS -->
<input name="email" type="email" />
<input name="password" type="password" />
<button>Login</button>
</form>
// +server.ts — REST API endpoint (for non-form clients):
import { json } from "@sveltejs/kit";
export async function GET({ params, url }) {
const posts = await db.getPosts({ page: Number(url.searchParams.get("page")) });
return json(posts);
}
export async function POST({ request }) {
const body = await request.json();
const post = await db.createPost(body);
return json(post, { status: 201 });
}
5. Deployment, SSR vs SSG, and Adapters
Choose the right adapter, prerender pages, and configure static vs dynamic rendering
// svelte.config.js — choose adapter:
import adapter from "@sveltejs/adapter-auto"; // Vercel/Netlify/Cloudflare auto-detect
// import adapter from "@sveltejs/adapter-node"; // self-hosted Node.js server
// import adapter from "@sveltejs/adapter-static"; // full static site (SSG)
// import adapter from "@sveltejs/adapter-cloudflare"; // Cloudflare Workers
export default {
kit: {
adapter: adapter(),
prerender: { handleHttpError: "warn" },
},
};
// Per-route rendering control (+page.ts or +layout.ts):
export const prerender = true; // SSG this route at build time
export const ssr = false; // CSR only (SPA mode for this route)
export const csr = false; // no JS hydration (pure HTML)
// +layout.ts at root level for site-wide SSG:
export const prerender = true; // prerender all routes by default
// Dynamic routes with SSG — provide all params:
export function entries() {
return [
{ slug: "hello-world" },
{ slug: "getting-started" },
];
}
export const prerender = true;
// Environment variables:
// Private (server only): import { DATABASE_URL } from "$env/static/private";
// Public (client + server): import { PUBLIC_API_URL } from "$env/static/public";
// Runtime: import { env } from "$env/dynamic/private";
// Recommended project structure:
// src/lib/ → shared utilities ($lib alias)
// src/lib/server/ → server-only code (never sent to browser)
// src/routes/ → filesystem router
// static/ → static assets (no processing)
Track Svelte releases at ReleaseRun. Related: React Reference | Vue.js 3 Reference | Next.js App Router Reference | TypeScript Reference
🔍 Free tool: npm Package Health Checker — check SvelteKit and Svelte ecosystem packages for EOL status, known CVEs, and latest versions.
Founded
2023 in London, UK
Contact
hello@releaserun.com