From svelte-5-migration
Migrates .svelte files from Svelte 3/4 to Svelte 5 runes by converting $: reactive blocks, export let props, createEventDispatcher, and slot patterns. Handles interop with unmigrated Svelte 4 components.
How this skill is triggered — by the user, by Claude, or both
Slash command
/svelte-5-migration:migration-svelte-5The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Invoke these at the indicated points. This skill says WHEN —
Invoke these at the indicated points. This skill says WHEN — they say HOW.
| Skill | When | Source |
|---|---|---|
svelte:svelte-code-writer | Before writing/editing ANY .svelte or .svelte.ts | plugin: svelte/svelte |
svelte:svelte-core-bestpractices | Before writing ANY Svelte 5 component logic | plugin: svelte/svelte |
svelte-5:code-style-svelte | After every .svelte file edit | this marketplace |
frontend:code-style | After every file edit | this marketplace |
frontend:editing | During every code edit | this marketplace |
svelte-5:doc-component | After creating or migrating a .svelte component | this marketplace |
svelte-5:storybook | When creating/updating stories | this marketplace |
svelte-5:storybook-vitest | When writing play functions | this marketplace |
svelte-5:testing-svelte | When writing vitest browser tests | this marketplace |
frontend:playwright | Before AND after migration (baselines + verification) | this marketplace |
frontend:pixel-perfect | For any CSS/layout changes during migration | this marketplace |
frontend:validate-file | After EVERY file edit | this marketplace |
frontend:migration | Framework-agnostic phases this skill builds on | this marketplace |
Nothing gets edited until baselines are recorded.
browser_evaluate to
measure key element positions/sizes per frontend:pixel-perfect.writable() stores (from svelte/store) are
used, check how many components consume them. If all
consumers are in scope, consider migrating the store to a
.svelte.ts runes-based state module.npx sv migrate svelte-5 (use with caution)Svelte provides an auto-migration script. It converts let →
$state, on:click → onclick, slots → render tags. But:
createEventDispatcher (too risky)slot="name" → {#snippet} which fails
svelte-check when the child is still Svelte 4$: to run() from svelte/legacy instead
of $derived/$effectRun per-file via VS Code command "Migrate Component to Svelte 5 Syntax", review every change, fix interop issues manually. Do NOT run on the entire codebase at once.
{#each} bodies into child componentscreateEventDispatcher with a FIXME comment when
Svelte 5 parents consume the component; keep unchanged when
Svelte 4 parents still depend on it$: → $derived or $effect| Svelte 4 | Svelte 5 | Notes |
|---|---|---|
$: foo = expr | let foo = $derived(expr) | Pure derivation, no side effects |
$: { sideEffect() } | $effect(() => { sideEffect() }) | Only for true side effects (DOM, fetch, logging) |
$: if (cond) { ... } | $effect(() => { if (cond) { ... } }) | Review carefully — race conditions, execution order |
$: (dep, action()) | $effect(() => { void dep; action() }) | Explicit dependency tracking |
$: ({ a, b } = $store) | let a = $derived($store.a) per field | Don't destructure Svelte 3/4 stores in effects |
NEVER use $effect to set $state. Use $derived instead.
Test the runtime behavior — $: ran synchronously before
render, $effect runs asynchronously after DOM updates.
export let → $props()// Before
export let editable = true
export let title: string
// After
let { editable = true, title }: {
editable?: boolean
title: string
} = $props()
$bindable() for bound props: In Svelte 4, every export let
prop is bindable. In runes mode, props need explicit $bindable():
// without default
let { value = $bindable() }: { value?: string } = $props()
// with default
let { count = $bindable(0) }: { count?: number } = $props()
Check ALL parents for bind: usage before removing export let.
$$props / $$restProps → destructured rest// Before
<button {...$$restProps}>click</button>
// After
let { class: className, ...rest } = $props()
<button class={className} {...rest}>click</button>
createEventDispatcher → callback props// Before
const dispatch = createEventDispatcher()
dispatch('valuesChanged')
// After — ONLY when parent is also Svelte 5
let { onValuesChanged }: { onValuesChanged?: () => void } = $props()
onValuesChanged?.()
Keep createEventDispatcher when the parent is still Svelte 4.
Svelte 4 parents use on:valuesChanged={handler} which does NOT
map to callback props on a runes-mode child. Use the legacy
import as a stopgap until the parent is migrated.
on:click → onclick// Before
<button on:click={handler}>click</button>
<button on:click|preventDefault={handler}>click</button>
// After
<button onclick={handler}>click</button>
<button onclick={(event) => { event.preventDefault(); handler(event) }}>click</button>
Event modifiers (|once, |preventDefault) become wrapper
functions or inline logic. on: syntax still works but is
deprecated.
Svelte 5 parent → Svelte 4 child (child uses <slot>):
slot="header" attribute — passes svelte-check{#snippet header()} — fails svelte-check
(sveltejs/language-tools#2716)on:click / on:event for Svelte 4 child eventsSvelte 5 parent → Svelte 5 child (child uses {@render}):
// Child
let { header, children } = $props()
{@render header?.()}
{@render children?.()}
// Parent
<Child>
{#snippet header()}Header{/snippet}
Body content
</Child>
<svelte:component> → direct rendering// Before — required for dynamic components
<svelte:component this={DynamicComp} {prop} />
// After — Svelte 5 re-renders when the variable changes
<DynamicComp {prop} />
onMount / onDestroy → $effect (context-dependent)Reactive subscription — replace with $effect:
// Before
let unsubscribe
onMount(() => { unsubscribe = store.subscribe(handler) })
onDestroy(() => unsubscribe())
// After — $store auto-subscribes in runes mode
$effect(() => { handler($store) })
Note: store.subscribe(handler) passes the store value to
handler. The $effect replacement must do the same —
handler($store), not handler().
If the handler needs cleanup, return a teardown function:
$effect(() => {
const connection = createConnection($config)
return () => { connection.close() }
})
One-time init — keep onMount:
onMount(() => { fetchInitialData() })
onMount works in Svelte 5. Use for one-time initialization.
$effect for reactive re-runs. $effect.pre for code that
must run before DOM updates (replaces beforeUpdate).
| Parent | Child | Slots | Events | Props |
|---|---|---|---|---|
| Svelte 5 | Svelte 4 | slot="name" | on:event | bind: works |
| Svelte 4 | Svelte 5 | Keep <slot> in child | Keep createEventDispatcher (legacy import) | $props() safe |
| Svelte 5 | Svelte 5 | {#snippet} + {@render} | callback props | $props() |
Stories must exist and render correctly before any test can verify behavior:
asChild pattern — NOT decorators for components that
depend on Svelte 3/4 writable() stores (from svelte/store)store.set() with
mock data + setContext() for required Svelte contextsfn() from storybook/test as callback spy for critical
callbacks — NOT noop. Pass the spy in the asChild markup
AND assert it in the play function.Each step gates the next:
fn() spy play functions.browser_evaluate, compare against BEFORE
baselines from Phase 1. Use frontend:pixel-perfect diff tables.eslint-disable or @ts-ignore/@ts-expect-error
without FIXMEbind:value
for callback-only, extract the value in the parent.{#snippet} on Svelte 4 children — fails svelte-check.
Use slot="name" instead.
(sveltejs/language-tools#2716)Writable<Record<string, Writable<...>>> —
conflicts with svelte/require-store-reactive-access. Use
in operator for existence checks.writable() stores, objects with methods get serialized
away. Use asChild with wrapper components instead.noop callbacks hide bugs — () => {} silently swallows
wrong data types. Use fn() spy and assert in play functions.Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub fubits1/svelte-skills --plugin svelte-5-migration