{"id":5247,"date":"2026-08-10T19:02:05","date_gmt":"2026-08-10T11:02:05","guid":{"rendered":"https:\/\/arc.dev\/employer-blog\/?p=5247"},"modified":"2026-08-11T22:39:07","modified_gmt":"2026-08-11T14:39:07","slug":"svelte-5-runes-migration-guide","status":"publish","type":"post","link":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/","title":{"rendered":"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity"},"content":{"rendered":"\n<p>Svelte 5 runes are the biggest API shift in the framework&#8217;s history, and most teams stall on the upgrade because the release notes don&#8217;t tell you <em>what breaks first<\/em>. <strong>Runes replace Svelte 4&#8217;s implicit, compiler-magic reactivity (built on <\/strong><strong>let<\/strong><strong>, <\/strong><strong>export let<\/strong><strong>, and <\/strong><strong>$:<\/strong><strong>) with an explicit syntax: <\/strong><strong>$state<\/strong><strong>, <\/strong><strong>$derived<\/strong><strong>, <\/strong><strong>$effect<\/strong><strong>, and <\/strong><strong>$props<\/strong><strong>.<\/strong> The old patterns still compile, but mixing them incorrectly silently kills reactivity in ways that don&#8217;t throw errors.<\/p>\n\n\n\n<p>If you want a risk-ordered <a href=\"https:\/\/svelte.dev\/docs\/svelte\/v5-migration-guide\">migration path for Svelte 5 releases<\/a>, we&#8217;ve got you covered. You&#8217;ll know which patterns to fix first, which can wait, and which won&#8217;t break at all.<\/p>\n\n\n\n<p><strong>In this guide:&nbsp;<\/strong><\/p>\n\n\n\n<p>What Changed in Reactivity and Why It Matters First<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>What Fine-Grained Updates Change for Real Apps<\/li>\n<\/ul>\n\n\n\n<p>Why Svelte Moved to Runes<\/p>\n\n\n\n<p>The Old-to-New API Map With Runnable Before and After Examples<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Top-Level let to $state<\/li>\n\n\n\n<li>$: Computed Values to $derived<\/li>\n\n\n\n<li>$: Side Effects to $effect<\/li>\n\n\n\n<li>export let to $props<\/li>\n\n\n\n<li>Two-Way Binding to $bindable<\/li>\n<\/ul>\n\n\n\n<p>The Risk-Ordered Migration Path for Existing Codebases<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Highest Risk: Shared Stores and Cross-Component Reactive Flows<\/li>\n\n\n\n<li>High Risk: Reactive Blocks That Mix Derivation and Side Effects<\/li>\n\n\n\n<li>Medium Risk: Props, Callback Contracts, and Bindings<\/li>\n\n\n\n<li>Lower Risk: Local State and Straightforward Derived Values<\/li>\n<\/ul>\n\n\n\n<p>Should You Migrate Now or Wait?<\/p>\n\n\n\n<p>Events, Props, and Component Composition Changes That Commonly Break<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Event Attributes Replace on: Directives<\/li>\n\n\n\n<li>createEventDispatcher to Callback Props<\/li>\n\n\n\n<li>Slots to Snippets and Children<\/li>\n<\/ul>\n\n\n\n<p>Interop and Incremental Adoption in Mixed Component Trees<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>SvelteKit, Vite, SSR, and Hydration Checks During Upgrade<\/li>\n<\/ul>\n\n\n\n<p>Using sv migrate and What It Won&#8217;t Fix<\/p>\n\n\n\n<p>Common Breakage Patterns and How to Fix Them Fast<\/p>\n\n\n\n<p>Frequently Asked Questions<\/p>\n\n\n\n<p>Migrate in Risk Order, Not File Order<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>What Changed in Reactivity and Why It Matters First<\/strong><\/h2>\n\n\n\n<p>Svelte 4&#8217;s reactivity felt like magic. You declared let count = 0 inside a .svelte file, and the compiler automatically made it reactive. That magic came from static analysis: the Svelte compiler scanned your component files at build time and injected update calls wherever it found assignments to top-level variables.<\/p>\n\n\n\n<p>Svelte 5 replaces that model with <em>runes<\/em>, explicit function calls that mark state, derivations, and side effects. The reactivity system now uses fine-grained signals under the hood, tracking dependencies at the individual-value level rather than the component level.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>What Fine-Grained Updates Change for Real Apps<\/strong><\/h3>\n\n\n\n<p>Svelte 4 re-ran component update logic whenever <em>any<\/em> reactive variable in that component changed. Svelte 5&#8217;s signal-based system tracks which specific values each piece of UI depends on, then updates only those parts.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Behavior<\/strong><\/td><td><strong>Svelte 4<\/strong><\/td><td><strong>Svelte 5<\/strong><\/td><\/tr><tr><td>Reactivity scope<\/td><td>Per-component<\/td><td>Per-signal (fine-grained)<\/td><\/tr><tr><td>Reactive state in .js\/.ts<\/td><td>Not possible (need stores)<\/td><td>Supported via .svelte.js\/.svelte.ts<\/td><\/tr><tr><td>Derivation vs. side effect<\/td><td>Both use $:<\/td><td>$derived vs. $effect (explicit)<\/td><\/tr><tr><td>Update granularity<\/td><td>Entire component re-checks<\/td><td>Only dependent DOM nodes update<\/td><\/tr><tr><td>Native TypeScript support<\/td><td>Limited type inference<\/td><td>Full type inference with runes<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>In terms of performance, this means fewer unnecessary DOM updates in components with many reactive values. For correctness, it means you always know whether a block computes a value or runs a side effect, because the rune you chose tells you.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Why Svelte Moved to Runes<\/strong><\/h2>\n\n\n\n<p>The Svelte 4 compiler could only make things reactive inside .svelte files. If you extracted a counter function into a plain .js or .ts file, it lost all reactivity. You had to reach for Svelte stores (writable, readable) just to share reactive state across files, even for simple values.<\/p>\n\n\n\n<p>Reactive blocks ($:) had another problem: the compiler couldn&#8217;t always tell whether a $: block was a derivation (computing a value) or a side effect (doing work). It ran both the same way, which led to unpredictable execution order when blocks depended on each other.<\/p>\n\n\n\n<p>This meant two things for real codebases:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Reusable reactive logic required stores, adding boilerplate.<\/li>\n\n\n\n<li>Complex $: chains ran in an order the compiler chose, not the order you wrote them.<\/li>\n<\/ul>\n\n\n\n<p>Runes make reactivity a runtime primitive instead of a compile-time trick. You can now write reactive state in .svelte.js or .svelte.ts files, and it works the same way it does inside components.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- counter.svelte.js --&gt;\n\n&lt;script module&gt;\n\n&nbsp;&nbsp;export function createCounter() {\n\n&nbsp;&nbsp;&nbsp;&nbsp;let count = $state(0);\n\n&nbsp;&nbsp;&nbsp;&nbsp;const doubled = $derived(count * 2);\n\n&nbsp;&nbsp;&nbsp;&nbsp;return {\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;get count() { return count },\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;get doubled() { return doubled },\n\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;increment() { count++ }\n\n&nbsp;&nbsp;&nbsp;&nbsp;};\n\n&nbsp;&nbsp;}\n\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p>That function works in any component that imports it. No stores, no special subscription syntax. The Svelte compiler processes .svelte.js and .svelte.ts files as compiler-aware modules, so runes just work there.<\/p>\n\n\n\n<p>This is the single biggest unlock: reactive state can live anywhere, not just inside component &lt;script&gt; tags.<\/p>\n\n\n\n<p><strong>Read more: <\/strong><a href=\"https:\/\/arc.dev\/employer-blog\/svelte-vs-react-mapping-the-choice-to-team-size-and-app-lifespan\/\"><strong>Svelte vs React in 2026: Mapping the Choice to Team Size and App Lifespan<\/strong><\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>The Old-to-New API Map With Runnable Before and After Examples<\/strong><\/h2>\n\n\n\n<p>Each rune maps directly to a Svelte 4 pattern. Here is the exact translation for every case you&#8217;ll encounter during migration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Top-Level <\/strong><strong>let<\/strong><strong> to <\/strong><strong>$state<\/strong><\/h3>\n\n\n\n<p>In Svelte 4, any top-level let in a component&#8217;s &lt;script&gt; block was automatically reactive.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let count = 0;\n\n&lt;\/script&gt;\n\n&lt;button on:click={() =&gt; count++}&gt;\n\n&nbsp;&nbsp;Clicked {count} times\n\n&lt;\/button&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let count = $state(0);\n\n&lt;\/script&gt;\n\n&lt;button onclick={() =&gt; count++}&gt;\n\n&nbsp;&nbsp;Clicked {count} times\n\n&lt;\/button&gt;<\/code><\/pre>\n\n\n\n<p>The only change is wrapping the initial value in $state(). The variable still behaves like a normal let, and you reassign it the same way.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>$:<\/strong><strong> Computed Values to <\/strong><strong>$derived<\/strong><\/h3>\n\n\n\n<p>Svelte 4 used $: for both computed values and side effects. Svelte 5 splits these into two separate runes.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let count = 0;\n\n&nbsp;&nbsp;$: doubled = count * 2;\n\n&lt;\/script&gt;\n\n&lt;p&gt;{doubled}&lt;\/p&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let count = $state(0);\n\n&nbsp;&nbsp;let doubled = $derived(count * 2);\n\n&lt;\/script&gt;\n\n&lt;p&gt;{doubled}&lt;\/p&gt;<\/code><\/pre>\n\n\n\n<p>$derived accepts an expression and recalculates only when its dependencies change. It&#8217;s read-only; you cannot assign to it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>$:<\/strong><strong> Side Effects to <\/strong><strong>$effect<\/strong><\/h3>\n\n\n\n<p>If your $: block did work (logging, fetching, pushing to an array) rather than computing a value, it maps to $effect.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let count = 0;\n\n&nbsp;&nbsp;$: {\n\n&nbsp;&nbsp;&nbsp;&nbsp;console.log(`Count is now ${count}`);\n\n&nbsp;&nbsp;}\n\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let count = $state(0);\n\n&nbsp;&nbsp;$effect(() =&gt; {\n\n&nbsp;&nbsp;&nbsp;&nbsp;console.log(`Count is now ${count}`);\n\n&nbsp;&nbsp;});\n\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p>$effect runs after the DOM updates. It auto-tracks any reactive value you read inside it and re-runs when those values change.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>export let<\/strong><strong> to <\/strong><strong>$props<\/strong><\/h3>\n\n\n\n<p>Svelte 4 used export let to declare component props. Svelte 5 uses a single $props() call with destructuring.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;export let name;\n\n&nbsp;&nbsp;export let greeting = 'Hello';\n\n&lt;\/script&gt;\n\n&lt;p&gt;{greeting}, {name}&lt;\/p&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let { name, greeting = 'Hello' } = $props();\n\n&lt;\/script&gt;\n\n&lt;p&gt;{greeting}, {name}&lt;\/p&gt;<\/code><\/pre>\n\n\n\n<p>Default values work the same way through standard JavaScript destructuring defaults. All props come from one $props() call.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Two-Way Binding to <\/strong><strong>$bindable<\/strong><\/h3>\n\n\n\n<p>When a parent binds to a child&#8217;s prop with bind:, the child needs to mark that prop as bindable.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Child.svelte --&gt;\n\n&lt;script&gt;\n\n&nbsp;&nbsp;export let value;\n\n&lt;\/script&gt;\n\n&lt;input bind:value \/&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Child.svelte --&gt;\n\n&lt;script&gt;\n\n&nbsp;&nbsp;let { value = $bindable() } = $props();\n\n&lt;\/script&gt;\n\n&lt;input bind:value \/&gt;<\/code><\/pre>\n\n\n\n<p>$bindable() signals that the parent can use bind:value on this component. Without it, the binding silently fails.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Svelte 4 Syntax<\/strong><\/td><td><strong>Svelte 5 Rune<\/strong><\/td><td><strong>Notes<\/strong><\/td><\/tr><tr><td>let count = 0<\/td><td>let count = $state(0)<\/td><td>Wrap initial value<\/td><\/tr><tr><td>$: doubled = x * 2<\/td><td>let doubled = $derived(x * 2)<\/td><td>Read-only, auto-tracked<\/td><\/tr><tr><td>$: { sideEffect() }<\/td><td>$effect(() =&gt; { sideEffect() })<\/td><td>Runs after DOM update<\/td><\/tr><tr><td>export let name<\/td><td>let { name } = $props()<\/td><td>Single destructured call<\/td><\/tr><tr><td>export let value + bind:value<\/td><td>let { value = $bindable() } = $props()<\/td><td>Required for bind: to work<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>The Risk-Ordered Migration Path for Existing Codebases<\/strong><\/h2>\n\n\n\n<p>The Svelte 5 migration guide says you can mix old and new syntax across components. That&#8217;s true. But <em>within<\/em> a single component, you must use one syntax or the other. You cannot mix $: and $derived in the same file.<\/p>\n\n\n\n<p>This means migration is per-component, and the order you migrate components matters. Start with the highest-risk patterns first, because those are the ones that break in non-obvious ways.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Risk Level<\/strong><\/td><td><strong>Component\/Pattern Type<\/strong><\/td><td><strong>What Breaks<\/strong><\/td><td><strong>Fix Order<\/strong><\/td><\/tr><tr><td><strong>Highest<\/strong><\/td><td>Shared stores and cross-component reactive flows<\/td><td>Store subscriptions behave differently; $: blocks that depend on store values lose implicit tracking<\/td><td>1st<\/td><\/tr><tr><td><strong>High<\/strong><\/td><td>Reactive blocks mixing derivation and side effects<\/td><td>A single $: block that computes a value <em>and<\/em> triggers side effects must be split into $derived + $effect<\/td><td>2nd<\/td><\/tr><tr><td><strong>Medium<\/strong><\/td><td>Props, callback contracts, and bindings<\/td><td>export let to $props() is mechanical, but bind: requires $bindable() or it silently stops working<\/td><td>3rd<\/td><\/tr><tr><td><strong>Lower<\/strong><\/td><td>Local state and simple derived values<\/td><td>let x = 0 to $state(0) is a safe find-and-replace; $: y = x * 2 to $derived is equally safe<\/td><td>4th<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Highest Risk: Shared Stores and Cross-Component Reactive Flows<\/strong><\/h3>\n\n\n\n<p>If your app uses Svelte stores (writable, derived from svelte\/store) as global state shared across many components, these are the first things to migrate. The reason: when surrounding components switch to runes, the $storeName auto-subscription syntax still works, but reactive blocks that depend on store values no longer have the same execution timing.<\/p>\n\n\n\n<p><strong>Concrete symptom:<\/strong> A component reads $myStore inside a $: block that also calls a function with side effects. After migration, the $effect version re-runs at a different point in the update cycle, and the side effect fires before the DOM reflects the new value.<\/p>\n\n\n\n<p><strong>Fix:<\/strong> Convert shared stores to .svelte.js modules using $state and exported getter functions. This eliminates the store abstraction entirely.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- store.svelte.js --&gt;\n\n&lt;script module&gt;\n\n&nbsp;&nbsp;let items = $state(&#91;]);\n\n&nbsp;&nbsp;export function addItem(item) {\n\n&nbsp;&nbsp;&nbsp;&nbsp;items.push(item);\n\n&nbsp;&nbsp;}\n\n&nbsp;&nbsp;export function getItems() {\n\n&nbsp;&nbsp;&nbsp;&nbsp;return items;\n\n&nbsp;&nbsp;}\n\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>High Risk: Reactive Blocks That Mix Derivation and Side Effects<\/strong><\/h3>\n\n\n\n<p>This is where Svelte 4&#8217;s $: ambiguity causes real trouble. A $: block that both computes a value <em>and<\/em> pushes to an array will behave differently under $effect, because $effect doesn&#8217;t track array mutations by default unless the array itself is $state.<\/p>\n\n\n\n<p><strong>Concrete symptom:<\/strong> You had a $: block like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let items = &#91;];\n\n&nbsp;&nbsp;let filter = '';\n\n&nbsp;&nbsp;$: {\n\n&nbsp;&nbsp;&nbsp;&nbsp;filteredItems = items.filter(i =&gt; i.includes(filter));\n\n&nbsp;&nbsp;&nbsp;&nbsp;console.log('Filtered', filteredItems.length);\n\n&nbsp;&nbsp;}\n\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p>After migration, you need to split this into a $derived for computation and a separate $effect for logging.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let items = $state(&#91;]);\n\n&nbsp;&nbsp;let filter = $state('');\n\n&nbsp;&nbsp;let filteredItems = $derived(items.filter(i =&gt; i.includes(filter)));\n\n&nbsp;&nbsp;$effect(() =&gt; {\n\n&nbsp;&nbsp;&nbsp;&nbsp;console.log('Filtered', filteredItems.length);\n\n&nbsp;&nbsp;});\n\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Medium Risk: Props, Callback Contracts, and Bindings<\/strong><\/h3>\n\n\n\n<p>Converting export let to $props() is mechanical. The risk comes from bindings: if a child component used export let value and a parent used bind:value, you must add $bindable() in the child or the binding silently breaks with no error.<\/p>\n\n\n\n<p><strong>Concrete symptom:<\/strong> A form input component stops syncing its value to the parent after migration, because $bindable() was not added.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Lower Risk: Local State and Straightforward Derived Values<\/strong><\/h3>\n\n\n\n<p>Simple local state (let count = 0 to let count = $state(0)) and basic derivations ($: doubled = count * 2 to let doubled = $derived(count * 2)) are safe, mechanical transforms. The migration script handles these reliably, and they rarely introduce bugs.<\/p>\n\n\n\n<p>Run npx sv migrate svelte-5 on these components with confidence. Save your manual review time for the higher-risk tiers.<\/p>\n\n\n\n<p><strong>Read more: <\/strong><a href=\"https:\/\/arc.dev\/employer-blog\/best-platforms-to-hire-svelte-developers\/\"><strong>Best Platforms to Hire Svelte Developers in 2026<\/strong><\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Should You Migrate Now or Wait?<\/strong><\/h2>\n\n\n\n<p>Not every codebase should migrate on the same timeline. The right call depends on how your state is structured today and how much review bandwidth your team has, not on how new the release is.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Factor<\/strong><\/td><td><strong>Migrate Now<\/strong><\/td><td><strong>Wait<\/strong><\/td><\/tr><tr><td><strong>State architecture<\/strong><\/td><td>Mostly local component state, few or no stores<\/td><td>Heavy reliance on shared stores across many components<\/td><\/tr><tr><td><strong>Project size<\/strong><\/td><td>Small to mid-size app, or a new project starting fresh<\/td><td>Large legacy app with hundreds of components and deep $: chains<\/td><\/tr><tr><td><strong>Team bandwidth<\/strong><\/td><td>You have time for manual review on high-risk patterns (stores, mixed $: blocks)<\/td><td>Team is at capacity on other priorities; migration would get rushed<\/td><\/tr><tr><td><strong>Third-party dependencies<\/strong><\/td><td>Your UI libraries already ship Svelte 5-compatible versions<\/td><td>You depend on libraries that haven&#8217;t published runes-compatible releases<\/td><\/tr><tr><td><strong>SSR\/SvelteKit usage<\/strong><\/td><td>Low reliance on $: blocks for SSR-critical values<\/td><td>Complex SSR logic that mixes derivations and side effects<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p><strong>Migrate now if:<\/strong>&nbsp;<\/p>\n\n\n\n<p>Your app is small enough to review every component manually, your state is mostly local, and your dependencies already support Svelte 5. The mechanical transforms are safe, and the risk-ordered path above keeps the harder parts contained.<\/p>\n\n\n\n<p><strong>Wait if:<\/strong>&nbsp;<\/p>\n\n\n\n<p>You have a large store-based architecture, tight deadlines elsewhere, or dependencies that haven&#8217;t caught up. Svelte 4 and Svelte 5 components interoperate, so you can start migrating new components in runes syntax now and defer the legacy core until you have dedicated time for the highest-risk tier.<\/p>\n\n\n\n<p>There&#8217;s no version deprecation deadline forcing an all-at-once switch. The interop model exists specifically so you can move at the pace your codebase and team bandwidth support.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Events, Props, and Component Composition Changes That Commonly Break<\/strong><\/h2>\n\n\n\n<p>Svelte 5 changes more than reactivity. Event handling, slots, and component composition all have new patterns. These are the areas where migration scripts miss things, and you need manual fixes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Event Attributes Replace <\/strong><strong>on:<\/strong><strong> Directives<\/strong><\/h3>\n\n\n\n<p>Svelte 4 used on:click, on:submit, and similar directives. Svelte 5 replaces these with standard HTML event attributes.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;button on:click={handleClick}&gt;Click me&lt;\/button&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;button onclick={handleClick}&gt;Click me&lt;\/button&gt;<\/code><\/pre>\n\n\n\n<p>Event modifiers like |preventDefault and |stopPropagation are gone. Handle them inside your callback instead.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;function handleSubmit(event) {\n\n&nbsp;&nbsp;&nbsp;&nbsp;event.preventDefault();\n\n&nbsp;&nbsp;&nbsp;&nbsp;\/\/ form logic\n\n&nbsp;&nbsp;}\n\n&lt;\/script&gt;\n\n&lt;form onsubmit={handleSubmit}&gt;...&lt;\/form&gt;<\/code><\/pre>\n\n\n\n<p>Modifiers like capture, passive, and nonpassive are no longer available as shorthand. You need to use addEventListener directly if you need them.<\/p>\n\n\n\n<p>Multiple event handlers on the same event are now straightforward: wrap them in a single function.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;button onclick={(e) =&gt; { logClick(e); handleClick(e); }}&gt;\n\n&nbsp;&nbsp;Click me\n\n&lt;\/button&gt;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>createEventDispatcher<\/strong><strong> to Callback Props<\/strong><\/h3>\n\n\n\n<p>createEventDispatcher is removed in Svelte 5. Components that dispatch events now accept callback functions as props instead.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Child.svelte --&gt;\n\n&lt;script&gt;\n\n&nbsp;&nbsp;import { createEventDispatcher } from 'svelte';\n\n&nbsp;&nbsp;const dispatch = createEventDispatcher();\n\n&lt;\/script&gt;\n\n&lt;button on:click={() =&gt; dispatch('submit', { value: 42 })}&gt;\n\n&nbsp;&nbsp;Submit\n\n&lt;\/button&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Child.svelte --&gt;\n\n&lt;script&gt;\n\n&nbsp;&nbsp;let { onsubmit } = $props();\n\n&lt;\/script&gt;\n\n&lt;button onclick={() =&gt; onsubmit({ value: 42 })}&gt;\n\n&nbsp;&nbsp;Submit\n\n&lt;\/button&gt;<\/code><\/pre>\n\n\n\n<p>The parent passes the handler as a prop:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;Child onsubmit={(data) =&gt; console.log(data.value)} \/&gt;<\/code><\/pre>\n\n\n\n<p>Bubbling events (on:click without a handler to forward the event) no longer exist. You must explicitly pass callbacks down.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Slots to Snippets and Children<\/strong><\/h3>\n\n\n\n<p>Svelte 4 used &lt;slot&gt; for component composition. Svelte 5 replaces slots with <em>snippets<\/em> and a children prop.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Card.svelte --&gt;\n\n&lt;div class=\"card\"&gt;\n\n&nbsp;&nbsp;&lt;slot \/&gt;\n\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Card.svelte --&gt;\n\n&lt;script&gt;\n\n&nbsp;&nbsp;let { children } = $props();\n\n&lt;\/script&gt;\n\n&lt;div class=\"card\"&gt;\n\n&nbsp;&nbsp;{@render children()}\n\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p>The parent syntax stays the same:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;Card&gt;\n\n&nbsp;&nbsp;&lt;p&gt;This is the card content.&lt;\/p&gt;\n\n&lt;\/Card&gt;<\/code><\/pre>\n\n\n\n<p>Named slots become named snippets. The parent defines a snippet with {#snippet}, and the child renders it with {@render}.<\/p>\n\n\n\n<p><strong>Svelte 4:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Layout.svelte --&gt;\n\n&lt;header&gt;&lt;slot name=\"header\" \/&gt;&lt;\/header&gt;\n\n&lt;main&gt;&lt;slot \/&gt;&lt;\/main&gt;\n\n&lt;!-- Parent --&gt;\n\n&lt;Layout&gt;\n\n&nbsp;&nbsp;&lt;h1 slot=\"header\"&gt;Title&lt;\/h1&gt;\n\n&nbsp;&nbsp;&lt;p&gt;Body content&lt;\/p&gt;\n\n&lt;\/Layout&gt;<\/code><\/pre>\n\n\n\n<p><strong>Svelte 5:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Layout.svelte --&gt;\n\n&lt;script&gt;\n\n&nbsp;&nbsp;let { header, children } = $props();\n\n&lt;\/script&gt;\n\n&lt;header&gt;{@render header()}&lt;\/header&gt;\n\n&lt;main&gt;{@render children()}&lt;\/main&gt;\n\n&lt;!-- Parent --&gt;\n\n&lt;Layout&gt;\n\n&nbsp;&nbsp;{#snippet header()}\n\n&nbsp;&nbsp;&nbsp;&nbsp;&lt;h1&gt;Title&lt;\/h1&gt;\n\n&nbsp;&nbsp;{\/snippet}\n\n&nbsp;&nbsp;&lt;p&gt;Body content&lt;\/p&gt;\n\n&lt;\/Layout&gt;<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Svelte 4 Pattern<\/strong><\/td><td><strong>Svelte 5 Replacement<\/strong><\/td><\/tr><tr><td>on:click={handler}<\/td><td>onclick={handler}<\/td><\/tr><tr><td>on:click|preventDefault<\/td><td>Handle in callback<\/td><\/tr><tr><td>createEventDispatcher<\/td><td>Callback props<\/td><\/tr><tr><td>on:click (bubbling)<\/td><td>Pass callback explicitly<\/td><\/tr><tr><td>&lt;slot \/&gt;<\/td><td>{@render children()}<\/td><\/tr><tr><td>&lt;slot name=&#8221;x&#8221; \/&gt;<\/td><td>{@render x()} via $props()<\/td><\/tr><tr><td>slot=&#8221;x&#8221; on parent<\/td><td>{#snippet x()} &#8230; {\/snippet}<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Interop and Incremental Adoption in Mixed Component Trees<\/strong><\/h2>\n\n\n\n<p>Svelte 5 doesn&#8217;t force you to migrate everything at once. You can run Svelte 4 syntax and Svelte 5 runes side by side in the same app. The key constraint: each individual .svelte file must use <em>one<\/em> syntax or the other.<\/p>\n\n\n\n<p>Your app can look like this during migration:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>LegacyComponent.svelte uses let, $:, export let (Svelte 4 syntax)<\/li>\n\n\n\n<li>NewComponent.svelte uses $state, $derived, $props (Svelte 5 runes)<\/li>\n\n\n\n<li>AnotherLegacy.svelte uses Svelte 4 syntax<\/li>\n<\/ul>\n\n\n\n<p>All three work together in the same component tree. A runes-based parent can render a legacy child, and vice versa. Props pass between them normally.<\/p>\n\n\n\n<p>The rule is simple: within a single component file, you cannot mix $: with $derived or export let with $props(). Pick one model per file.<\/p>\n\n\n\n<p>The svelte\/legacy module provides backward-compatible APIs for patterns that can&#8217;t be replaced one-to-one. Use these as temporary bridges, not permanent solutions.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>createClassComponent and asClassComponent let you wrap Svelte 5 components in the old class-based API for third-party libraries that expect it.<\/li>\n\n\n\n<li>beforeUpdate and afterUpdate lifecycle functions still work through svelte\/legacy but should eventually become $effect.pre and $effect.<\/li>\n<\/ul>\n\n\n\n<p>Keep legacy APIs when a third-party dependency requires them or when a component is too complex to migrate safely in a single pass.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>SvelteKit, Vite, SSR, and Hydration Checks During Upgrade<\/strong><\/h3>\n\n\n\n<p>If you&#8217;re using SvelteKit, the framework version needs to match your Svelte version. Upgrade both together.<\/p>\n\n\n\n<p>Key things to check after the upgrade:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>SSR output:<\/strong> Run your app in SSR mode and compare the HTML output before and after migration. Runes components may render differently if $effect code runs logic that previously ran during SSR via $: blocks. $effect does not run on the server.<\/li>\n\n\n\n<li><strong>Hydration:<\/strong> If SSR output changes, hydration mismatches will appear in the browser console. Look for warnings about mismatched attributes or text content.<\/li>\n\n\n\n<li><strong>Vite config:<\/strong> No changes needed to vite.config.js for the Svelte 5 upgrade itself, but check that your @sveltejs\/vite-plugin-svelte version is compatible.<\/li>\n\n\n\n<li><strong>mount<\/strong><strong>, <\/strong><strong>hydrate<\/strong><strong>, <\/strong><strong>unmount<\/strong><strong>:<\/strong> These new functions replace the old new Component() class instantiation pattern. If you manually mount components in non-SvelteKit contexts (such as embedding Svelte in other frameworks), update these calls accordingly.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Using <\/strong><strong>sv migrate<\/strong><strong> and What It Won&#8217;t Fix<\/strong><\/h2>\n\n\n\n<p>Run the official migration script with:<\/p>\n\n\n\n<p>npx sv migrate svelte-5<\/p>\n\n\n\n<p>This tool handles:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Converting let to $state() for top-level reactive variables<\/li>\n\n\n\n<li>Rewriting $: to $derived or $effect (best-guess)<\/li>\n\n\n\n<li>Converting export let to $props()<\/li>\n\n\n\n<li>Updating on:event to onevent attributes<\/li>\n\n\n\n<li>Adding @migration comments where it can&#8217;t determine the correct transform<\/li>\n<\/ul>\n\n\n\n<p>It does <em>not<\/em> handle:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Splitting $: blocks that mix derivation and side effects<\/li>\n\n\n\n<li>Adding $bindable() where bind: is used<\/li>\n\n\n\n<li>Converting createEventDispatcher to callback props<\/li>\n\n\n\n<li>Restructuring store-based global state into .svelte.js modules<\/li>\n\n\n\n<li>Fixing logic that depends on implicit reactivity timing<\/li>\n<\/ul>\n\n\n\n<p>Search your codebase for @migration after running the tool. Each annotation marks a spot that needs manual review. Treat the script as a first pass, not a finish line: it clears the mechanical work so you can spend your review time on the risk tiers that actually cause bugs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Common Breakage Patterns and How to Fix Them Fast<\/strong><\/h2>\n\n\n\n<p>These are the specific bugs teams hit most often during Svelte 5 migration. Each one has a clear cause and a direct fix.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Lost reactivity from destructuring stateful objects.<\/strong> Destructuring a $state object (let { name, age } = user) copies plain values, not reactive references, so the UI stops updating. Fix: access properties directly from the reactive object, or wrap each value you need in $derived(user.name).<br><\/li>\n\n\n\n<li><strong>Infinite loops inside <\/strong><strong>$effect<\/strong><strong>.<\/strong> An $effect that reads and writes the same reactive value (count = count + 1 inside its own effect) triggers itself endlessly. Fix: use $effect only for side effects that don&#8217;t mutate their own dependencies; use $derived for transforms, and untrack for the rare case where you must write to state inside an effect.<br><\/li>\n\n\n\n<li><strong>Stale closures in event callbacks.<\/strong> Handlers passed through multiple component layers or stored in a non-reactive variable can hold an outdated value instead of reading the current one. Fix: read $state values inside the callback body at call time, not in an outer scope captured once.<br><\/li>\n\n\n\n<li><strong>SSR and hydration mismatches after refactors.<\/strong> $effect doesn&#8217;t run during server-side rendering, so moving $: logic that affected initial HTML into $effect creates a mismatch between server and client output. Fix: use $derived for any value that must appear in server-rendered HTML; reserve $effect for client-only work like DOM manipulation or analytics.<br><\/li>\n\n\n\n<li><strong>Library edge cases in UI and utility layers.<\/strong> Older component libraries built for Svelte 4 slots, or utility libraries that compare Svelte state by reference, may break against runes-based proxies. Fix: check for a Svelte 5-compatible release of the library, use svelte\/legacy wrappers where needed, and call $state.snapshot(data) before passing reactive state into non-Svelte code (for example, a charting library):<br><\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\n\n&nbsp;&nbsp;let data = $state(&#91;1, 2, 3]);\n\n&nbsp;&nbsp;function sendToChartLib() {\n\n&nbsp;&nbsp;&nbsp;&nbsp;chartLib.update($state.snapshot(data));\n\n&nbsp;&nbsp;}\n\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Frequently Asked Questions<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Is Svelte 5 backward compatible with Svelte 4?<\/strong><\/h3>\n\n\n\n<p>Yes. Svelte 5 runs Svelte 4 syntax and Svelte 5 runes side by side in the same project, so most teams upgrade the core dependency with only a few lines of code changed initially. The constraint is per-file, not per-app: a single .svelte file must use one syntax or the other, but a legacy component and a runes-based component can sit in the same tree and pass props normally.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Do I have to rewrite my entire app to use runes?<\/strong><\/h3>\n\n\n\n<p>No. You can upgrade the svelte package version, keep every existing component on let, $:, and export let, and migrate components one at a time whenever you touch them. There&#8217;s no deprecation deadline forcing an all-at-once rewrite, which is why the risk-ordered approach in this guide works: you migrate the highest-risk patterns first and leave lower-risk local state for whenever you get to it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Is <\/strong><strong>$:<\/strong><strong> still supported in Svelte 5?<\/strong><\/h3>\n\n\n\n<p>$: still compiles and runs in components that haven&#8217;t adopted runes. But you cannot mix $: with $derived or $effect in the same component file. Once you add any rune to a file, that file is in &#8220;runes mode,&#8221; and $: blocks in it will not behave as expected.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>What is the difference between <\/strong><strong>$state<\/strong><strong> and a regular <\/strong><strong>let<\/strong><strong>?<\/strong><\/h3>\n\n\n\n<p>A plain let in a Svelte 5 component is not reactive on its own. Wrapping the initial value in $state() is what makes Svelte track reads and writes to that variable and update the DOM when it changes. Outside of that wrapper, the variable behaves like normal JavaScript.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Are Svelte stores deprecated in Svelte 5?<\/strong><\/h3>\n\n\n\n<p>No, writable, readable, and derived from svelte\/store are still shipped and maintained. But the main reasons to reach for them, sharing reactive state across files and extracting reusable logic, are largely covered by runes now, since $state and $derived work inside .svelte.js and .svelte.ts files. Most new shared state should use runes; keep stores for cases like RxJS interop or third-party libraries that expect the store contract.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Will the <\/strong><strong>sv migrate<\/strong><strong> command break my app?<\/strong><\/h3>\n\n\n\n<p>It can introduce issues if you run it across a large codebase in one pass and don&#8217;t review the output. The tool handles mechanical transforms reliably, like converting let to $state(), but it makes best-guess decisions on $: blocks that mix derivations with side effects, and it flags anything it&#8217;s unsure about with an @migration comment. Commit your changes before running it, review every flagged spot, and test SSR output if you&#8217;re on SvelteKit.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Migrate in Risk Order, Not File Order<\/strong><\/h2>\n\n\n\n<p>Start with shared stores and cross-component reactive flows, because those create the most subtle bugs. Move to mixed $: blocks that need splitting into $derived and $effect. Then handle props and bindings. Save local state for last since it&#8217;s the safest and most automatable step.<\/p>\n\n\n\n<p>Run npx sv migrate svelte-5 early to handle mechanical transforms, then search for @migration comments to find the spots that need your judgment. Test SSR output at each step if you use SvelteKit. Keep svelte\/legacy APIs as temporary bridges for third-party dependencies, not as long-term solutions.<\/p>\n\n\n\n<p>The sequencing protects you from the hardest class of bugs: silent reactivity loss and timing changes that don&#8217;t throw errors but change behavior.<\/p>\n\n\n\n<p>If your team is mid-migration and needs an extra hand, <strong>Arc <\/strong>vets frontend engineers on the specific frameworks they claim expertise in, including hands-on experience with Svelte 5 and runes. That means you&#8217;re matching with someone who has already split a $: block into $derived and $effect on a real codebase, so there&#8217;s no ramp-up period spent explaining what runes are.&nbsp;<\/p>\n\n\n\n<p>Arc&#8217;s vetting process includes live technical assessments, so you&#8217;re reviewing engineers who&#8217;ve already cleared a skill bar, not a stack of resumes. <a href=\"https:\/\/arc.dev\/hire-developers\/svelte\"><strong>Explore vetted Svelte engineers<\/strong><\/a><strong> and get access to global talent.<\/strong><\/p>\n\n\n\n<script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"TechArticle\",\n      \"headline\": \"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity\",\n      \"description\": \"Svelte 5 runes replace Svelte 4's implicit reactivity with an explicit syntax. This risk-ordered migration guide shows what breaks first and how to fix it.\",\n      \"image\": \"https:\/\/cdn-employer-wp.arc.dev\/wp-content\/uploads\/2026\/08\/svelte-5-runes-migration-guide-1128x635.png\",\n      \"author\": {\n        \"@type\": \"Organization\",\n        \"name\": \"Arc\",\n        \"url\": \"https:\/\/arc.dev\"\n      },\n      \"publisher\": {\n        \"@type\": \"Organization\",\n        \"name\": \"Arc\",\n        \"logo\": {\n          \"@type\": \"ImageObject\",\n          \"url\": \"https:\/\/cdn.arc.dev\/arc-next-landing\/images\/arc\/share-logo.png\"\n        }\n      },\n      \"mainEntityOfPage\": {\n        \"@type\": \"WebPage\",\n        \"@id\": \"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\"\n      },\n      \"datePublished\": \"2026-08-10\",\n      \"dateModified\": \"2026-08-10\"\n    },\n    {\n      \"@type\": \"FAQPage\",\n      \"mainEntity\": [\n        {\n          \"@type\": \"Question\",\n          \"name\": \"Is Svelte 5 backward compatible with Svelte 4?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"Yes. Svelte 5 runs Svelte 4 syntax and Svelte 5 runes side by side in the same project, so most teams upgrade the core dependency with only a few lines of code changed initially. The constraint is per-file, not per-app: a single .svelte file must use one syntax or the other, but a legacy component and a runes-based component can sit in the same tree and pass props normally.\"\n          }\n        },\n        {\n          \"@type\": \"Question\",\n          \"name\": \"Do I have to rewrite my entire app to use runes?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"No. You can upgrade the svelte package version, keep every existing component on let, $:, and export let, and migrate components one at a time whenever you touch them. There's no deprecation deadline forcing an all-at-once rewrite, which is why the risk-ordered approach in this guide works: you migrate the highest-risk patterns first and leave lower-risk local state for whenever you get to it.\"\n          }\n        },\n        {\n          \"@type\": \"Question\",\n          \"name\": \"Is $: still supported in Svelte 5?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"$: still compiles and runs in components that haven't adopted runes. But you cannot mix $: with $derived or $effect in the same component file. Once you add any rune to a file, that file is in runes mode, and $: blocks in it will not behave as expected.\"\n          }\n        },\n        {\n          \"@type\": \"Question\",\n          \"name\": \"What is the difference between $state and a regular let?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"A plain let in a Svelte 5 component is not reactive on its own. Wrapping the initial value in $state() is what makes Svelte track reads and writes to that variable and update the DOM when it changes. Outside of that wrapper, the variable behaves like normal JavaScript.\"\n          }\n        },\n        {\n          \"@type\": \"Question\",\n          \"name\": \"Are Svelte stores deprecated in Svelte 5?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"No, writable, readable, and derived from svelte\/store are still shipped and maintained. But the main reasons to reach for them, sharing reactive state across files and extracting reusable logic, are largely covered by runes now, since $state and $derived work inside .svelte.js and .svelte.ts files. Most new shared state should use runes; keep stores for cases like RxJS interop or third-party libraries that expect the store contract.\"\n          }\n        },\n        {\n          \"@type\": \"Question\",\n          \"name\": \"Will the sv migrate command break my app?\",\n          \"acceptedAnswer\": {\n            \"@type\": \"Answer\",\n            \"text\": \"It can introduce issues if you run it across a large codebase in one pass and don't review the output. The tool handles mechanical transforms reliably, like converting let to $state(), but it makes best-guess decisions on $: blocks that mix derivations with side effects, and it flags anything it's unsure about with an @migration comment. Commit your changes before running it, review every flagged spot, and test SSR output if you're on SvelteKit.\"\n          }\n        }\n      ]\n    }\n  ]\n}\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Svelte 5 runes are the biggest API shift in the framework&#8217;s history, and most teams stall on the upgrade because the release notes don&#8217;t tell you what breaks first. Runes replace Svelte 4&#8217;s implicit, compiler-magic reactivity (built on let, export let, and $:) with an explicit syntax: $state, $derived, $effect, and $props. The old patterns [&hellip;]<\/p>\n","protected":false},"author":15,"featured_media":5248,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[93],"tags":[],"class_list":["post-5247","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.2 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Svelte 5 Runes: What Changed and How to Migrate Without Breaking Reactivity - Arc Employer Blog<\/title>\n<meta name=\"description\" content=\"Svelte 5 runes replace $: and export let with explicit reactivity. This Svelte 5 migration guide gives you a risk-ordered path, so upgrades don&#039;t break.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Svelte 5 Runes: What Changed and How to Migrate Without Breaking Reactivity - Arc Employer Blog\" \/>\n<meta property=\"og:description\" content=\"Svelte 5 runes replace $: and export let with explicit reactivity. This Svelte 5 migration guide gives you a risk-ordered path, so upgrades don&#039;t break.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/\" \/>\n<meta property=\"og:site_name\" content=\"Arc Employer Blog\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/arcdotdev\" \/>\n<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/arcdotdev\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-10T11:02:05+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-11T14:39:07+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2026\/08\/svelte-5-runes-migration-guide.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1672\" \/>\n\t<meta property=\"og:image:height\" content=\"941\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"The Arc Team\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@arcdotdev\" \/>\n<meta name=\"twitter:site\" content=\"@arcdotdev\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"The Arc Team\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"21 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/\"},\"author\":{\"name\":\"The Arc Team\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#\\\/schema\\\/person\\\/08dd4743f5c0f965590e77094c5579bc\"},\"headline\":\"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity\",\"datePublished\":\"2026-08-10T11:02:05+00:00\",\"dateModified\":\"2026-08-11T14:39:07+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/\"},\"wordCount\":3481,\"publisher\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/svelte-5-runes-migration-guide.png\",\"articleSection\":[\"Development\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/\",\"url\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/\",\"name\":\"Svelte 5 Runes: What Changed and How to Migrate Without Breaking Reactivity - Arc Employer Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/svelte-5-runes-migration-guide.png\",\"datePublished\":\"2026-08-10T11:02:05+00:00\",\"dateModified\":\"2026-08-11T14:39:07+00:00\",\"description\":\"Svelte 5 runes replace $: and export let with explicit reactivity. This Svelte 5 migration guide gives you a risk-ordered path, so upgrades don't break.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/#primaryimage\",\"url\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/svelte-5-runes-migration-guide.png\",\"contentUrl\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/svelte-5-runes-migration-guide.png\",\"width\":1672,\"height\":941,\"caption\":\"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/svelte-5-runes-migration-guide\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#website\",\"url\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/\",\"name\":\"Arc Employer Blog\",\"description\":\"Insights on hiring and remote work\",\"publisher\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#organization\",\"name\":\"Arc.dev\",\"url\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/wp-content\\\/uploads\\\/2022\\\/02\\\/Arc-alternate-logo.png\",\"contentUrl\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/wp-content\\\/uploads\\\/2022\\\/02\\\/Arc-alternate-logo.png\",\"width\":512,\"height\":512,\"caption\":\"Arc.dev\"},\"image\":{\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/arcdotdev\",\"https:\\\/\\\/x.com\\\/arcdotdev\",\"https:\\\/\\\/www.instagram.com\\\/arcdotdev\\\/\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/arcdotdev\",\"https:\\\/\\\/www.youtube.com\\\/c\\\/Arcdotdev\"]},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/#\\\/schema\\\/person\\\/08dd4743f5c0f965590e77094c5579bc\",\"name\":\"The Arc Team\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/c1380473325c827343a6d47c7b5d6916c147171af99760766d2acb56da62ed02?s=96&d=mm&r=pg\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/c1380473325c827343a6d47c7b5d6916c147171af99760766d2acb56da62ed02?s=96&d=mm&r=pg\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/c1380473325c827343a6d47c7b5d6916c147171af99760766d2acb56da62ed02?s=96&d=mm&r=pg\",\"caption\":\"The Arc Team\"},\"description\":\"The Arc team provides articles and expert advice on tech careers and remote work. From helping beginners land their first junior role to supporting remote workers facing challenges at home or guiding mid-level professionals toward leadership, Arc covers it all!\",\"sameAs\":[\"https:\\\/\\\/arc.dev\\\/developer-blog\\\/\",\"https:\\\/\\\/www.facebook.com\\\/arcdotdev\",\"https:\\\/\\\/www.instagram.com\\\/arcdotdev\\\/\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/arcdotdev\",\"https:\\\/\\\/x.com\\\/arcdotdev\",\"https:\\\/\\\/www.youtube.com\\\/c\\\/Arcdotdev\"],\"url\":\"https:\\\/\\\/arc.dev\\\/employer-blog\\\/author\\\/thearcteam\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Svelte 5 Runes: What Changed and How to Migrate Without Breaking Reactivity - Arc Employer Blog","description":"Svelte 5 runes replace $: and export let with explicit reactivity. This Svelte 5 migration guide gives you a risk-ordered path, so upgrades don't break.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/","og_locale":"en_US","og_type":"article","og_title":"Svelte 5 Runes: What Changed and How to Migrate Without Breaking Reactivity - Arc Employer Blog","og_description":"Svelte 5 runes replace $: and export let with explicit reactivity. This Svelte 5 migration guide gives you a risk-ordered path, so upgrades don't break.","og_url":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/","og_site_name":"Arc Employer Blog","article_publisher":"https:\/\/www.facebook.com\/arcdotdev","article_author":"https:\/\/www.facebook.com\/arcdotdev","article_published_time":"2026-08-10T11:02:05+00:00","article_modified_time":"2026-08-11T14:39:07+00:00","og_image":[{"width":1672,"height":941,"url":"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2026\/08\/svelte-5-runes-migration-guide.png","type":"image\/png"}],"author":"The Arc Team","twitter_card":"summary_large_image","twitter_creator":"@arcdotdev","twitter_site":"@arcdotdev","twitter_misc":{"Written by":"The Arc Team","Est. reading time":"21 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/#article","isPartOf":{"@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/"},"author":{"name":"The Arc Team","@id":"https:\/\/arc.dev\/employer-blog\/#\/schema\/person\/08dd4743f5c0f965590e77094c5579bc"},"headline":"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity","datePublished":"2026-08-10T11:02:05+00:00","dateModified":"2026-08-11T14:39:07+00:00","mainEntityOfPage":{"@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/"},"wordCount":3481,"publisher":{"@id":"https:\/\/arc.dev\/employer-blog\/#organization"},"image":{"@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/#primaryimage"},"thumbnailUrl":"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2026\/08\/svelte-5-runes-migration-guide.png","articleSection":["Development"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/","url":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/","name":"Svelte 5 Runes: What Changed and How to Migrate Without Breaking Reactivity - Arc Employer Blog","isPartOf":{"@id":"https:\/\/arc.dev\/employer-blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/#primaryimage"},"image":{"@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/#primaryimage"},"thumbnailUrl":"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2026\/08\/svelte-5-runes-migration-guide.png","datePublished":"2026-08-10T11:02:05+00:00","dateModified":"2026-08-11T14:39:07+00:00","description":"Svelte 5 runes replace $: and export let with explicit reactivity. This Svelte 5 migration guide gives you a risk-ordered path, so upgrades don't break.","breadcrumb":{"@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/#primaryimage","url":"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2026\/08\/svelte-5-runes-migration-guide.png","contentUrl":"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2026\/08\/svelte-5-runes-migration-guide.png","width":1672,"height":941,"caption":"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity"},{"@type":"BreadcrumbList","@id":"https:\/\/arc.dev\/employer-blog\/svelte-5-runes-migration-guide\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/arc.dev\/employer-blog\/"},{"@type":"ListItem","position":2,"name":"Svelte 5 and Runes: What Actually Changed and How to Migrate Without Breaking Reactivity"}]},{"@type":"WebSite","@id":"https:\/\/arc.dev\/employer-blog\/#website","url":"https:\/\/arc.dev\/employer-blog\/","name":"Arc Employer Blog","description":"Insights on hiring and remote work","publisher":{"@id":"https:\/\/arc.dev\/employer-blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/arc.dev\/employer-blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/arc.dev\/employer-blog\/#organization","name":"Arc.dev","url":"https:\/\/arc.dev\/employer-blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/arc.dev\/employer-blog\/#\/schema\/logo\/image\/","url":"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2022\/02\/Arc-alternate-logo.png","contentUrl":"https:\/\/arc.dev\/employer-blog\/wp-content\/uploads\/2022\/02\/Arc-alternate-logo.png","width":512,"height":512,"caption":"Arc.dev"},"image":{"@id":"https:\/\/arc.dev\/employer-blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/arcdotdev","https:\/\/x.com\/arcdotdev","https:\/\/www.instagram.com\/arcdotdev\/","https:\/\/www.linkedin.com\/company\/arcdotdev","https:\/\/www.youtube.com\/c\/Arcdotdev"]},{"@type":"Person","@id":"https:\/\/arc.dev\/employer-blog\/#\/schema\/person\/08dd4743f5c0f965590e77094c5579bc","name":"The Arc Team","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/c1380473325c827343a6d47c7b5d6916c147171af99760766d2acb56da62ed02?s=96&d=mm&r=pg","url":"https:\/\/secure.gravatar.com\/avatar\/c1380473325c827343a6d47c7b5d6916c147171af99760766d2acb56da62ed02?s=96&d=mm&r=pg","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/c1380473325c827343a6d47c7b5d6916c147171af99760766d2acb56da62ed02?s=96&d=mm&r=pg","caption":"The Arc Team"},"description":"The Arc team provides articles and expert advice on tech careers and remote work. From helping beginners land their first junior role to supporting remote workers facing challenges at home or guiding mid-level professionals toward leadership, Arc covers it all!","sameAs":["https:\/\/arc.dev\/developer-blog\/","https:\/\/www.facebook.com\/arcdotdev","https:\/\/www.instagram.com\/arcdotdev\/","https:\/\/www.linkedin.com\/company\/arcdotdev","https:\/\/x.com\/arcdotdev","https:\/\/www.youtube.com\/c\/Arcdotdev"],"url":"https:\/\/arc.dev\/employer-blog\/author\/thearcteam\/"}]}},"_links":{"self":[{"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/posts\/5247","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/users\/15"}],"replies":[{"embeddable":true,"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/comments?post=5247"}],"version-history":[{"count":1,"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/posts\/5247\/revisions"}],"predecessor-version":[{"id":5255,"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/posts\/5247\/revisions\/5255"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/media\/5248"}],"wp:attachment":[{"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/media?parent=5247"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/categories?post=5247"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/arc.dev\/employer-blog\/wp-json\/wp\/v2\/tags?post=5247"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}