Svelte 5 introduces runes—a new system for reactive state and effects. Instead of relying on implicit reactivity through assignment, you use explicit primitives like $state, $derived, and $effect. This makes reactivity clearer, more portable, and better integrated with TypeScript.
Why runes?
Svelte 4's reactivity was elegant but had limitations:
| Svelte 4 limitation | Runes solution |
|---|---|
| Reactivity only in .svelte files | Works in .svelte.ts modules |
let vs reactive unclear |
$state is explicit |
$: dual purpose (derived + effect) |
Separate $derived and $effect |
| TypeScript inference issues | Full type inference |
Runes make Svelte's reactivity explicit without losing its simplicity.
$state
$state declares reactive state:
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
Clicked {count} times
</button>
Compare to Svelte 4:
<!-- Svelte 4 -->
<script>
let count = 0; // Implicitly reactive
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0); // Explicitly reactive
</script>
Objects and arrays
$state works with objects and arrays:
<script>
let user = $state({
name: 'Alice',
age: 30,
});
let items = $state(['apple', 'banana']);
</script>
<button onclick={() => user.age++}>
{user.name} is {user.age}
</button>
<button onclick={() => items.push('cherry')}>
Items: {items.length}
</button>
Nested updates are reactive automatically.
Deep reactivity
By default, $state creates deep reactivity. For shallow reactivity:
<script>
import { $state } from 'svelte';
let data = $state.raw({ nested: { value: 1 } });
// data.nested.value = 2 won't trigger updates
// data = { nested: { value: 2 } } will
</script>
$derived
$derived creates computed values:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
</script>
<p>{count} × 2 = {doubled}</p>
<p>{count} × 4 = {quadrupled}</p>
Compare to Svelte 4:
<!-- Svelte 4 -->
<script>
let count = 0;
$: doubled = count * 2;
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
Complex derivations
For multi-statement derivations:
<script>
let items = $state([1, 2, 3, 4, 5]);
let filter = $state(2);
let filtered = $derived.by(() => {
// Complex logic here
const result = items.filter(item => item > filter);
return result.sort((a, b) => a - b);
});
</script>
$effect
$effect runs side effects when dependencies change:
<script>
let count = $state(0);
$effect(() => {
console.log('Count changed to:', count);
// Automatically tracks count as dependency
});
</script>
Compare to Svelte 4:
<!-- Svelte 4 -->
<script>
let count = 0;
$: console.log('Count changed to:', count);
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0);
$effect(() => {
console.log('Count changed to:', count);
});
</script>
Cleanup
Return a cleanup function:
<script>
let interval = $state(1000);
$effect(() => {
const id = setInterval(() => console.log('tick'), interval);
return () => clearInterval(id);
});
</script>
Pre-effects
Run before DOM updates:
<script>
$effect.pre(() => {
// Runs before DOM update
});
</script>
$props
Declare component props:
<!-- Button.svelte -->
<script>
let { label, onclick, disabled = false } = $props();
</script>
<button {onclick} {disabled}>
{label}
</button>
With TypeScript:
<script lang="ts">
interface Props {
label: string;
onclick: () => void;
disabled?: boolean;
}
let { label, onclick, disabled = false }: Props = $props();
</script>
$bindable
Make props two-way bindable:
<!-- Input.svelte -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script>
let name = $state('');
</script>
<Input bind:value={name} />
Shared state with runes
Runes work in .svelte.ts files, enabling shared state without stores:
// state.svelte.ts
export function createCounter(initial = 0) {
let count = $state(initial);
return {
get count() { return count; },
increment() { count++; },
decrement() { count--; },
};
}
// Usage in component
<script>
import { createCounter } from './state.svelte.ts';
const counter = createCounter(10);
</script>
<button onclick={counter.increment}>
{counter.count}
</button>
Migration guide
| Svelte 4 | Svelte 5 |
|---|---|
let x = 0 |
let x = $state(0) |
$: y = x * 2 |
let y = $derived(x * 2) |
$: { console.log(x) } |
$effect(() => { console.log(x) }) |
export let prop |
let { prop } = $props() |
writable(0) |
$state(0) in .svelte.ts |
Gradual migration
Svelte 5 supports both syntaxes during migration:
<script>
// Old syntax still works
let oldCount = 0;
// New syntax
let newCount = $state(0);
</script>
Event handlers
Svelte 5 uses standard event attributes:
<!-- Svelte 4 -->
<button on:click={handleClick}>Click</button>
<!-- Svelte 5 -->
<button onclick={handleClick}>Click</button>
Snippets
Snippets replace slots for reusable template chunks:
<script>
let { header, children } = $props();
</script>
{@render header()}
<main>
{@render children()}
</main>
<!-- Usage -->
<Layout>
{#snippet header()}
<h1>Title</h1>
{/snippet}
<p>Main content</p>
</Layout>
TypeScript support
Runes have full TypeScript inference:
<script lang="ts">
let count = $state(0); // Type: number
let items = $state<string[]>([]); // Explicit generic
let doubled = $derived(count * 2); // Type: number
interface User {
name: string;
age: number;
}
let user = $state<User>({
name: 'Alice',
age: 30,
});
</script>
Performance
Runes maintain Svelte's performance:
| Aspect | Impact |
|---|---|
| Bundle size | Slightly smaller runtime |
| Reactivity | Same fine-grained updates |
| Compilation | Similar output |
The main benefit is developer experience, not performance.
Summary
Svelte 5 runes make reactivity explicit with $state, $derived, and $effect. They work in regular .svelte.ts files, enabling shared reactive state without stores. Props use $props() with full TypeScript support. The old syntax works during migration. Runes improve clarity and TypeScript integration while maintaining Svelte's simplicity and performance.
