← Back to blog

Notes on Migrating to Svelte 5 Runes

1 min read
SvelteRunesRefactoring

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 4Svelte 5 runes
export let xlet { x } = $props()
$: y = ...const y = $derived(...)
<slot />{@render children()}
on:clickonclick
<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-check passing 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.