Svelte 5 Migration Guide
Contents
Svelte 5 Migration Guide
Overview
Complete guide for migrating Svelte 4 applications to Svelte 5. Covers breaking changes, codemods, and incremental adoption strategies.
Breaking Changes Summary
| Area | Svelte 4 | Svelte 5 |
|---|---|---|
| Reactivity | Compiler-driven ($:) | Runes ($state, $derived, $effect) |
| Props | export let | $props() |
| Slots | <slot> | Snippets ({#snippet}, {@render}) |
| Events | on:click | onclick |
| Context | setContext/getContext | Still works, but $state modules preferred |
| Dispatch | createEventDispatcher | Callback props |
Automated Migration
1. Run the Official Codemod
npx @sveltejs/migrate@latest svelte-5
This handles:
export letโ$props()on:eventโonevent$:โ$derived/$effect<slot>โ snippets (partial)
2. Manual Fixes Required After Codemod
The codemod is ~90% accurate. You must review:
- Reactive statements (
$:) โ codemod guesses$derivedvs$effect - Slot usages โ complex slot patterns need manual snippet conversion
- Event dispatching โ
dispatch('event', data)โ callback props - Component typing โ update TypeScript interfaces
Step-by-Step Migration
Step 1: Update Dependencies
npm install -D svelte@latest @sveltejs/vite-plugin-svelte@latest
npm install svelte@latest
Update package.json:
{
"devDependencies": {
"svelte": "^5.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0"
}
}
Step 2: Run Codemod
npx @sveltejs/migrate@latest svelte-5
Step 3: Fix Reactive Statements
Before (Svelte 4):
<script>
let count = 0;
$: doubled = count * 2;
$: if (count > 10) console.log('big');
</script>
After (Svelte 5):
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
if (count > 10) console.log('big');
});
</script>
Decision tree for $: x = ...:
- Pure computation โ
$derived - Side effect (DOM, fetch, console) โ
$effect - Complex logic needing function โ
$derived.by(() => ...)
Step 4: Convert Props
Before:
<script>
export let title = 'Default';
export let count: number;
export let onClick: () => void;
</script>
After:
<script lang="ts">
let {
title = 'Default',
count = 0,
onClick
} = $props<{ title?: string; count?: number; onClick?: () => void }>();
</script>
Step 5: Convert Slots to Snippets
Before (Parent):
<Card>
<div slot="header">Title</div>
<p slot="body">Content</p>
</Card>
Before (Card.svelte):
<div class="card">
<header><slot name="header" /></header>
<main><slot name="body" /></main>
</div>
After (Parent):
<script>
import Card from './Card.svelte';
</script>
<Card>
{#snippet header()}
<div>Title</div>
{/snippet}
{#snippet body()}
<p>Content</p>
{/snippet}
</Card>
After (Card.svelte):
<script>
let { header, body } = $props<{
header: Snippet;
body: Snippet
}>();
</script>
<div class="card">
<header>{@render header()}</header>
<main>{@render body()}</main>
</div>
Step 6: Convert Event Handlers
Before:
<button on:click={handleClick}>Click</button>
<CustomComponent on:customEvent={handleCustom} />
After:
<button onclick={handleClick}>Click</button>
<CustomComponent oncustomEvent={handleCustom} />
Step 7: Replace Event Dispatching
Before (Child):
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function select(item) {
dispatch('select', item);
}
</script>
After (Child):
<script>
let { onSelect }: { onSelect?: (item: Item) => void } = $props();
function select(item) {
onSelect?.(item);
}
</script>
Parent usage:
<Child onSelect={handleSelect} />
Step 8: Update Component TypeScript
<!-- Svelte 5 component with typed props -->
<script lang="ts">
interface Props {
title: string;
count?: number;
onAction?: (id: string) => void;
renderItem?: Snippet<[Item]>;
}
let {
title,
count = 0,
onAction,
renderItem
} = $props<Props>();
</script>
Incremental Migration Strategy
Option A: Per-Component (Recommended)
- Run codemod on entire codebase
- Fix one component at a time
- Test each component before moving on
- Svelte 4 and 5 components can coexist during migration
Option B: Per-Feature Branch
- Create feature branch
- Migrate entire feature
- Test thoroughly
- Merge
Compatibility Notes
- Svelte 4 components work inside Svelte 5 apps
- Svelte 5 components work inside Svelte 4 apps (with caveats)
- Shared state via
$statein.svelte.jsworks across versions - Don’t mix runes and legacy reactivity in same component
Common Pitfalls
1. Forgetting $state for Mutable Values
<!-- WRONG: count won't be reactive -->
<script>
let count = 0;
function inc() { count++; }
</script>
<!-- CORRECT -->
<script>
let count = $state(0);
function inc() { count++; }
</script>
2. Using $derived for Side Effects
<!-- WRONG: derivation with side effect -->
let x = $derived.by(() => { fetch(...); return y; });
<!-- CORRECT: use $effect -->
$effect(() => { fetch(...); });
3. Mutating Props Directly
<!-- WRONG: props are read-only from parent perspective -->
<script>
let { items } = $props();
items.push(newItem); // Mutates parent's array!
</script>
<!-- CORRECT: use callback or return new array -->
<script>
let { items, onAdd } = $props();
function add() { onAdd?.(newItem); }
</script>
4. Slot Migration Edge Cases
<!-- Default slot + named slots -->
<Layout>
<header slot="header">...</header>
<main>Default content</main>
</Layout>
<!-- Becomes: -->
<Layout>
{#snippet header()}...{/snippet}
{#snippet default()}...{/snippet}
</Layout>
<!-- Layout.svelte: -->
<script>
let { header, default: children } = $props();
</script>
<header>{@render header?.()}</header>
<main>{@render children?.()}</main>
Testing
# Run tests after each component migration
npm test
# Check for TypeScript errors
npx svelte-check --tsconfig tsconfig.json
# Build verification
npm run build
Performance Validation
After migration, verify:
- Bundle size (should be similar or smaller)
- Hydration time (improved with fine-grained reactivity)
- Runtime performance (no more
$:statement overhead)
Resources
Related Topics
Evolution Notes
Content last updated: 2026-06-05 Next review: 2026-06-12