--- title: Svelte 5 migration guide --- Version 5 comes with an overhauled syntax and reactivity system. While it may look different at first, you'll soon notice many similarities. This guide goes over the changes in detail and shows you how to upgrade. Along with it, we also provide information on _why_ we did these changes. You don't have to migrate to the new syntax right away - Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically. ## Reactivity syntax changes At the heart of Svelte 5 is the new runes API. Runes are basically compiler instructions that inform Svelte about reactivity. Syntactically, runes are functions starting with a dollar-sign. ### let -> $state In Svelte 4, a `let` declaration at the top level of a component was implicitly reactive. In Svelte 5, things are more explicit: a variable is reactive when created using the `$state` rune. Let's migrate the counter to runes mode by wrapping the counter in `$state`: ```svelte ``` Nothing else changes. `count` is still the number itself, and you read and write directly to it, without a wrapper like `.value` or `getCount()`. > [!DETAILS] Why we did this > `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API in an outside the top level of components. Head to TODO LINK TO TUTORIAL to learn more. ### $: -> $derived/$effect In Svelte 4, a `$:` statement at the top level of a component could be used to declare a derivation, i.e. state that is entirely defined through a computation of other state. In Svelte 5, this is achieved using the `$derived` rune: ```svelte ``` As with `$state`, nothing else changes. `double` is still the number itself, and you read it directly, without a wrapper like `.value` or `getCount()`. A `$:` statement could also be used to create side effects. In Svelte 5, this is achieved using the `$effect` rune: ```svelte ``` > [!DETAILS] Why we did this > `$:` was a great shorthand and easy to get started with: you could slap a `$:` in front of most code and it would somehow work. This intuitiveness was also its drawback the more complicated your code became, because it wasn't as easy to reason about. Was the intent of the code to create a derivation, or a side effect? With `$derived` and `$effect`, you have a bit more up-front decision making to do (spoiler alert: 90% of the time you want `$derived`), but future-you and other developers on your team will have an easier time. > > There were also gotchas that were hard to spot: > > - `$:` only updated directly before rendering, which meant you could read stale values in-between rerenders > - `$:` only ran once per tick, which meant that statements may run less often than you think > - `$:` dependencies were determined through static analysis of the dependencies. This worked in most cases, but could break in subtle ways during a refactoring where dependencies would be > for example moved into a function and no longer be visible as a result > - `$:` statements were also ordered by using static analysis of the dependencies. In some cases there could be ties and the ordering would be wrong as a result, needing manual > interventions. Ordering could also break while refactoring code and some dependencies no longer being visible as a result. > > Lastly, it wasn't TypeScript-friendly (our editor tooling had to jump through some hoops to make it valid for TypeScript), which was a blocker for making Svelte's reactivity model truly > universal. > > `$derived` and `$effect` fix all of these by > > - always returning the latest value > - running as often as needed to be stable > - determining the dependencies at runtime, and therefore being immune to refactorings > - executing dependencies as needed and therefore being immune to ordering problems > - being TypeScript-friendly ### export let -> $props In Svelte 4, properties of a component were declared using `export let`. Each property was one declaration. In Svelte 5, all properties are declared through the `$props` rune, through destructuring: ```svelte ``` There are multiple cases where declaring properties becomes less straightforward than having a few `export let` declarations: - you want to rename the property, for example because the name is a reserved identifier (e.g. `class`) - you don't know which other properties to expect in advance - you want to forward every property to another component All these cases need special syntax in Svelte 4: - renaming: `export { klass as class}` - other properties: `$$restProps` - all properties `$$props` In Svelte 5, the `$props` rune makes this straightforward without any additional Svelte-specific syntax: - renaming: use property renaming `let { class: klass } = $props();` - other properties: use spreading `let { foo, bar, ...rest } = $props();` - all properties: don't destructure `let props = $props();` ```svelte ``` > [!DETAILS] Why we did this > `export let` was one of the more controversial API decisions, and there was a lot of debate about whether you should think about a property being `export`ed or `import`ed. `$props` doesn't have this trait. It's also in line with the other runes, and the general thinking reduces to "everything special to reactivity in Svelte is a rune". > > There were also a lot of limitations around `export let`, which required additional API, as shown above. `$props` unite this in one syntactical concept that leans heavily on regular JavaScript destructuring syntax. ## Event changes Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words - remove the colon): ```svelte ``` Since they're just properties, you can use the normal shorthand syntax... ```svelte ``` ...though when using a named event handler function it's usually better to use a more descriptive name. ### Component events In Svelte 4, components could emit events by creating a dispatcher with `createEventDispatcher`. This function is deprecated in Svelte 5. Instead, components should accept _callback props_ - which means you then pass functions as properties to these components: ```svelte { size += power---.details---; if (size > 75) burst = true; }} ---on:---deflate={(power) => { if (size > 0) size -= power---.details---; }} /> {#if burst} 💥 {:else} 🎈 {/if} ``` ```svelte Pump power: {power} ``` ### Bubbling events Instead of doing ` ``` Note that this also means you can 'spread' event handlers onto the element along with other props instead of tediously forwarding each event separately: ```svelte ``` ### Event modifiers In Svelte 4, you can add event modifiers to handlers: ```svelte ``` Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers. Since event handlers are just functions, you can create your own wrappers as necessary: ```svelte ``` There are three modifiers — `capture`, `passive` and `nonpassive` — that can't be expressed as wrapper functions, since they need to be applied when the event handler is bound rather than when it runs. For `capture`, we add the modifier to the event name: ```svelte ``` Changing the [`passive`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) option of an event handler, meanwhile, is not something to be done lightly. If you have a use case for it — and you probably don't! — then you will need to use an action to apply the event handler yourself. ### Multiple event handlers In Svelte 4, this is possible: ```svelte ``` Duplicate attributes/properties on elements — which now includes event handlers — are not allowed. Instead, do this: ```svelte ``` When spreading props, local event handlers must go _after_ the spread, or they risk being overwritten: ```svelte ``` > [!DETAILS] Why we did this > `createEventDispatcher` was always a bit boilerplate-y: > > - import the function > - call the function to get a dispatch function > - call said dispatch function with a string and possibly a payload > - retrieve said payload on the other end through a `.details` property, because the event itself was always a `CustomEvent` > > It was always possible to use component callback props, but because you had to listen to dom events using `on:`, it made sense to use `createEventDispatcher` for component events due to > syntactical consiscency. Now that we have event attributes (`onclick`), it's the other way around: Callback props are now the more sensible thing to do. > > The removal of event modifiers is arguably one of the changes that seems like a step back for those who've liked the shorthand syntax of event modifiers. Given that they are not used that > frequently, we traded a smaller surface area for more explicitness. Modifiers also were inconsistent, because most of them were only useable on Dom elements. > > Multiple listeners for the same event are also no longer possible, but it was something of an anti-pattern anyway, since it impedes readability: if there are many attributes, it becomes > harder to spot that there are two handlers unless they are right next to each other. It also implies that the two handlers are independent, when in fact something like `event.> stopImmediatePropagation()` inside `one` would prevent `two` from being called. > > By deprecating `createEventDispatcher` and the `on:` directive in favour of callback props and normal element properties, we: > > - reduce Svelte's learning curve > - remove boilerplate, particularly around `createEventDispatcher` > - remove the overhead of creating `CustomEvent` objects for events that may not even have listeners > - add the ability to spread event handlers > - add the ability to know which event handlers were provided to a component > - add the ability to express whether a given event handler is required or optional > - increase type safety (previously, it was effectively impossible for Svelte to guarantee that a component didn't emit a particular event) ## Snippets instead of slots In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets which are more powerful and flexible, and as such slots are deprecated in Svelte 5. They continue to work, however, and you can mix and match snippets and slots in your components. When using custom elements, you should still use `` like before. In a future version, when Svelte removes its internal version of slots, it will leave those slots as-is, i.e. output a regular DOM tag instead of transforming it. ### Default content In Svelte 4, the easiest way to pass a piece of UI to the child was using a ``. In Svelte 5, this is done using the `children` prop instead, which is then shown with `{@render children()}`: ```svelte ------ +++{@render children?.()}+++ ``` ### Multiple content placeholders If you wanted multiple UI placeholders, you had to use named slots. In Svelte 5, use props instead, name them however you like and `{@render ...}` them: ```svelte
------ +++{@render header()}+++
------ +++{@render main()}+++
------ +++{@render footer()}+++
``` ### Passing data back up In Svelte 4, you would pass data to a `` and then retrieve it with `let:` in the parent component. In Svelte 5, snippets take on that responsibility: ```svelte +++{#snippet item(text)}+++ {text} +++{/snippet}+++ ---No items yet--- +++{#snippet empty()} No items yet {/snippet}+++ ``` ```svelte {#if items.length}
    {#each items as entry}
  • ------ +++{@render item(entry)}+++
  • {/each}
{:else} ------ +++{@render empty?.()}+++ {/if} ``` > [!DETAILS] Why we did this > Slots were easy to get started with, but the more advanced the use case became, the more involved and confusing the syntax became: > > - the `let:` syntax was confusing to many people as it _creates_ a variable whereas all other `:` directives _receive_ a variable > - the scope of a variable declared with `let:` wasn't clear. In the example above, it may look like you can use the `item` slot prop in the `empty` slot, but that's not true > - named slots had to be applied to an element using the `slot` attribute. Sometimes you didn't want to create an element, so we had to add the `` API > - named slots could also be applied to a component, which changed the semantics of where `let:` directives are available (even today us maintainers often don't know which way around it > works) > > Snippets solve all of these problems by being much more readable and clear. At the same time they're more powerful as they allow you to define sections of UI that you can render _anywhere_, not just passing them as props to a component. ## Migration script By now you should have a pretty good understanding of the before/after and how the old syntax relates to the new syntax. It probably also became clear that a lot of these migrations are rather technical and repetitive - something you don't want to do by hand. We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things: - bump core dependencies in your `package.json` - migrate to runes (`let` -> `$state` etc) - migrate to event attributes for Dom elements (`on:click` -> `onclick`) - migrate slot creations to render tags (`` -> `{@render children()}`) - migrate slot usages to snippets (`
...
` -> `{#snippet x()}
...
{/snippet}`) - migrate obvious component creations (`new Component(...)` -> `mount(Component, ...)`) You can also migrate a single component in VS Code through the `Migrate Component to Svelte 5 Syntax` command, or in our Playground through the `Migrate` button. Not everything can be migrated automatically, and some migrations need manual cleanup afterwards. The following sections describe these in more detail. ### run You may see that the migration script converts some of your `$:` statements to a `run` function which is imported from `svelte/legacy`. This happens if the migration script couldn't reliably migrate the statement to a `$derived` and concluded this is a side effect instead. In some cases this may be wrong and it's best to change this to use a `$derived` instead. In other cases it may be right, but since `$:` statements also ran on the server but `$effect` does not, it isn't safe to transform it as such. Instead, `run` is used as a stopgap solution. `run` mimics most of the characteristics of `$:`, in that it runs on the server once, and runs as `$effect.pre` on the client (`$effect.pre` runs _before_ changes are applied to the DOM; most likely you want to use `$effect` instead). ```svelte ``` ### Event modifiers Event modifiers are not applicable to event attributes (e.g. you can't do `onclick|preventDefault={...}`). Therefore, when migrating event directives to event attributes, we need a function-replacement for these modifiers. These are imported from `svelte/legacy`, and should be migrated away from in favor of e.g. just using `event.preventDefault()`. ```svelte ``` ### Things that are not automigrated The migration script does not convert `createEventDispatcher`. You need to adjust those parts manually. It doesn't do it because it's too risky because it could result in breakage for users of the component, which the migration script cannot find out. The migration script does not convert `beforeUpdate/afterUpdate`. It doesn't do it because it's impossible to determine the actual intent of the code. As a rule of thumb you can often go with a combination of `$effect.pre` (runs at the same time as `beforeUpdate` did) and `tick` (imported from `svelte`, allows you to wait until changes are applied to the DOM and then do some work). ## Components are no longer classes In Svelte 3 and 4, components are classes. In Svelte 5 they are functions and should be instantiated differently. If you need to manually instantiate components, you should use `mount` or `hydrate` (imported from `svelte`) instead. If you see this error using SvelteKit, try updating to the latest version of SvelteKit first, which adds support for Svelte 5. If you're using Svelte without SvelteKit, you'll likely have a `main.js` file (or similar) which you need to adjust: ```js +++import { mount } from 'svelte';+++ import App from './App.svelte' ---const app = new App({ target: document.getElementById("app") });--- +++const app = mount(App, { target: document.getElementById("app") });+++ export default app; ``` `mount` and `hydrate` have the exact same API. The difference is that `hydrate` will pick up the Svelte's server-rendered HTML inside its target and hydrate it. Both return an object with the exports of the component and potentially property accessors (if compiled with `accessors: true`). They do not come with the `$on`, `$set` and `$destroy` methods you may know from the class component API. These are its replacements: For `$on`, instead of listening to events, pass them via the `events` property on the options argument. ```js +++import { mount } from 'svelte';+++ import App from './App.svelte' ---const app = new App({ target: document.getElementById("app") }); app.$on('event', callback);--- +++const app = mount(App, { target: document.getElementById("app"), events: { event: callback } });+++ ``` > [!NOTE] Note that using `events` is discouraged — instead, [use callbacks](#Event-changes) For `$set`, use `$state` instead to create a reactive property object and manipulate it. If you're doing this inside a `.js` or `.ts` file, adjust the ending to include `.svelte`, i.e. `.svelte.js` or `.svelte.ts`. ```js +++import { mount } from 'svelte';+++ import App from './App.svelte' ---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } }); app.$set('event', { foo: 'baz' });--- +++const props = $state({ foo: 'bar' }); const app = mount(App, { target: document.getElementById("app"), props }); props.foo = 'baz';+++ ``` For `$destroy`, use `unmount` instead. ```js +++import { mount, unmount } from 'svelte';+++ import App from './App.svelte' ---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } }); app.$destroy();--- +++const app = mount(App, { target: document.getElementById("app") }); unmount(app);+++ ``` As a stop-gap-solution, you can also use `createClassComponent` or `asClassComponent` (imported from `svelte/legacy`) instead to keep the same API known from Svelte 4 after instantiating. ```js +++import { createClassComponent } from 'svelte/legacy';+++ import App from './App.svelte' ---const app = new App({ target: document.getElementById("app") });--- +++const app = createClassComponent({ component: App, target: document.getElementById("app") });+++ export default app; ``` If this component is not under your control, you can use the `compatibility.componentApi` compiler option for auto-applied backwards compatibility, which means code using `new Component(...)` keeps working without adjustments (note that this adds a bit of overhead to each component). This will also add `$set` and `$on` methods for all component instances you get through `bind:this`. ```js /// svelte.config.js export default { compilerOptions: { compatibility: { componentApi: 4 } } }; ``` Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount` won't have been called by the time the function returns and the pending block of promises will not have been rendered yet (because `#await` waits a microtask to wait for a potentially immediately-resolved promise). If you need that guarantee, call `flushSync` (import from `'svelte'`) after calling `mount/hydrate`. ### Server API changes Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`: ```js +++import { render } from 'svelte/server';+++ import App from './App.svelte'; ---const { html, head } = App.render({ props: { message: 'hello' }});--- +++const { html, head } = render(App, { props: { message: 'hello' }});+++ ``` In Svelte 4, rendering a component to a string also returned the CSS of all components. In Svelte 5, this is no longer the case by default because most of the time you're using a tooling chain that takes care of it in other ways (like SvelteKit). If you need CSS to be returned from `render`, you can set the `css` compiler option to `'injected'` and it will add `