Svelte 5 introduces significant improvements to reactivity, component architecture, and developer experience. This guide will help you migrate your Svelte 4 application to Svelte 5.
Overview of Major Changes
Svelte 5 brings several fundamental changes:
- Runes: New reactive primitives replace legacy reactive syntax
- Components as functions: Components are now functions, not classes
- Snippets: Replace slots for content composition
- Event handlers: Use callback props instead of
createEventDispatcher
- Props: New
$props() rune replaces export let
Breaking Changes
The following Svelte 4 patterns are deprecated or removed in Svelte 5:
$: reactive statements (use $derived and $effect)
export let for props (use $props())
createEventDispatcher (use callback props)
<slot> elements (use snippets)
on: event directive (use event attributes)
- Components as class instances (now functions)
Migration Strategy
Incremental Migration
Svelte 5 supports both legacy mode and runes mode, allowing you to migrate incrementally:
- Update Svelte: Install Svelte 5
- Component by component: Migrate one component at a time
- Test thoroughly: Ensure functionality remains intact
- Update dependencies: Check third-party packages for Svelte 5 compatibility
Automatic Migration
Svelte provides migration tools to help automate the process. Run the migration script on your codebase to get started.
Reactivity Changes
From $: to Runes
Svelte 4’s reactive statements ($:) are replaced by $derived and $effect runes.
<script>
let count = 0;
let doubled;
// Reactive assignment
$: doubled = count * 2;
// Reactive statement
$: console.log(`count is ${count}`);
// Reactive block
$: {
if (count > 10) {
console.log('Count is high!');
}
}
</script>
<button on:click={() => count++}>
{count} / {doubled}
</button>
<script>
let count = $state(0);
// Derived state
let doubled = $derived(count * 2);
// Effect for side effects
$effect(() => {
console.log(`count is ${count}`);
if (count > 10) {
console.log('Count is high!');
}
});
</script>
<button onclick={() => count++}>
{count} / {doubled}
</button>
State Management
<script>
let items = [];
let total = 0;
$: total = items.reduce((sum, item) => sum + item.price, 0);
function addItem(item) {
items = [...items, item];
}
</script>
<script>
let items = $state([]);
let total = $derived(items.reduce((sum, item) => sum + item.price, 0));
function addItem(item) {
items.push(item); // Direct mutation works with $state
}
</script>
Props Changes
From export let to $props()
export let is deprecated in runes mode. Use the $props() rune instead.
<!-- Child.svelte -->
<script>
export let name;
export let age = 0;
export let email = undefined;
</script>
<div>
<p>{name} ({age})</p>
{#if email}
<p>{email}</p>
{/if}
</div>
<!-- Child.svelte -->
<script>
let { name, age = 0, email } = $props();
</script>
<div>
<p>{name} ({age})</p>
{#if email}
<p>{email}</p>
{/if}
</div>
Bindable Props
<!-- Counter.svelte -->
<script>
export let count = 0;
</script>
<button on:click={() => count++}>
{count}
</button>
<!-- Parent.svelte -->
<script>
let value = 0;
</script>
<Counter bind:count={value} />
<!-- Counter.svelte -->
<script>
let { count = $bindable(0) } = $props();
</script>
<button onclick={() => count++}>
{count}
</button>
<!-- Parent.svelte -->
<script>
let value = $state(0);
</script>
<Counter bind:count={value} />
Event Handling Changes
From createEventDispatcher to Callbacks
createEventDispatcher is deprecated in Svelte 5. Use callback props instead.
<!-- Button.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('click', { timestamp: Date.now() });
}
</script>
<button on:click={handleClick}>
Click me
</button>
<!-- Parent.svelte -->
<script>
function handleButtonClick(event) {
console.log('Clicked at:', event.detail.timestamp);
}
</script>
<Button on:click={handleButtonClick} />
<!-- Button.svelte -->
<script>
let { onclick } = $props();
function handleClick() {
onclick?.({ timestamp: Date.now() });
}
</script>
<button onclick={handleClick}>
Click me
</button>
<!-- Parent.svelte -->
<script>
function handleButtonClick(data) {
console.log('Clicked at:', data.timestamp);
}
</script>
<Button onclick={handleButtonClick} />
Event Directives
<script>
let count = 0;
</script>
<button on:click={() => count++}>
{count}
</button>
<form on:submit|preventDefault={handleSubmit}>
<input type="text" />
</form>
<script>
let count = $state(0);
function handleSubmit(e) {
e.preventDefault();
// handle submit
}
</script>
<button onclick={() => count++}>
{count}
</button>
<form onsubmit={handleSubmit}>
<input type="text" />
</form>
Component API Changes
Components as Functions
In Svelte 5, components are functions, not class instances. This means:
- No
new Component() syntax
- No
component.$set() or component.$on() methods
- Use
mount() and unmount() for programmatic rendering
import Component from './Component.svelte';
const component = new Component({
target: document.body,
props: { name: 'world' }
});
component.$set({ name: 'Svelte' });
component.$on('event', handler);
component.$destroy();
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
const component = mount(Component, {
target: document.body,
props: { name: 'world' }
});
// Props are reactive - just update them
component.name = 'Svelte';
// Use callback props instead of events
unmount(component);
Slots to Snippets
Basic Content Projection
<!-- Card.svelte -->
<div class="card">
<slot />
</div>
<!-- App.svelte -->
<Card>
<h1>Hello World</h1>
</Card>
<!-- Card.svelte -->
<script>
let { children } = $props();
</script>
<div class="card">
{@render children()}
</div>
<!-- App.svelte -->
<Card>
<h1>Hello World</h1>
</Card>
Named Slots
<!-- Modal.svelte -->
<div class="modal">
<div class="header">
<slot name="header" />
</div>
<div class="body">
<slot />
</div>
<div class="footer">
<slot name="footer" />
</div>
</div>
<!-- App.svelte -->
<Modal>
<h2 slot="header">Title</h2>
<p>Content</p>
<button slot="footer">Close</button>
</Modal>
<!-- Modal.svelte -->
<script>
let { header, children, footer } = $props();
</script>
<div class="modal">
<div class="header">
{@render header()}
</div>
<div class="body">
{@render children()}
</div>
<div class="footer">
{@render footer()}
</div>
</div>
<!-- App.svelte -->
<Modal>
{#snippet header()}
<h2>Title</h2>
{/snippet}
<p>Content</p>
{#snippet footer()}
<button>Close</button>
{/snippet}
</Modal>
Scoped Slots (Render Props)
<!-- List.svelte -->
<script>
export let items;
</script>
<ul>
{#each items as item}
<li>
<slot item={item} />
</li>
{/each}
</ul>
<!-- App.svelte -->
<List items={users} let:item>
<strong>{item.name}</strong>
</List>
<!-- List.svelte -->
<script>
let { items, children } = $props();
</script>
<ul>
{#each items as item}
<li>
{@render children(item)}
</li>
{/each}
</ul>
<!-- App.svelte -->
<List items={users}>
{#snippet children(item)}
<strong>{item.name}</strong>
{/snippet}
</List>
Lifecycle Changes
<script>
import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';
onMount(() => {
console.log('Component mounted');
return () => {
console.log('Cleanup on unmount');
};
});
onDestroy(() => {
console.log('Component destroying');
});
beforeUpdate(() => {
console.log('Before update');
});
afterUpdate(() => {
console.log('After update');
});
</script>
<script>
import { onMount } from 'svelte';
onMount(() => {
console.log('Component mounted');
return () => {
console.log('Cleanup on unmount');
};
});
// Use $effect for update tracking
$effect(() => {
console.log('Effect runs on mount and when dependencies change');
return () => {
console.log('Effect cleanup');
};
});
</script>
Store Changes
Stores continue to work in Svelte 5, but you may want to migrate to runes for better performance:
Tab Title
Tab Title
Tab Title
<script>
import { writable } from 'svelte/store';
const count = writable(0);
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>
{$count}
</button>
<script>
import { writable } from 'svelte/store';
const count = writable(0);
function increment() {
count.update(n => n + 1);
}
</script>
<button onclick={increment}>
{$count}
</button>
<script>
let count = $state(0);
function increment() {
count++;
}
</script>
<button onclick={increment}>
{count}
</button>
TypeScript Changes
Update your TypeScript types for Svelte 5:
<script lang="ts">
export let name: string;
export let age: number = 0;
interface User {
id: number;
name: string;
}
let users: User[] = [];
</script>
<script lang="ts">
interface Props {
name: string;
age?: number;
}
let { name, age = 0 }: Props = $props();
interface User {
id: number;
name: string;
}
let users = $state<User[]>([]);
</script>
Common Migration Patterns
Two-Way Binding
<script>
let text = '';
</script>
<input bind:value={text} />
<p>{text}</p>
<script>
let text = $state('');
</script>
<input bind:value={text} />
<p>{text}</p>
Component Composition
<!-- Wrapper.svelte -->
<script>
export let title;
</script>
<div class="wrapper">
<h1>{title}</h1>
<slot />
</div>
<!-- App.svelte -->
<Wrapper title="My App">
<p>Content goes here</p>
</Wrapper>
<!-- Wrapper.svelte -->
<script>
let { title, children } = $props();
</script>
<div class="wrapper">
<h1>{title}</h1>
{@render children()}
</div>
<!-- App.svelte -->
<Wrapper title="My App">
<p>Content goes here</p>
</Wrapper>
Migration Checklist
Next Steps