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.
npx claudepluginhub fubits1/svelte-skills --plugin svelte-5-migrationThis skill uses the workspace's default tool permissions.
Invoke these at the indicated points. This skill says WHEN —
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
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.