To understand why Runes exist, we need to understand what came before and why it wasn't enough.
Svelte 4: Implicit Reactivity
In Svelte 4, reactivity was achieved through compiler analysis of assignments:
svelte<script> let count = 0; // Svelte detects this declaration count += 1; // Svelte sees this assignment → triggers update $: doubled = count * 2; // Reactive statement </script>
This felt magical — you just wrote normal JavaScript and things "just worked". But this magic had real problems:
Problem 1: Component Boundaries
Reactivity ONLY worked inside .svelte files. You couldn't extract logic to a .js file:
js// utils.js — This does NOT work in Svelte 4! let count = 0; // Not reactive export function increment() { count += 1; // Doesn't trigger any updates }
Problem 2: The $: Confusion
The $: syntax was overloaded with two meanings:
svelte<script> $: doubled = count * 2; // Reactive declaration $: console.log(count); // Side effect $: if (count > 10) reset(); // Conditional side effect </script>
When should you use $:? When does it re-run? The rules were complex and often surprising.
Problem 3: Object/Array Reactivity
svelte<script> let items = [1, 2, 3]; items.push(4); // Does NOT trigger update! items = items; // Workaround: reassign to trigger </script>
Runes: Explicit is Better Than Implicit
Svelte 5 introduces Runes — explicit symbols that clearly mark reactive behavior:
svelte<script> let count = $state(0); // Explicitly reactive let doubled = $derived(count * 2); // Explicitly derived $effect(() => { // Explicit side effect console.log(count); }); </script>
No more guessing. Every reactive piece is clearly marked.
The Signals Pattern
Runes implement the Signals pattern, used by Solid.js, Preact Signals, and Angular Signals. A signal is:
- A container for a value
- That tracks who reads it (subscribers)
- And notifies subscribers when it changes
This enables fine-grained updates — when count changes, only the exact DOM nodes using count update, not the entire component.