Overview
Svelte 5 introduces runes — a new reactivity model that replaces $: reactive declarations, let bindings, and Svelte stores with explicit, composable primitives. Runes are functions that start with $ and are processed by the Svelte compiler.
Key changes from Svelte 4:
let count = 0→let count = $state(0)$: doubled = count * 2→let doubled = $derived(count * 2)$: { ... }side effect →$effect(() => { ... })export let prop→let { prop } = $props()- Svelte stores →
$statein.svelte.tsfiles - Slots → Snippets
Runes Reference
$state — reactive state
<script>
let count = $state(0);
let user = $state({ name: "Alice", age: 30 });
// Deep reactivity: nested properties trigger updates
function birthday() {
user.age++; // reactive!
}
</script>
<button onclick={() => count++}>{count}</button>
<p>{user.name} is {user.age}</p>
$state.raw — shallow (non-deep) reactive value:
<script>
// Only the top-level reference is reactive; mutations don't trigger updates
let items = $state.raw([1, 2, 3]);
function addItem() {
items = [...items, items.length + 1]; // must reassign
}
</script>
$state.snapshot — get a plain (non-reactive) copy:
<script>
let form = $state({ name: "", email: "" });
async function submit() {
const data = $state.snapshot(form); // plain object for API call
await fetch("/api", { method: "POST", body: JSON.stringify(data) });
}
</script>
$derived — computed values
<script>
let price = $state(100);
let quantity = $state(3);
// Recomputes when price or quantity changes
let total = $derived(price * quantity);
let discounted = $derived(total > 200 ? total * 0.9 : total);
</script>
<p>Total: ${total}</p>
<p>With discount: ${discounted.toFixed(2)}</p>
$derived.by — for multi-line derived logic:
<script>
let items = $state([{ name: "A", price: 10 }, { name: "B", price: 20 }]);
let summary = $derived.by(() => {
const total = items.reduce((sum, item) => sum + item.price, 0);
const count = items.length;
return { total, count, avg: count > 0 ? total / count : 0 };
});
</script>
<p>{summary.count} items, avg ${summary.avg.toFixed(2)}</p>
$effect — side effects
<script>
let query = $state("");
let results = $state([]);
// Runs when query changes; cleanup runs before next execution
$effect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(data => { results = data; });
// Cleanup function (equivalent to useEffect return)
return () => controller.abort();
});
</script>
<input bind:value={query} placeholder="Search..." />
$effect.pre — runs before DOM updates:
<script>
let messages = $state([]);
let scrollContainer: HTMLElement;
$effect.pre(() => {
// Access messages.length to track changes
messages.length;
// Runs before DOM update — useful for scroll position
});
</script>
$props — component props
<!-- Button.svelte -->
<script>
let {
label,
variant = "primary", // default value
onclick, // event handler
class: className = "", // renamed (class is reserved)
...rest // rest props spread to element
} = $props();
</script>
<button
class="btn btn-{variant} {className}"
{onclick}
{...rest}
>
{label}
</button>
Usage:
<Button label="Submit" variant="danger" onclick={() => console.log("clicked")} />
$bindable — two-way binding
<!-- Input.svelte — exposes value for bind: -->
<script>
let { value = $bindable(""), placeholder = "" } = $props();
</script>
<input bind:value {placeholder} />
<!-- Parent.svelte -->
<script>
import Input from "./Input.svelte";
let name = $state("");
</script>
<Input bind:value={name} placeholder="Your name" />
<p>Hello, {name}!</p>
$inspect — debug reactive values
<script>
let count = $state(0);
let doubled = $derived(count * 2);
// Logs to console whenever count or doubled changes (dev only)
$inspect(count, doubled);
// Custom handler
$inspect(count).with((type, value) => {
console.log(`[${type}] count =`, value); // type: "init" | "update"
});
</script>
Snippets (replacing Slots)
Svelte 5 replaces slots with snippets — typed, reusable markup fragments.
Basic snippet
<!-- Card.svelte -->
<script>
let { header, children } = $props();
</script>
<div class="card">
<div class="card-header">
{@render header()}
</div>
<div class="card-body">
{@render children()}
</div>
</div>
<!-- Parent.svelte -->
<Card>
{#snippet header()}
<h2>My Card Title</h2>
{/snippet}
<p>Card body content here.</p>
</Card>
Snippet with parameters
<!-- List.svelte -->
<script>
let { items, row } = $props();
</script>
<ul>
{#each items as item}
<li>{@render row(item)}</li>
{/each}
</ul>
<!-- Parent.svelte -->
<List {items}>
{#snippet row(item)}
<strong>{item.name}</strong> — {item.description}
{/snippet}
</List>
Migration from Svelte 4
Reactive declarations → $derived
<!-- Svelte 4 -->
<script>
let count = 0;
$: doubled = count * 2;
$: console.log("count changed:", count);
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => { console.log("count changed:", count); });
</script>
Stores → $state in module
// Svelte 4: stores/counter.ts
import { writable, derived } from "svelte/store";
export const count = writable(0);
export const doubled = derived(count, $c => $c * 2);
// Svelte 5: state/counter.svelte.ts
// Use .svelte.ts extension for runes outside components
let count = $state(0);
let doubled = $derived(count * 2);
export function getCounter() {
return {
get count() { return count; },
get doubled() { return doubled; },
increment() { count++; },
reset() { count = 0; },
};
}
<!-- Component.svelte -->
<script>
import { getCounter } from "$lib/state/counter.svelte.ts";
const counter = getCounter();
</script>
<p>{counter.count} (doubled: {counter.doubled})</p>
<button onclick={counter.increment}>+1</button>
Props: export let → $props()
<!-- Svelte 4 -->
<script>
export let name;
export let age = 0;
</script>
<!-- Svelte 5 -->
<script>
let { name, age = 0 } = $props();
</script>
Slots → Snippets
<!-- Svelte 4 -->
<div><slot name="header" /><slot /></div>
<!-- Svelte 5 -->
<script>
let { header, children } = $props();
</script>
<div>{@render header?.()}{@render children?.()}</div>
SvelteKit 2 Compatibility
SvelteKit 2 works with Svelte 5 out of the box. Key patterns:
<!-- +page.svelte — load data -->
<script>
let { data } = $props(); // from +page.ts load function
</script>
<h1>{data.title}</h1>
// +page.ts
export async function load({ fetch }) {
const res = await fetch("/api/posts");
return { posts: await res.json() };
}
Form actions still work
<form method="POST" action="?/create">
<input name="title" />
<button>Create</button>
</form>
Common Patterns
Toggle with $state
<script>
let open = $state(false);
</script>
<button onclick={() => (open = !open)}>
{open ? "Close" : "Open"}
</button>
{#if open}<div class="modal">Content</div>{/if}
Async derived with $effect
<script>
let userId = $state(1);
let user = $state(null);
let loading = $state(false);
$effect(() => {
loading = true;
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => { user = data; loading = false; });
});
</script>
{#if loading}<p>Loading...</p>
{:else if user}<p>{user.name}</p>{/if}
Class component pattern (shared state)
// lib/theme.svelte.ts
class ThemeStore {
current = $state<"light" | "dark">("light");
toggle() {
this.current = this.current === "light" ? "dark" : "light";
}
}
export const theme = new ThemeStore();
<script>
import { theme } from "$lib/theme.svelte.ts";
</script>
<button onclick={theme.toggle}>
Switch to {theme.current === "light" ? "dark" : "light"}
</button>