
Svelte Runes
- 528 installs
- 92 repo stars
- Updated April 29, 2026
- spences10/svelte-skills-kit
svelte-runes is an agent skill that authors and migrates Svelte 5 runes ($state, $derived, $effect) for developers replacing legacy stores with modern reactive UI logic.
About
svelte-runes is a frontend agent skill in spences10/svelte-skills-kit focused on Svelte 5 runes-based reactivity for new components and migrations away from legacy store patterns. It helps agents apply $state for mutable component state, $derived for computed values, and $effect for side effects without the subtle pitfalls of pre-runes stores and manual subscriptions. Developers reach for svelte-runes when upgrading SvelteKit apps, building component libraries on Svelte 5, or when agents generate reactive UI logic that must compile against runes semantics. The skill fits feature work where derived data, local UI state, and effect cleanup must stay idiomatic after a Svelte 4 store refactor. Use it during component authoring, runes migration pull requests, and library APIs that expose reactive props consistently. Agents should prefer runes-native patterns over writable stores unless interoperability requires legacy APIs. The skill aligns with Svelte 5 compiler expectations so agents do not reintroduce `$:` labels or store boilerplate that fight the new reactivity model during incremental upgrades.
- $state $derived $effect
- Store migration paths
- Fine-grained reactivity
- Effect cleanup rules
- Component API clarity
Svelte Runes by the numbers
- 528 all-time installs (skills.sh)
- +15 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #612 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/svelte-skills-kit --skill svelte-runesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 528 |
|---|---|
| repo stars | ★ 92 |
| Last updated | April 29, 2026 |
| Repository | spences10/svelte-skills-kit ↗ |
How do you migrate Svelte stores to runes?
Migrate or author Svelte 5 runes—$state, $derived, $effect—for reactive UI logic without legacy store pitfalls in new features and component libraries.
Who is it for?
Frontend developers on Svelte 5 or SvelteKit who need agents to write runes-native reactivity instead of legacy store boilerplate.
Skip if: Teams still standardized on Svelte 4-only stores with no Svelte 5 upgrade planned should skip svelte-runes.
When should I use this skill?
User edits Svelte 5 components, mentions $state/$derived/$effect, or asks to migrate stores to runes.
What you get
Svelte 5 runes-based components, derived reactive values, and effect-safe side-effect blocks.
- Runes-native Svelte components
- Store-to-runes migration patches
- Derived and effect-safe reactive logic
By the numbers
- Centers on 3 core Svelte 5 runes: $state, $derived, and $effect
Files
Svelte Runes
Quick Start
Which rune? Props: $props() | Bindable: $bindable() | Computed: $derived() | Side effect: $effect() | State: $state()
Key rules: Runes are top-level only. $derived can be overridden (use const for read-only). Don't mix Svelte 4/5 syntax. Objects/arrays are deeply reactive by default.
Example
<script>
let count = $state(0); // Mutable state
const doubled = $derived(count * 2); // Computed (const = read-only)
$effect(() => {
console.log(`Count is ${count}`); // Side effect
});
</script>
<button onclick={() => count++}>
{count} (doubled: {doubled})
</button>Reference Files
- reactivity-patterns.md - When
to use each rune
- migration-gotchas.md - Svelte 4→5
translation
- component-api.md - $props, $bindable
patterns
- snippets-vs-slots.md - New
snippet syntax
- common-mistakes.md - Anti-patterns
with fixes
For @attach and other template directives, see thesvelte-template-directives skill.
Notes
- Use
onclicknoton:click,{@render children()}in layouts $derivedcan be reassigned (5.25+) - useconstfor read-only- Use
createContextoversetContext/getContextfor type safety - Use
$inspect.traceto debug reactivity issues - Last verified: 2026-03-12
<!-- PROGRESSIVE DISCLOSURE GUIDELINES:
- Keep this file ~50 lines total (max ~150 lines)
- Use 1-2 code blocks only (recommend 1)
- Keep description <200 chars for Level 1 efficiency
- Move detailed docs to references/ for Level 3 loading
- This is Level 2 - quick reference ONLY, not a manual
LLM WORKFLOW (when editing this file): 1. Write/edit SKILL.md 2. Format (if formatter available) 3. Run: npx skills add . --list 4. If the skill is not discovered, check SKILL.md frontmatter formatting 5. Validate again to confirm -->
<!--
EXAMPLE: $bindable Props - Two-Way Binding
This shows proper examples of creating bindable components.
The commented sections show how to create actual component files.
-->
<script lang="ts">
// Demo state
let name = $state('');
let email = $state('');
let isEnabled = $state(false);
let count = $state(0);
let formValid = $derived(name.length > 0 && email.includes('@'));
</script>
<div class="demo">
<h2>$bindable Props Demo</h2>
<div class="form">
<h3>Interactive Form</h3>
<div class="field">
<label>
Name
<input type="text" bind:value={name} />
</label>
</div>
<div class="field">
<label>
Email
<input type="email" bind:value={email} />
</label>
</div>
<div class="field">
<label>
<input type="checkbox" bind:checked={isEnabled} />
Enable feature
</label>
</div>
<div class="field">
<button onclick={() => count++}>Count: {count}</button>
</div>
<p class:valid={formValid} class:invalid={!formValid}>
Form is {formValid ? 'valid' : 'invalid'}
</p>
</div>
<div class="state-display">
<h3>Current State</h3>
<pre>{JSON.stringify(
{ name, email, isEnabled, count, formValid },
null,
2,
)}</pre>
</div>
<div class="guidelines">
<h3>Creating Bindable Components</h3>
<h4>1. TextInput Component (with $bindable)</h4>
<pre><code
><!-- TextInput.svelte -->
<script lang="ts">
interface Props {
value?: string;
label: string;
}
let { value = $bindable(''), label }: Props = $props();
</script>
<label>
{label}
<input type="text" bind:value />
</label>
<!-- Usage -->
<TextInput bind:value={name} label="Name" /></code
></pre>
<h4>2. Counter Component (callback pattern - no $bindable)</h4>
<pre><code
><!-- Counter.svelte -->
<script lang="ts">
interface Props {
count: number;
onIncrement: () => void;
}
let { count, onIncrement }: Props = $props();
</script>
<button onclick={onIncrement}>Count: {count}</button>
<!-- Usage -->
<Counter {count} onIncrement={() => count++} /></code
></pre>
<h4>When to Use $bindable</h4>
<ul>
<li>✅ Form input wrappers</li>
<li>✅ Parent needs to read AND write</li>
<li>✅ Two-way data flow is natural</li>
<li>❌ Parent only reads (use callback)</li>
<li>❌ Need validation (use callback)</li>
</ul>
<h4>Decision Tree</h4>
<pre>Parent needs to read child state?
├─ No → Pass callbacks
└─ Yes → Parent needs to UPDATE child?
├─ No → Callback (onChange)
└─ Yes → Use $bindable()</pre>
</div>
</div>
<style>
.demo {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.form {
margin: 2rem 0;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
.field {
margin: 1rem 0;
}
label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
}
input[type='text'],
input[type='email'] {
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
flex: 1;
}
input[type='checkbox'] {
width: 1.25rem;
height: 1.25rem;
}
button {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
background: #007bff;
color: white;
border: none;
border-radius: 0.25rem;
}
button:hover {
background: #0056b3;
}
.valid {
color: green;
font-weight: bold;
}
.invalid {
color: red;
font-weight: bold;
}
.state-display {
margin: 2rem 0;
padding: 1rem;
background: #f5f5f5;
border-radius: 0.5rem;
}
.state-display pre {
margin: 0;
overflow-x: auto;
}
.guidelines {
margin: 2rem 0;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
h3 {
margin-top: 0;
}
h4 {
margin: 1rem 0 0.5rem 0;
}
pre {
background: #f5f5f5;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
}
code {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
ul {
margin: 0.5rem 0;
}
</style>
<!--
EXAMPLE: When to use $derived vs $effect
This demonstrates the key difference:
- $derived: For computed values (data transformation)
- $effect: For side effects (logging, DOM, external APIs)
-->
<script lang="ts">
let count = $state(0);
// ✅ CORRECT: Use $derived for computed values
let doubled = $derived(count * 2);
let tripled = $derived(count * 3);
let message = $derived(
count === 0 ? 'Zero' : count > 10 ? 'High' : 'Low',
);
// ✅ CORRECT: Use $effect for side effects
$effect(() => {
// Side effect: Logging
console.log(`Count changed to ${count}`);
// Side effect: Update document title
document.title = `Count: ${count}`;
// Side effect: Save to localStorage
localStorage.setItem('count', String(count));
});
// ✅ CORRECT: $effect with cleanup
$effect(() => {
const interval = setInterval(() => {
console.log('Current count:', count);
}, 1000);
// Cleanup function (runs when effect re-runs or component unmounts)
return () => clearInterval(interval);
});
// ❌ WRONG: Don't use $effect for derived state
// let wrongDoubled = $state(0);
// $effect(() => {
// wrongDoubled = count * 2; // BAD - use $derived!
// });
// ❌ WRONG: Don't update dependencies in $effect
// $effect(() => {
// count++; // INFINITE LOOP!
// });
function increment() {
count++;
}
function decrement() {
count--;
}
function reset() {
count = 0;
}
</script>
<div>
<h2>$derived vs $effect Demo</h2>
<div class="counter">
<button onclick={decrement}>-</button>
<span>{count}</span>
<button onclick={increment}>+</button>
<button onclick={reset}>Reset</button>
</div>
<div class="computed">
<h3>Computed Values ($derived)</h3>
<p>Doubled: {doubled}</p>
<p>Tripled: {tripled}</p>
<p>Message: {message}</p>
</div>
<div class="effects">
<h3>Side Effects ($effect)</h3>
<ul>
<li>Check console for log output</li>
<li>Check document title</li>
<li>Check localStorage (key: 'count')</li>
</ul>
</div>
<div class="guidelines">
<h3>When to Use Each</h3>
<h4>Use $derived when:</h4>
<ul>
<li>Transforming data (multiply, format, filter)</li>
<li>Computing values based on other state</li>
<li>Value is used in template</li>
<li>Need read-only computed property</li>
</ul>
<h4>Use $effect when:</h4>
<ul>
<li>Logging or analytics</li>
<li>Updating external state (localStorage, DOM)</li>
<li>Fetching data</li>
<li>Setting up subscriptions (intervals, listeners)</li>
<li>Any operation with side effects</li>
</ul>
</div>
</div>
<style>
.counter {
display: flex;
gap: 1rem;
align-items: center;
margin: 1rem 0;
}
button {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
}
.counter span {
font-size: 2rem;
font-weight: bold;
min-width: 3rem;
text-align: center;
}
.computed,
.effects,
.guidelines {
margin: 2rem 0;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
h3 {
margin-top: 0;
}
h4 {
margin-bottom: 0.5rem;
}
ul {
margin-top: 0.5rem;
}
</style>
Svelte5 Runes
Expert guidance on Svelte 5 runes ($state, $derived, $effect, $props, $bindable). Use when working with Svelte 5 reactive state, component props, side effects, or migrating from Svelte 4. Prevents common mistakes like mixing reactive statements with runes, misusing $effect for derived state, and shallow reactivity issues.
Structure
SKILL.md- Main skill instructionsreferences/- Detailed documentation loaded as neededscripts/- Executable code for deterministic operationsassets/- Templates, images, or other resources
Usage
This skill is automatically discovered by compatible agents when relevant to the task.
Common Mistakes: Anti-Patterns and Fixes
Top 10 Svelte 5 Mistakes
1. Using $effect for Derived State ❌
WRONG:
<script>
let count = $state(0);
let doubled = $state(0);
$effect(() => {
doubled = count * 2; // BAD - use $derived!
});
</script>RIGHT:
<script>
let count = $state(0);
let doubled = $derived(count * 2); // GOOD - computed value
</script>Why: $effect runs after DOM updates and is for side effects. $derived is optimized for computed values.
---
1b. Using $effect When Event Handler Works ❌
WRONG:
<script>
let count = $state(0);
let lastClicked = $state(null);
$effect(() => {
// BAD - reacting to count change just to log
console.log(`Count is now ${count}`);
});
</script>
<button onclick={() => count++}>Increment</button>RIGHT:
<script>
let count = $state(0);
function increment() {
count++;
console.log(`Count is now ${count}`); // Side effect in handler
}
</script>
<button onclick={increment}>Increment</button>Why: Per Svelte docs: "If you can put your side effects in an event handler, that's almost always preferable." Event handlers are predictable and run once per action.
---
1c. Using $effect to Sync Linked Values ❌
WRONG:
<script>
let celsius = $state(0);
let fahrenheit = $state(32);
// Two effects trying to sync each other - fragile!
$effect(() => {
fahrenheit = (celsius * 9) / 5 + 32;
});
$effect(() => {
celsius = ((fahrenheit - 32) * 5) / 9;
});
</script>
<input type="number" bind:value={celsius} />
<input type="number" bind:value={fahrenheit} />RIGHT - Use oninput callbacks:
<script>
let celsius = $state(0);
let fahrenheit = $state(32);
function updateFromCelsius(e) {
celsius = +e.target.value;
fahrenheit = (celsius * 9) / 5 + 32;
}
function updateFromFahrenheit(e) {
fahrenheit = +e.target.value;
celsius = ((fahrenheit - 32) * 5) / 9;
}
</script>
<input type="number" value={celsius} oninput={updateFromCelsius} />
<input type="number" value={fahrenheit} oninput={updateFromFahrenheit} />Why: Per Svelte docs, avoid effects for "connecting one value to another". Use oninput callbacks or function bindings instead.
---
1d. Using $effect to Sync Async Data into Form State ❌
WRONG:
<script>
let query = $derived(get_item({ id }))
let name = $state('')
// BAD — $effect as escape hatch to sync query → form state
$effect(() => {
if (query.ready) name = query.current.name
})
</script>
<input bind:value={name} />RIGHT — Gate child component behind `.ready`:
<!-- Parent.svelte -->
<script>
let query = $derived(get_item({ id }))
</script>
{#if !query.ready}
<Skeleton />
{:else}
<EditForm item={query.current} />
{/if}<!-- EditForm.svelte -->
<script>
let { item } = $props()
// svelte-ignore state_referenced_locally
let form = $state({ name: item.name }) // init from prop at mount
</script>
<input bind:value={form.name} />Why: The child component initializes $state from props once at mount time. No $effect needed, no state_unsafe_mutation warning. This is the standard pattern for editable forms backed by async data.
---
2. Reassigning $derived Values ⚠️
Note: As of Svelte 5.25+, $derived CAN be reassigned, but will recalculate when dependencies change.
CONFUSING (works but not recommended):
<script>
let count = $state(0);
let doubled = $derived(count * 2);
function reset() {
doubled = 0; // Temporarily overrides, but recalculates when count changes
}
</script>CLEARER - Use const for read-only:
<script>
let count = $state(0);
const doubled = $derived(count * 2); // const = truly read-only
function reset() {
count = 0; // Update source, derived updates automatically
}
</script>Why: While reassignment is allowed, it's clearer to update the source state. Use const to enforce read-only behavior.
---
3. Optional Chaining Breaks Effect Reactivity ❌
WRONG:
<script>
let particles = $state(undefined);
let scheme = $state('dark');
$effect(() => {
// If particles is undefined, scheme is NEVER read!
// Effect won't re-run when scheme changes
particles?.updateScheme(scheme);
});
</script>RIGHT:
<script>
let particles = $state(undefined);
let scheme = $state('dark');
$effect(() => {
// Read scheme first to create dependency
const currentScheme = scheme;
if (particles) {
particles.updateScheme(currentScheme);
}
});
</script>Why: JavaScript short-circuits optional chaining. If particles is nullish, scheme is never evaluated, so no dependency is created.
---
4. Creating Infinite Loops in $effect ❌
WRONG:
<script>
let count = $state(0);
$effect(() => {
count++; // INFINITE LOOP - effect triggers itself!
});
</script>RIGHT - Don't update dependencies
<script>
let count = $state(0);
let log = $state([]);
$effect(() => {
log.push(count); // Updates different state
});
</script>RIGHT - Use untrack() to read without subscribing
<script>
import { untrack } from 'svelte';
let count = $state(0);
$effect(() => {
console.log('Effect ran');
// Read count without creating dependency
const current = untrack(() => count);
// Now updating count won't re-trigger this effect
});
</script>Why: $effect runs when any accessed $state changes. Updating that state creates a loop. Use untrack() to read state without creating a dependency.
---
4b. Using $effect to Sync State with DOM Elements ❌
WRONG - Dialog sync via effect:
<script>
let is_open = $state(false);
let dialog_element = $state<HTMLDialogElement>();
$effect(() => {
if (is_open) {
dialog_element?.showModal();
} else {
dialog_element?.close(); // Fires 'close' event → handler → loop!
}
});
</script>
<dialog bind:this={dialog_element} onclose={() => is_open = false}>Why it fails: dialog.close() fires the native close event, which triggers your handler, which may cause loops or double-firing.
RIGHT - State class with @attach:
// state.svelte.ts
class DialogState {
dialog: HTMLDialogElement | null = null;
is_open = $state(false);
register = (el: HTMLDialogElement) => {
this.dialog = el;
return () => {
this.dialog = null;
};
};
open() {
if (!this.dialog?.open) {
this.is_open = true;
this.dialog?.showModal();
}
}
close() {
this.is_open = false;
this.dialog?.close();
}
}<!-- Component.svelte -->
<dialog {@attach dialog_state.register} onclose={dialog_state.close}>Why: Per Svelte docs, "$effect is best thought of as an escape hatch" and "you should not update state inside effects". Use @attach to register elements with state, then call DOM methods directly.
---
4c. Using Runes Inside Functions ❌
WRONG:
<script>
function createCounter() {
let count = $state(0); // ERROR - runes must be top-level!
return count;
}
const counter = createCounter();
</script>RIGHT - Option 1: Top-level runes
<script>
let count = $state(0);
</script>RIGHT - Option 2: Reactive class fields
<script>
class Counter {
count = $state(0); // OK in class fields
}
const counter = new Counter();
</script>Why: Runes must be statically analyzable at compile time. Use classes for encapsulation.
---
5. Understanding Deep Reactivity in Svelte 5 ✅
GOOD NEWS: Deep reactivity works by default!
<script>
let user = $state({ profile: { name: 'Alex' } });
function updateName() {
user.profile.name = 'Bo'; // This DOES trigger reactivity!
}
</script>
<p>{user.profile.name}</p> <!-- Will update correctly -->Why: $state() creates deep reactive proxies by default. Nested mutations trigger updates.
When to use $state.raw() instead:
<script>
// For large, immutable data structures where you don't need reactivity
let config = $state.raw(hugeConfigObject); // Skip deep proxy overhead for performance
// For data you'll fully replace, not mutate
let apiResponse = $state.raw(data); // Will replace entire object later
</script>Why: Use $state.raw() for performance optimization when you don't need deep reactivity, not because deep reactivity doesn't work.
---
6. Mixing Svelte 4 and 5 Syntax ❌
WRONG:
<script>
let count = $state(0);
$: doubled = count * 2; // DON'T MIX reactive statements with runes!
</script>
<button on:click={() => count++}>
<!-- DON'T MIX on: with runes -->
{count}
</button>RIGHT:
<script>
let count = $state(0);
let doubled = $derived(count * 2); // Use runes consistently
</script>
<button onclick={() => count++}>
<!-- Use onclick -->
{count}
</button>Why: Svelte 5 requires consistent syntax. Pick one version.
---
7. Forgetting $state for Reactive Variables ❌
WRONG:
<script>
let count = 0; // Not reactive in Svelte 5!
</script>
<button onclick={() => count++}>{count}</button>
<!-- UI won't update! -->RIGHT:
<script>
let count = $state(0); // Reactive
</script>
<button onclick={() => count++}>{count}</button>Why: Plain variables aren't reactive in Svelte 5. Must use $state.
---
8. Not Using $bindable for Two-Way Binding ❌
WRONG:
<!-- Child.svelte -->
<script>
let { value } = $props(); // Not bindable!
</script>
<input bind:value />
<!-- Parent.svelte -->
<Child bind:value={text} />
<!-- ERROR - value is not bindable -->RIGHT:
<!-- Child.svelte -->
<script>
let { value = $bindable() } = $props(); // Make it bindable
</script>
<input bind:value />
<!-- Parent.svelte -->
<Child bind:value={text} />
<!-- Works! -->Why: Props must explicitly declare they're bindable with $bindable().
---
9. Forgetting {@render} for Children ❌
WRONG:
<script>
let { children } = $props();
</script>
<div>{children}</div> <!-- Won't render! Shows [object Object] -->RIGHT:
<script>
let { children } = $props();
</script>
<div>{@render children()}</div> <!-- Renders children -->Why: Children is a snippet, not a value. Must use {@render}.
---
10. Using on: Event Handlers ❌
WRONG:
<button on:click={handler}>Click</button>
<!-- Svelte 4 syntax -->
<button on:click|preventDefault={handler}>Click</button>RIGHT:
<button onclick={handler}>Click</button>
<!-- Svelte 5 syntax -->
<button
onclick={(e) => {
e.preventDefault();
handler(e);
}}>Click</button
>Why: Svelte 5 uses standard DOM properties instead of on: directives.
---
Array and Object Mutations Work!
Svelte 5 has deep reactivity - array and object mutations trigger updates:
Arrays - All Methods Work
<script>
let items = $state([1, 2, 3]);
function addItem() {
items.push(4); // ✅ Works! Triggers reactivity
// OR
items[items.length] = 5; // ✅ Also works!
// OR
items = [...items, 6]; // ✅ Also works!
}
</script>Nested Arrays - Also Work!
<script>
let data = $state({ items: [1, 2, 3], nested: { arr: [10, 20] } });
function addItem() {
data.items.push(4); // ✅ Works! Deep reactivity
}
function addNested() {
data.nested.arr.push(30); // ✅ Works! Deeply reactive
}
</script>All mutations trigger UI updates because $state() creates deep proxies.
Performance Mistakes
1. Using $state When You Don't Need Reactivity
UNNECESSARY:
<script>
const API_URL = $state('https://api.example.com'); // Doesn't change!
</script>BETTER:
<script>
const API_URL = 'https://api.example.com'; // Plain const
</script>2. Using $state.raw for Performance
When you have large immutable data:
<script>
// Deep proxy has overhead for large objects
let bigConfig = $state(hugeImmutableObject); // Slower
// Skip proxies for data you don't mutate
let bigConfig = $state.raw(hugeImmutableObject); // Faster
</script>Use $state.raw() when:
- Data is large and immutable
- You'll replace entire object, not mutate it
- Performance is critical
Don't use $state.raw() when:
- You need to mutate nested properties
- Data is small/medium sized
3. Unnecessary $derived
UNNECESSARY:
<script>
let { count } = $props();
let doubled = $derived(count * 2); // Used only once
</script>
<p>{doubled}</p>SIMPLER:
<script>
let { count } = $props();
</script>
<p>{count * 2}</p> <!-- Inline is fine -->TypeScript Mistakes
1. Not Typing Props
WRONG:
<script lang="ts">
let { name, age } = $props(); // No types!
</script>RIGHT:
<script lang="ts">
interface Props {
name: string;
age: number;
}
let { name, age }: Props = $props();
</script>2. Wrong Bindable Type
WRONG:
<script lang="ts">
let { value = $bindable() }: { value: string } = $props();
// ^^^^^^ Should be optional
</script>RIGHT:
<script lang="ts">
let { value = $bindable('') }: { value?: string } = $props();
// ^ Optional
</script>Error Messages and Fixes
"Cannot access 'count' before initialization"
Cause: Using rune inside function or wrong order
<!-- WRONG -->
<script>
const doubled = count * 2;
let count = $state(0);
</script>
<!-- RIGHT -->
<script>
let count = $state(0);
const doubled = $derived(count * 2);
</script>"Cannot read properties of undefined (reading '$effect')"
Cause: Using rune outside component scope
<!-- WRONG -->
<script context="module">
let count = $state(0); // ERROR - not in component scope
</script>
<!-- RIGHT -->
<script>
let count = $state(0); // OK
</script>"bind:value is not available on this component"
Cause: Forgot $bindable
Fix: Add $bindable() to prop definition
Best Practices Summary
1. ✅ Prefer event handlers over $effect for side effects 2. ✅ Use $derived for computed values, not $effect 3. ✅ Use @attach for DOM element operations 4. ✅ Don't update dependencies inside $effect 5. ✅ Keep runes at component top-level 6. ✅ Use consistent Svelte 5 syntax (no mixing) 7. ✅ Wrap reactive variables with $state() 8. ✅ Use $bindable() for two-way binding 9. ✅ Use {@render children()} not {children} 10. ✅ Use onclick not on:click 11. ✅ Remember: $effect doesn't run during SSR
Debugging: $inspect.trace
$inspect.trace is a debugging tool for reactivity. Add it as the first line of an $effect or $derived.by to trace dependencies and discover which one triggered an update.
<script>
let count = $state(0);
let name = $state('world');
$effect(() => {
$inspect.trace('greeting effect');
console.log(`Hello ${name}, count is ${count}`);
});
const message = $derived.by(() => {
$inspect.trace('message derived');
return `${name}: ${count}`;
});
</script>When to use:
- Something is not updating when it should
- An effect or derived is running more often than expected
- You need to identify which dependency triggered a re-run
Note: Remove $inspect.trace before production — it's for debugging only, like {@debug}.
Component API: $props and $bindable
$props() - Accepting Props
Basic Usage
<script>
let { name, age } = $props();
</script>
<p>{name} is {age} years old</p>
<!-- Usage: <Person name="Alex" age={30} /> -->With Defaults
<script>
let { name = 'Anonymous', age = 18 } = $props();
</script>With TypeScript
<script lang="ts">
interface Props {
name: string;
age?: number; // Optional with default
}
let { name, age = 18 }: Props = $props();
</script>Rest Props
Capture all additional props:
<script>
let { name, age, ...rest } = $props();
</script>
<div {...rest}>
<p>{name} is {age}</p>
</div>
<!-- Usage: <Person name="Alex" age={30} class="card" id="p1" /> -->
<!-- rest = { class: 'card', id: 'p1' } -->Accessing All Props
<script>
let props = $props();
</script>
<p>{props.name} is {props.age}</p>When to use:
- When you need to pass all props to a child
- When you don't know prop names in advance
- When you want to iterate over props
$bindable() - Two-Way Binding
Basic Bindable Prop
<!-- Toggle.svelte -->
<script>
let { checked = $bindable(false) } = $props();
</script>
<input type="checkbox" bind:checked />
<!-- Usage -->
<script>
let isEnabled = $state(false);
</script>
<Toggle bind:checked={isEnabled} />
<p>Enabled: {isEnabled}</p>When to Use $bindable
Use when:
- Parent needs to read the value
- Parent needs to update the value
- Two-way data flow is appropriate
Examples:
- Form inputs (text, checkbox, select)
- Toggles, sliders, color pickers
- Custom input components
Default Values
<script>
let { value = $bindable('') } = $props();
// ^^^^^ default if parent doesn't provide
</script>Optional Bindable Props
<script lang="ts">
interface Props {
value?: string; // Optional
}
let { value = $bindable('default') }: Props = $props();
</script>Multiple Bindable Props
<!-- Slider.svelte -->
<script>
let { min = $bindable(0), max = $bindable(100) } = $props();
</script>
<input type="range" bind:value={min} />
<input type="range" bind:value={max} />
<!-- Usage -->
<script>
let minPrice = $state(0);
let maxPrice = $state(1000);
</script>
<Slider bind:min={minPrice} bind:max={maxPrice} />
<p>Range: ${minPrice} - ${maxPrice}</p>Common Patterns
Form Input Wrapper
<!-- Input.svelte -->
<script>
let {
value = $bindable(''),
label,
type = 'text',
...rest
} = $props();
</script>
<label>
{label}
<input {type} bind:value {...rest} />
</label>
<!-- Usage -->
<script>
let email = $state('');
</script>
<Input
bind:value={email}
label="Email"
type="email"
placeholder="you@example.com"
required
/>Controlled Component (No $bindable)
When parent fully controls the state:
<!-- Counter.svelte -->
<script>
let { count, onIncrement } = $props();
</script>
<button onclick={onIncrement}>
Count: {count}
</button>
<!-- Usage -->
<script>
let count = $state(0);
</script>
<Counter
{count}
onIncrement={() => count++}
/>Use this pattern when:
- Parent should control all updates
- You need to validate/transform updates
- Side effects should run on change
Hybrid: Bindable with Callback
<!-- Slider.svelte -->
<script>
let {
value = $bindable(50),
onChange
} = $props();
function handleChange() {
onChange?.(value); // Notify parent
}
</script>
<input
type="range"
bind:value
oninput={handleChange}
/>
<!-- Usage -->
<script>
let volume = $state(50);
</script>
<Slider
bind:value={volume}
onChange={(v) => console.log('Volume changed:', v)}
/>Props vs Bindable Decision Tree
Parent needs to read child state?
├─ No → Just pass callbacks (controlled component)
├─ Yes → Parent needs to UPDATE child state?
├─ No → Callback to notify parent (onChange pattern)
└─ Yes → Use $bindable (two-way binding)TypeScript Best Practices
Strict Props Interface
<script lang="ts">
interface Props {
// Required props
name: string;
age: number;
// Optional props
email?: string;
// Props with defaults (must be optional in interface)
role?: string;
// Bindable props
checked?: boolean;
// Callbacks
onSave?: (data: FormData) => void;
// Rest props (for spreading to elements)
[key: string]: unknown;
}
let {
name,
age,
email,
role = 'user',
checked = $bindable(false),
onSave,
...rest
}: Props = $props();
</script>Generic Components
<script lang="ts" generics="T">
interface Props<T> {
items: T[];
selected?: T;
onSelect?: (item: T) => void;
}
let { items, selected, onSelect }: Props<T> = $props();
</script>
{#each items as item}
<button onclick={() => onSelect?.(item)}>
{item}
</button>
{/each}Common Mistakes
❌ Forgetting $bindable
<!-- WRONG -->
<script>
let { value } = $props();
</script>
<input bind:value />
<!-- Parent tries: <Component bind:value={text} /> -->
<!-- ERROR: Cannot bind to non-bindable prop -->
<!-- RIGHT -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />❌ Mutating Non-Bindable Props
<!-- WRONG -->
<script>
let { count } = $props(); // Not bindable!
function increment() {
count++; // BAD - mutating prop from parent
}
</script>
<!-- RIGHT - Option 1: Use callback -->
<script>
let { count, onIncrement } = $props();
</script>
<button onclick={onIncrement}>+</button>
<!-- RIGHT - Option 2: Make bindable -->
<script>
let { count = $bindable(0) } = $props();
</script>
<button onclick={() => count++}>+</button>❌ Not Providing Default for Bindable
<!-- RISKY -->
<script>
let { value = $bindable() } = $props();
// ^^^ undefined if parent doesn't provide
</script>
<!-- SAFER -->
<script>
let { value = $bindable('default') } = $props();
// ^^^^^^^^^ explicit default
</script>❌ Unnecessary Bindable
<!-- WRONG - Overkill -->
<script>
let { label = $bindable('Submit') } = $props();
</script>
<button>{label}</button>
<!-- RIGHT - Label doesn't need to be bindable -->
<script>
let { label = 'Submit' } = $props();
</script>
<button>{label}</button>Rule of thumb: Only use $bindable when parent _needs_ to update the prop value.
Prop Drilling vs Context
For deeply nested components, use createContext instead of prop drilling. See createContext below for the recommended pattern.
Performance: Props Are Reactive
Props are automatically reactive - no need for extra $derived:
<!-- UNNECESSARY -->
<script>
let { count } = $props();
let doubled = $derived(count * 2); // Overkill if just used once
</script>
<p>{doubled}</p>
<!-- SIMPLER -->
<script>
let { count } = $props();
</script>
<p>{count * 2}</p>Use $derived when:
- Value is used multiple times
- Computation is expensive
- You need to derive from multiple props
createContext - Type-Safe Context
Per official Svelte best practices: use createContext rather than setContext and getContext, as it provides type safety.
Basic Usage
// context.ts
import { createContext } from 'svelte';
const [get_theme, set_theme] = createContext<{ current: string }>('theme');
export { get_theme, set_theme };<!-- Provider.svelte -->
<script>
import { set_theme } from './context';
let theme = $state('dark');
set_theme({
get current() { return theme; },
set current(value) { theme = value; }
});
</script>
{@render children()}<!-- Consumer.svelte -->
<script>
import { get_theme } from './context';
const theme = get_theme();
</script>
<p>Theme: {theme.current}</p>
<button onclick={() => theme.current = 'light'}>Light mode</button>Why createContext over set/getContext
| Feature | setContext/getContext | createContext |
|---|---|---|
| Type safety | Manual casting | Automatic |
| Key management | String keys (typo-prone) | Module-scoped |
| Default values | Manual check | Built-in support |
Context vs Shared Module State
Per best practices: use context instead of declaring state in a shared module. Context scopes state to the component tree, preventing leaks between users during SSR.
// BAD - shared module state leaks between SSR requests
export let theme = $state('dark');
// GOOD - context is scoped per component tree
const [get_theme, set_theme] = createContext<string>('theme');Migration Gotchas: Svelte 4 → Svelte 5
Quick Translation Table
| Svelte 4 | Svelte 5 | Notes |
|---|---|---|
let count = 0 | let count = $state(0) | Make reactive |
$: doubled = count * 2 | let doubled = $derived(count * 2) | Computed value |
$: { console.log(count); } | $effect(() => { console.log(count); }) | Side effect |
$: if (count > 10) { ... } | $effect(() => { if (count > 10) { ... } }) | Conditional effect |
export let name | let { name } = $props() | Props |
export let value (bindable) | let { value = $bindable() } = $props() | Two-way binding |
on:click={handler} | onclick={handler} | Event handler |
| `on:click\ | preventDefault` | onclick={(e) => { e.preventDefault(); ... }} |
<slot /> | {@render children()} | Default slot |
<slot name="header" /> | {@render header()} | Named slot |
| N/A | {#snippet name()}...{/snippet} | Define reusable markup |
Critical Differences
1. Reactive Statements → Runes
Svelte 4:
<script>
let count = 0;
$: doubled = count * 2; // Computed
$: {
// Effect
console.log(count);
document.title = `Count: ${count}`;
}
</script>Svelte 5:
<script>
let count = $state(0);
let doubled = $derived(count * 2); // Computed
$effect(() => {
// Effect
console.log(count);
document.title = `Count: ${count}`;
});
</script>Why: Runes are more explicit about intent (derived vs effect) and avoid ambiguity.
2. Props
Svelte 4:
<script>
export let name;
export let age = 18;
</script>Svelte 5:
<script>
let { name, age = 18 } = $props();
</script>With rest props:
<script>
let { name, age, ...rest } = $props();
</script>
<div {...rest}>{name}</div>3. Two-Way Binding (bind:)
Svelte 4:
<!-- Child.svelte -->
<script>
export let value;
</script>
<input bind:value />
<!-- Parent: <Child bind:value={text} /> -->Svelte 5:
<!-- Child.svelte -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />
<!-- Parent: <Child bind:value={text} /> -->Why: Explicit $bindable() makes it clear which props support two-way binding.
4. Event Handlers
Svelte 4:
<button on:click={handleClick}>Click</button>
<button on:click|preventDefault={handleClick}>Click</button>
<button on:click={() => count++}>Increment</button>Svelte 5:
<button onclick={handleClick}>Click</button>
<button
onclick={(e) => {
e.preventDefault();
handleClick(e);
}}>Click</button
>
<button onclick={() => count++}>Increment</button>Why: Standard DOM properties instead of directives.
5. Slots → Children & Snippets
Svelte 4:
<!-- Layout.svelte -->
<div class="layout">
<header><slot name="header" /></header>
<main><slot /></main>
</div>
<!-- Usage -->
<Layout>
<div slot="header">Header content</div>
Main content
</Layout>Svelte 5:
<!-- Layout.svelte -->
<script>
let { header, children } = $props();
</script>
<div class="layout">
<header>{@render header()}</header>
<main>{@render children()}</main>
</div>
<!-- Usage -->
<Layout>
{#snippet header()}
Header content
{/snippet}
Main content
</Layout>Why: More explicit and composable.
6. Lifecycle Functions
Svelte 4:
<script>
import { onMount, onDestroy } from 'svelte';
onMount(() => {
console.log('mounted');
return () => console.log('cleanup');
});
onDestroy(() => {
console.log('destroyed');
});
</script>Svelte 5:
<script>
import { onMount } from 'svelte';
onMount(() => {
console.log('mounted');
return () => console.log('cleanup');
});
// For most cleanup, use $effect:
$effect(() => {
const interval = setInterval(() => {...}, 1000);
return () => clearInterval(interval);
});
</script>Why: $effect with cleanup function covers most onDestroy use cases.
Common Migration Mistakes
❌ Mixing Svelte 4 and 5 Syntax
<!-- WRONG - DON'T MIX -->
<script>
let count = $state(0);
$: doubled = count * 2; // Mixing runes with reactive statements!
</script>Fix: Use runes consistently:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>❌ Forgetting to Use $state
<!-- WRONG -->
<script>
let count = 0; // Not reactive in Svelte 5!
</script>
<button onclick={() => count++}>{count}</button>
<!-- UI won't update! -->Fix:
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>❌ Using on: Instead of onclick
<!-- WRONG -->
<button on:click={handler}>Click</button>
<!-- RIGHT -->
<button onclick={handler}>Click</button>❌ Forgetting {@render} for Children
<!-- WRONG -->
<script>
let { children } = $props();
</script>
<div>{children}</div>
<!-- Won't render! -->
<!-- RIGHT -->
<div>{@render children()}</div>❌ Trying to Bind Without $bindable
<!-- Child.svelte - WRONG -->
<script>
let { value } = $props(); // Not bindable!
</script>
<!-- Parent tries: <Child bind:value={text} /> -->
<!-- Will error! -->
<!-- Child.svelte - RIGHT -->
<script>
let { value = $bindable() } = $props();
</script>Stores Still Work!
Svelte stores (writable, readable, derived) still work in Svelte 5:
<script>
import { writable } from 'svelte/store';
const count = writable(0);
</script>
<button onclick={() => $count++}>{$count}</button>When to use stores vs runes:
- Runes: Component-local state
- Stores: Global state, shared across components
TypeScript Changes
Svelte 4:
<script lang="ts">
export let count: number;
</script>Svelte 5:
<script lang="ts">
interface Props {
count: number;
}
let { count }: Props = $props();
</script>Reactive Class Fields
Svelte 5 introduces reactive class fields:
<script>
class Counter {
count = $state(0);
doubled = $derived(this.count * 2);
increment() {
this.count++;
}
}
const counter = new Counter();
</script>
<button onclick={() => counter.increment()}>
{counter.count} (doubled: {counter.doubled})
</button>Migration Strategy
1. Don't mix syntaxes - Migrate one component at a time fully 2. Start with leaf components - Migrate from bottom up 3. Test incrementally - Ensure each component works before moving on 4. Use TypeScript - Catch binding/prop errors at compile time 5. Read the migration guide - https://svelte.dev/docs/svelte/v5-migration-guide
Feature Detection
If you need to support both Svelte 4 and 5:
<script>
import { VERSION } from 'svelte/compiler';
const isSvelte5 = VERSION.startsWith('5');
// Conditionally use syntax based on version
</script>But: Generally better to fully migrate to Svelte 5 than maintain dual syntax.
Reactivity Patterns: When to Use Each Rune
Decision Matrix
| Need | Use | Why |
|---|---|---|
| Mutable state | $state() | Base reactive variable |
| Computed value | $derived() | Auto-updates when dependencies change |
| Complex computation | $derived.by() | Use function body for multi-line logic |
| Large immutable data | $state.raw() | Skip deep reactivity for performance |
| Read-only snapshot | $state.snapshot() | Get plain JS value, no proxy |
| Side effect | $effect() | Run code when dependencies change |
| Pre-DOM effect | $effect.pre() | Run before DOM updates |
| Accept props | $props() | Declare component props |
| Bindable prop | $bindable() | Allow parent to bind to prop |
| Reactive class field | $state (class field) | Reactive property in class |
$effect Decision Hierarchy
$effect is an escape hatch. Before using it, ask:
Need to react to state change?
├─ Can use event handler? → USE EVENT HANDLER (preferred)
├─ Is it a computed value? → USE $derived
├─ Is it DOM-specific? → USE @attach
└─ External side effect? → USE $effect (with cleanup)Why this order matters:
- Event handlers - Run once per user action, predictable timing
- $derived - Lazy, garbage-collectable, can be created anywhere
- @attach - Lifecycle tied to element, auto-cleanup
- $effect - Eager, requires lifecycle management, runs after DOM
$state - Mutable Reactive State
Use when: You need a variable that changes and triggers UI updates
<script>
let count = $state(0); // Primitive
let user = $state({ name: 'Alex', profile: { age: 30 } }); // Object (DEEP reactive)
let items = $state([1, 2, 3]); // Array (DEEP reactive)
</script>Key points:
- Must be top-level in component
- Objects/arrays are deeply reactive by default - nested mutations
trigger updates
- Mutate nested properties directly:
user.profile.age = 31✅
(works!)
- Reassigning also works:
user = { ...user, name: 'Bo' }✅
$derived - Computed Values
Use when: Value is calculated from other state
<script>
let count = $state(0);
let doubled = $derived(count * 2); // Simple computation
let message = $derived.by(() => {
// Complex computation
if (count === 0) return 'Zero';
return count > 10 ? 'High' : 'Low';
});
</script>Key points:
- Can be overridden - Reassignment allowed (but will recalculate
on dependency change)
- Use
constto make truly read-only:
const doubled = $derived(count * 2)
- Auto-tracks dependencies
- Use
$derived.by()for multi-line logic - Lazy - only computes when accessed
$effect - Side Effects (Escape Hatch)
Use when: You need external side effects that can't be handled by event handlers, $derived, or @attach.
<script>
let count = $state(0);
$effect(() => {
console.log(`Count changed to ${count}`);
document.title = `Count: ${count}`;
});
</script>Legitimate use cases:
- Logging/analytics
- Updating external state (localStorage, document.title)
- Setting up/tearing down subscriptions (WebSocket, intervals)
- Third-party library integration (when @attach isn't suitable)
Key points:
- Eager execution - Runs whenever dependencies change, until destroyed
- Lifecycle-bound - Can only be created in effect roots (components)
- Runs after DOM - Not before (use
$effect.prefor pre-DOM) - No SSR - Effects don't run during server-side rendering
- Return cleanup function:
return () => cleanup() - Don't update state that effect depends on (infinite loop!)
Why $derived is preferred for computed values:
- $derived is lazy - only computes when accessed
- $derived is garbage-collectable - no lifecycle management needed
- $effect is eager - keeps running until destroyed
$effect.pre - Pre-DOM Effects
Use when: You need to run before DOM updates
<script>
let element = $state(null);
$effect.pre(() => {
// Runs BEFORE DOM updates
// Useful for measuring DOM before changes
});
</script>$props - Component Props
Use when: Component accepts props from parent
<script>
let { name, age = 18, ...rest } = $props(); // Destructure with defaults
// OR
let props = $props(); // Access as props.name, props.age
</script>
<p>{name} is {age} years old</p>Key points:
- Replaces
export letfrom Svelte 4 - Supports defaults and rest props
- Props are reactive automatically
$bindable - Bindable Props
Use when: Parent should be able to bind to this prop
<!-- Child.svelte -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script>
let text = $state('');
</script>
<Child bind:value={text} />
<p>You typed: {text}</p>Key points:
- Makes prop two-way bindable
- Parent can use
bind:propName - Provide default:
$bindable('default')
Common Anti-Patterns
❌ Using $effect for derived state
<!-- WRONG -->
<script>
let count = $state(0);
let doubled = $state(0);
$effect(() => {
doubled = count * 2; // BAD - use $derived!
});
</script>
<!-- RIGHT -->
<script>
let count = $state(0);
let doubled = $derived(count * 2); // GOOD
</script>⚠️ Reassigning $derived (Svelte 5.25+)
<!-- WORKS but may be confusing -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
function reset() {
doubled = 0; // Temporarily overrides, but recalculates when count changes
}
</script>
<!-- CLEARER - Use const to prevent reassignment -->
<script>
let count = $state(0);
const doubled = $derived(count * 2); // const = truly read-only
function reset() {
// doubled = 0; // TypeScript error - cannot reassign const
}
</script>Note: As of Svelte 5.25+, $derived values CAN be reassigned, but they'll recalculate when dependencies change. Use const to make them truly read-only.
❌ Infinite loops in $effect
<!-- WRONG -->
<script>
let count = $state(0);
$effect(() => {
count++; // INFINITE LOOP - effect updates count, triggers effect...
});
</script>❌ Using runes inside functions
<!-- WRONG -->
<script>
function createCounter() {
let count = $state(0); // ERROR - runes must be top-level
return count;
}
</script>Performance: $state vs $state.raw
Use $state.raw() for performance optimization when you don't need reactivity:
<script>
// Large immutable config (never changes)
let config = $state.raw(hugeConfigObject); // No proxy overhead
// Data you'll replace entirely, not mutate
let apiData = $state.raw(data);
// Later: apiData = newData; (full replacement)
// If you WILL mutate nested properties, use $state:
let user = $state({ profile: { name: 'Alex' } });
user.profile.name = 'Bo'; // Works with deep reactivity
</script>When to use $state.raw():
- Large, immutable data structures (config, constants)
- Data you'll fully replace, not incrementally mutate
- Performance-critical scenarios where proxies are expensive
When NOT to use $state.raw():
- You need to mutate the object and see UI updates
- Data structures are small/medium sized
Getting Plain Values: $state.snapshot
Extract plain JavaScript values from proxies:
<script>
let user = $state({ name: 'Alex', age: 30 });
function saveToAPI() {
const plain = $state.snapshot(user); // Get plain object
fetch('/api/users', {
body: JSON.stringify(plain),
});
}
</script>createSubscriber - External Observables
Use createSubscriber from svelte/reactivity to observe external state without $effect. Per official best practices: use this instead of $effect when you need to observe something external to Svelte.
import { createSubscriber } from 'svelte/reactivity';
function createLocationStore() {
let location = window.location.href;
const subscribe = createSubscriber((update) => {
const handler = () => {
location = window.location.href;
update();
};
window.addEventListener('popstate', handler);
return () => window.removeEventListener('popstate', handler);
});
return {
get href() {
subscribe();
return location;
}
};
}When to use: Wrapping browser APIs, third-party event emitters, or any external source that doesn't integrate with Svelte's reactivity natively.
Snippets vs Slots: New Content Composition in Svelte 5
Quick Comparison
| Feature | Svelte 4 (Slots) | Svelte 5 (Snippets + Children) |
|---|---|---|
| Default content | <slot /> | {@render children()} |
| Named content | <slot name="header" /> | {@render header()} |
| Provide content | <div slot="header">...</div> | {#snippet header()}...{/snippet} |
| Slot props | <slot item={data} /> | {@render item(data)} |
| Fallback content | <slot>Fallback</slot> | {@render children?.() ?? 'Fallback'} |
Children (Default Slot Replacement)
Svelte 4: <slot />
<!-- Card.svelte -->
<div class="card">
<slot />
</div>
<!-- Usage -->
<Card>
<p>This is card content</p>
</Card>Svelte 5: {@render children()}
<!-- Card.svelte -->
<script>
let { children } = $props();
</script>
<div class="card">
{@render children()}
</div>
<!-- Usage -->
<Card>
<p>This is card content</p>
</Card>Key differences:
- Must declare
childrenin$props() - Use
{@render children()}to render - More explicit
Named Snippets (Named Slots Replacement)
Svelte 4: Named Slots
<!-- Layout.svelte -->
<div class="layout">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
<!-- Usage -->
<Layout>
<div slot="header">Header content</div>
<div slot="footer">Footer content</div>
Main content
</Layout>Svelte 5: Named Snippets
<!-- Layout.svelte -->
<script>
let { header, footer, children } = $props();
</script>
<div class="layout">
<header>{@render header()}</header>
<main>{@render children()}</main>
<footer>{@render footer()}</footer>
</div>
<!-- Usage -->
<Layout>
{#snippet header()}
Header content
{/snippet}
{#snippet footer()}
Footer content
{/snippet}
Main content
</Layout>Key differences:
- Snippets are props
- More structured and typed
- Can pass snippets around like functions
Snippet Parameters (Slot Props Replacement)
Svelte 4: Slot Props
<!-- List.svelte -->
<script>
export let items;
</script>
<ul>
{#each items as item}
<li>
<slot {item} index={i} />
</li>
{/each}
</ul>
<!-- Usage -->
<List items={users} let:item let:index>
{index}: {item.name}
</List>Svelte 5: Snippet Parameters
<!-- List.svelte -->
<script>
let { items, children } = $props();
</script>
<ul>
{#each items as item, i}
<li>
{@render children(item, i)}
</li>
{/each}
</ul>
<!-- Usage -->
<List items={users}>
{#snippet children(item, index)}
{index}: {item.name}
{/snippet}
</List>Key improvements:
- Parameters are explicit function arguments
- Better TypeScript support
- More intuitive syntax
Optional Snippets (Fallback Content)
With Fallback
<!-- Card.svelte -->
<script>
let { header, children } = $props();
</script>
<div class="card">
{#if header}
<h2>{@render header()}</h2>
{:else}
<h2>Default Title</h2>
{/if}
{@render children()}
</div>
<!-- Usage without header -->
<Card>
<p>Content only</p>
</Card>
<!-- Usage with header -->
<Card>
{#snippet header()}
Custom Title
{/snippet}
<p>Content</p>
</Card>Shorthand with ?.()
<script>
let { header, children } = $props();
</script>
<div class="card">
<h2>{@render header?.() ?? 'Default Title'}</h2>
{@render children()}
</div>Reusable Snippets
Snippets can be defined and reused within a component:
<script>
let items = $state(['Apple', 'Banana', 'Cherry']);
</script>
{#snippet listItem(text)}
<li class="item">{text}</li>
{/snippet}
<ul>
{#each items as item}
{@render listItem(item)}
{/each}
</ul>
<ul>
{#each items.slice(0, 2) as item}
{@render listItem(item)}
{/each}
</ul>Benefits:
- DRY (Don't Repeat Yourself)
- Keeps markup organized
- Can be passed to child components
Passing Snippets as Props
<!-- Table.svelte -->
<script>
let { data, renderCell } = $props();
</script>
<table>
{#each data as row}
<tr>
{#each row as cell}
<td>{@render renderCell(cell)}</td>
{/each}
</tr>
{/each}
</table>
<!-- Usage -->
<script>
let data = $state([[1, 2], [3, 4]]);
</script>
{#snippet boldCell(value)}
<strong>{value}</strong>
{/snippet}
<Table {data} renderCell={boldCell} />TypeScript with Snippets
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
header?: Snippet;
item?: Snippet<[{ name: string; age: number }]>; // Snippet with params
}
let { children, header, item }: Props = $props();
</script>
{#if header}
{@render header()}
{/if}
{@render children()}
{#if item}
{@render item({ name: 'Alex', age: 30 })}
{/if}Common Patterns
Conditional Rendering
<script>
let { header, showHeader = true, children } = $props();
</script>
{#if showHeader && header}
{@render header()}
{/if}
{@render children()}Multiple Children Sections
<script>
let { sidebar, main } = $props();
</script>
<div class="layout">
<aside>{@render sidebar()}</aside>
<main>{@render main()}</main>
</div>
<!-- Usage -->
<Layout>
{#snippet sidebar()}
<nav>Navigation</nav>
{/snippet}
{#snippet main()}
<p>Main content</p>
{/snippet}
</Layout>Snippet with Complex Logic
{#snippet userCard(user)}
<div class="card">
<h3>{user.name}</h3>
{#if user.email}
<p>{user.email}</p>
{/if}
{#if user.premium}
<span class="badge">Premium</span>
{/if}
</div>
{/snippet}
{#each users as user}
{@render userCard(user)}
{/each}Migration Guide
1. Simple Slot → Children
Before:
<div class="wrapper">
<slot />
</div>After:
<script>
let { children } = $props();
</script>
<div class="wrapper">
{@render children()}
</div>2. Named Slots → Named Snippets
Before:
<slot name="title" />
<slot name="content" />After:
<script>
let { title, content } = $props();
</script>
{@render title()}
{@render content()}3. Slot Props → Snippet Parameters
Before:
{#each items as item}
<slot {item} />
{/each}After:
<script>
let { children } = $props();
</script>
{#each items as item}
{@render children(item)}
{/each}4. Optional Slots → Optional Snippets
Before:
{#if $$slots.header}
<slot name="header" />
{:else}
<h1>Default</h1>
{/if}After:
<script>
let { header } = $props();
</script>
{#if header}
{@render header()}
{:else}
<h1>Default</h1>
{/if}Common Mistakes
❌ Forgetting to Declare children
<!-- RIGHT -->
<script>
let { children } = $props();
</script>
<!-- WRONG -->
<div>
{@render children()}
<!-- ERROR: children not defined -->
</div>
<div>
{@render children()}
</div>❌ Using Parentheses Wrong
<!-- WRONG -->
<script>
let { children } = $props();
</script>
{@render children} <!-- Missing () -->
<!-- RIGHT -->
{@render children()}❌ Mixing Svelte 4 and 5 Syntax
<!-- WRONG -->
<script>
let { children } = $props();
</script>
<slot />
<!-- Don't mix slot with snippet syntax! -->
<!-- RIGHT -->
{@render children()}❌ Not Handling Missing Optional Snippets
<!-- RISKY -->
<script>
let { header } = $props();
</script>
{@render header()}
<!-- Error if header not provided! -->
<!-- SAFE -->
{#if header}
{@render header()}
{/if}
<!-- OR -->
{@render header?.()}Why Snippets Are Better
1. More explicit - Props make it clear what content slots exist 2. Better TypeScript support - Can type snippet parameters 3. More composable - Snippets can be passed around like functions 4. Cleaner syntax - No let:prop bindings 5. More powerful - Can define reusable snippets within components 6. Consistent - Everything is a prop, not a special <slot> element
Related skills
How it compares
Pick svelte-runes over generic Svelte skills when the codebase is on Svelte 5 runes and store migration is the primary task.
FAQ
Which runes does svelte-runes cover?
svelte-runes covers Svelte 5 $state for mutable state, $derived for computed values, and $effect for side effects when authoring or migrating components in spences10/svelte-skills-kit projects.
When should agents use svelte-runes?
Agents should use svelte-runes when building new Svelte 5 features, migrating legacy stores, or fixing reactive UI logic in SvelteKit apps that target runes semantics.