
Svelte Runes
- 22 installs
- 217 repo stars
- Updated August 3, 2026
- spences10/svelte-claude-skills
Helps with ai & agent building tasks.
About
svelte-runes is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- svelte-runes
- AI & Agent Building
- AI-coding skill
Svelte Runes by the numbers
- 22 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #10,169 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/svelte-claude-skills --skill svelte-runesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 22 |
|---|---|
| repo stars | ★ 217 |
| Last updated | August 3, 2026 |
| Repository | spences10/svelte-claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
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
- attachments.md - @attach replaces use:
actions
Notes
- Use
onclicknoton:click,{@render children()}in layouts $derivedcan be reassigned (5.25+) - useconstfor read-only- Last verified: 2025-01-11
<!-- 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: claude-skills-cli validate <path> 4. If multi-line description warning: run claude-skills-cli doctor <path> 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 (, , , , ). 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 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 Claude when relevant to the task.
Attachments: The Modern Alternative to Actions
Available in Svelte 5.29+
Quick Decision
Use `@attach` instead of `use:` for new code. Attachments are more flexible and composable.
Basic Syntax
<script>
const myAttachment = (element) => {
console.log(element.nodeName);
return () => console.log('cleanup');
};
</script>
<div {@attach myAttachment}>...</div>Key Differences from Actions
| Feature | Actions (use:) | Attachments (@attach) |
|---|---|---|
| Re-runs on arg change | No | Yes |
| Multiple per element | Yes | Yes |
| Composable | Limited | Fully |
| Pass through components | Manual | Automatic via spread |
Attachment Factories (Common Pattern)
<script>
function tooltip(content) {
return (element) => {
const instance = tippy(element, { content });
return instance.destroy;
};
}
let content = $state('Hello');
</script>
<!-- Re-runs when content changes -->
<button {@attach tooltip(content)}>Hover me</button>Inline Attachments
<canvas
{@attach (canvas) => {
const ctx = canvas.getContext('2d');
$effect(() => {
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
});
}}
/>Component Pass-Through
Attachments pass through automatically when spreading props:
<!-- Button.svelte -->
<script>
let { children, ...props } = $props();
</script>
<button {...props}>
{@render children?.()}
</button>
<!-- Usage -->
<Button {@attach tooltip('Help')}>Click me</Button>Avoid Expensive Re-runs
Pass data via accessor functions to prevent setup re-execution:
<script>
function expensiveAttachment(getData) {
return (node) => {
veryExpensiveSetup(node); // Runs once
$effect(() => {
update(node, getData()); // Re-runs on data change
});
};
}
let data = $state({ value: 1 });
</script>
<div {@attach expensiveAttachment(() => data.value)}>...</div>Converting Actions to Attachments
Use fromAction for existing action libraries:
<script>
import { fromAction } from 'svelte/attachments';
import { someAction } from 'some-library';
const attached = fromAction(someAction);
</script>
<div {@attach attached(options)}>...</div>When to Still Use Actions
- Legacy code/libraries not yet updated
- When you specifically DON'T want re-runs on arg change
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.
---
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.
---
4. 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. ✅ Use $derived for computed values, not $effect 2. ✅ Never reassign $derived values 3. ✅ Don't update dependencies inside $effect 4. ✅ Keep runes at component top-level 5. ✅ Reassign objects/arrays for nested changes 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
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, consider Context API instead of prop drilling:
<!-- App.svelte -->
<script>
import { setContext } from 'svelte';
let theme = $state('dark');
setContext('theme', {
get current() { return theme; },
set current(value) { theme = value; }
});
</script>
<!-- DeepChild.svelte -->
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
</script>
<p>Current theme: {theme.current}</p>
<button onclick={() => theme.current = 'light'}>
Switch to light
</button>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
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 |
$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
Use when: You need to run code in response to state changes
<script>
let count = $state(0);
$effect(() => {
console.log(`Count changed to ${count}`);
document.title = `Count: ${count}`;
});
</script>Use cases:
- Logging/analytics
- Updating external state (localStorage, DOM)
- Fetching data
- Setting up/tearing down subscriptions
Key points:
- Runs after DOM updates
- Auto-tracks dependencies (any $state accessed)
- Return cleanup function:
return () => cleanup() - Don't update state that effect depends on (infinite loop!)
$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>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