Notes on Migrating to Svelte 5 Runes
Svelte 5 introduced runes — explicit, signal-based reactivity. The good news: Svelte 5 still compiles Svelte 4 syntax in a compatibility mode, so migration can be gradual rather than a big bang.
The mapping
Most of the work is mechanical. Here is the cheat sheet I keep coming back to:
| Svelte 4 | Svelte 5 runes |
|---|---|
export let x | let { x } = $props() |
$: y = ... | const y = $derived(...) |
<slot /> | {@render children()} |
on:click | onclick |
<svelte:component this={X} /> | <X /> |
A small example
A prop-driven derived value, before and after:
<script>
// before
export let type;
$: color = type === 'danger' ? 'red' : 'green';
</script> <script>
// after
let { type } = $props();
const color = $derived(type === 'danger' ? 'red' : 'green');
</script> Why the green check can lie
svelte-checkpassing with zero warnings does not mean the code uses runes. Legacy syntax compiles cleanly in compatibility mode — the debt is invisible until you opt in.
The fix is to migrate file by file, starting with shared UI primitives, then flip compilerOptions.runes: true to close the door on regressions.
Effects are a last resort
The one habit worth unlearning: reaching for $effect to compute values. Prefer $derived, and
keep effects for genuine side effects — syncing to a non-Svelte library, for instance. Less state to
keep in your head, fewer surprises.