Learn-skills.dev web-framework-svelte
Svelte 5 Runes reactivity - $state, $derived, $effect, $props, $bindable, components, snippets, event handling, context API
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/agents-inc/skills/web-framework-svelte" ~/.claude/skills/neversight-learn-skills-dev-web-framework-svelte && rm -rf "$T"
data/skills-md/agents-inc/skills/web-framework-svelte/SKILL.mdSvelte 5 Patterns
Quick Guide: Svelte 5 uses Runes for explicit reactivity. Use
for reactive variables,$statefor computed values,$derivedonly as an escape hatch. Use snippets instead of slots. Use callback props instead of event dispatchers. Keep components small and composable.$effect
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use Svelte 5 Runes syntax — NOT Svelte 4 patterns like
, export let
, or stores for component state)$:
(You MUST use
for computed values — NEVER use $derived
to synchronize state)$effect
(You MUST use snippets (
/ {#snippet}
) instead of slots ({@render}
))<slot>
(You MUST use callback props (
, onclick
) instead of onsomething
)createEventDispatcher
(You MUST use
for large objects/arrays that are replaced, not mutated)$state.raw()
(You MUST use
for type-safe context instead of raw createContext
/setContext
with string keys)getContext
</critical_requirements>
Auto-detection: Svelte 5, Runes, $state, $derived, $effect, $props, $bindable, $inspect, .svelte, snippet, @render, createContext, getContext, setContext, $state.raw, $state.eager, $derived.by, $effect.pre, ClassValue
When to use:
- Building Svelte 5 components with Runes reactivity
- Managing component state with
and computed values with$state$derived - Creating reusable markup with snippets (replacing slots)
- Handling events with native event attributes and callback props
- Sharing state across components with context API
- Two-way binding with
props$bindable
Key patterns covered:
- Runes:
,$state
,$derived
,$effect
,$props
,$bindable$inspect - Component composition with snippets and
{@render} - Event handling with native attributes and callback props
- Context API with
for type-safe cross-component statecreateContext - Class-based reactive state with
fields$state - Deep vs shallow reactivity (
vs$state
)$state.raw
When NOT to use:
- Meta-framework-specific patterns (routing, load functions, form actions) — use the corresponding meta-framework skill
- Svelte 4 patterns (
,export let
reactive statements,$:
,<slot>
)createEventDispatcher - Server-side logic (use your meta-framework's server hooks and routes)
Detailed Resources:
- For decision frameworks and anti-patterns, see reference.md
Runes & Reactivity:
- examples/core.md -
,$state
,$derived
,$effect
,$props
, component patterns$bindable
Component Patterns:
- examples/snippets.md - Snippet blocks,
, passing snippets as props, replacing slots{@render} - examples/events.md - Event handling, component events via callback props, event modifiers
Advanced:
- examples/advanced.md -
, context API,$inspect
,$state.raw
, class-based state, shared state modules$state.eager
<philosophy>
Philosophy
Svelte 5 introduces Runes — a set of primitives that bring explicit, fine-grained reactivity to Svelte. Unlike Svelte 4's compiler magic (
$:, export let), Runes make reactivity visible and portable across .svelte files, .ts files, and class definitions.
Core principles:
- Explicit reactivity — Runes (
,$state
,$derived
) make reactive declarations visible. No hidden compiler transformations.$effect - Derived over effects — Compute values with
, not$derived
. Effects are escape hatches, not primary tools.$effect - Deep reactivity by default —
creates deeply reactive proxies for objects/arrays. Mutations are tracked automatically.$state - Snippets replace slots —
blocks are more powerful, typed, and composable than{#snippet}
elements.<slot> - Callback props replace event dispatchers — Pass
callback props instead of usingonsomething
.createEventDispatcher - Compile-time optimization — Svelte compiles components to efficient imperative code. No virtual DOM diffing at runtime.
When to use Svelte 5 Runes:
- All new Svelte components (Runes are the default in Svelte 5)
- Reactive state in
or.svelte.ts
files.svelte.js - Class-based state with reactive fields
- Any computed value that depends on reactive state
When NOT to use:
- Non-reactive constants (use plain
orconst
)let - Server-side code that doesn't need reactivity
- Meta-framework concerns (routing, load functions, server hooks) — use the corresponding meta-framework skill
- Svelte 4 patterns —
,export let
, stores for component state,$:
,<slot>createEventDispatcher
<patterns>
Core Patterns
Pattern 1: Reactive State with $state
Use
$state to declare reactive variables. Updates to $state variables automatically trigger UI re-renders.
<!-- counter.svelte --> <script lang="ts"> let count = $state(0); const STEP = 5; function increment() { count += 1; } function incrementByStep() { count += STEP; } </script> <button onclick={increment}> Count: {count} </button> <button onclick={incrementByStep}> +{STEP} </button>
Why good: Explicit reactive declaration, named constants for magic numbers, plain function event handlers
<!-- BAD: Svelte 4 style --> <script> let count = 0; // Not explicitly reactive in Svelte 5 mode $: doubled = count * 2; // Svelte 4 reactive statement </script>
Why bad:
$: is Svelte 4 syntax deprecated in Svelte 5, implicit reactivity is confusing and non-portable
Deep Reactivity
$state creates deep proxies for objects and arrays — mutations are tracked automatically:
<script lang="ts"> interface Todo { done: boolean; text: string; } let todos = $state<Todo[]>([ { done: false, text: 'Learn Svelte 5' } ]); function addTodo(text: string) { todos.push({ done: false, text }); // Mutation tracked! } function toggleTodo(index: number) { todos[index].done = !todos[index].done; // Deep mutation tracked! } </script>
Why good: No need for immutable update patterns, array methods like
.push() trigger reactivity, property mutations tracked deeply
Pattern 2: Computed Values with $derived
Use
$derived for values that depend on other reactive state. Never use $effect to synchronize state.
<script lang="ts"> let count = $state(0); // Simple expression let doubled = $derived(count * 2); // Complex computation with $derived.by let stats = $derived.by(() => { const isEven = count % 2 === 0; const isPositive = count > 0; return { isEven, isPositive }; }); </script> <p>{count} doubled is {doubled}</p> <p>Even: {stats.isEven}, Positive: {stats.isPositive}</p>
Why good: Automatically recalculates when dependencies change, no side effects, push-pull reactivity avoids unnecessary recalculations
<!-- BAD: Using $effect to synchronize state --> <script lang="ts"> let count = $state(0); let doubled = $state(0); $effect(() => { doubled = count * 2; // WRONG: Use $derived instead }); </script>
Why bad:
$effect for derived state creates unnecessary reactive subscriptions, runs after DOM update (timing issues), harder to reason about data flow
Pattern 3: Component Props with $props
Use
$props to declare component inputs. Supports destructuring, defaults, rest props, and TypeScript.
<!-- user-card.svelte --> <script lang="ts"> interface Props { name: string; email: string; role?: string; class?: string; } let { name, email, role = 'member', ...rest }: Props = $props(); // Derived from props — updates when props change let initials = $derived( name.split(' ').map(n => n[0]).join('').toUpperCase() ); </script> <div class="user-card" {...rest}> <span class="avatar">{initials}</span> <h3>{name}</h3> <p>{email}</p> <span class="badge">{role}</span> </div>
Why good: Type-safe props with interface, destructuring with defaults, rest props for pass-through, derived values update with prop changes
<!-- BAD: Svelte 4 style --> <script> export let name; // Svelte 4 prop declaration export let email; export let role = 'member'; </script>
Why bad:
export let is Svelte 4 syntax deprecated in Svelte 5, no type safety, no rest props
Pattern 4: Two-Way Binding with $bindable
Use
$bindable to declare props that support two-way binding with bind:. Use sparingly — prefer one-way data flow.
<!-- text-input.svelte --> <script lang="ts"> interface Props { value: string; placeholder?: string; } let { value = $bindable(''), placeholder = '' }: Props = $props(); </script> <input bind:value={value} {placeholder} class="text-input" />
<!-- parent.svelte --> <script lang="ts"> import TextInput from './text-input.svelte'; let searchQuery = $state(''); </script> <TextInput bind:value={searchQuery} placeholder="Search..." /> <p>Searching for: {searchQuery}</p>
Why good: Explicit two-way binding declaration, parent controls the state, child can modify via
bind:, TypeScript-safe
When to use: Form inputs, UI primitives (sliders, toggles) where two-way binding simplifies the API
When not to use: Most component communication — prefer callback props for explicit data flow
Pattern 5: Side Effects with $effect
Use
$effect for side effects that need to run when reactive state changes. This is an escape hatch — prefer $derived for computed values and event handlers for user-triggered actions.
<script lang="ts"> let searchQuery = $state(''); let results = $state<string[]>([]); const DEBOUNCE_MS = 300; // Good: Side effect for external API calls $effect(() => { const query = searchQuery; if (!query) { results = []; return; } const timer = setTimeout(async () => { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`); results = await response.json(); }, DEBOUNCE_MS); // Cleanup function runs before next effect and on unmount return () => clearTimeout(timer); }); </script> <input bind:value={searchQuery} placeholder="Search..." /> {#each results as result} <p>{result}</p> {/each}
Why good: External API call is a legitimate side effect, cleanup prevents stale requests, named constant for debounce
When NOT to Use $effect
<script lang="ts"> let count = $state(0); // BAD: Synchronizing state — use $derived // $effect(() => { doubled = count * 2; }); // BAD: Logging in effect — use $inspect for debugging // $effect(() => { console.log(count); }); // BAD: Calling functions on change — use event handlers // $effect(() => { if (count > 10) showAlert(); }); // GOOD: Use $derived for computed values let doubled = $derived(count * 2); </script>
Pattern 6: Snippets (Replacing Slots)
Snippets are reusable markup blocks declared with
{#snippet} and rendered with {@render}. They replace Svelte 4's <slot> elements.
<!-- card.svelte --> <script lang="ts"> import type { Snippet } from 'svelte'; interface Props { title: string; children: Snippet; footer?: Snippet; } let { title, children, footer }: Props = $props(); </script> <div class="card"> <h2>{title}</h2> <div class="card-body"> {@render children()} </div> {#if footer} <div class="card-footer"> {@render footer()} </div> {/if} </div>
<!-- usage --> <script lang="ts"> import Card from './card.svelte'; </script> <Card title="Welcome"> <p>This becomes the children snippet automatically.</p> {#snippet footer()} <button>Learn More</button> {/snippet} </Card>
Why good: Type-safe with
Snippet type, optional snippets with conditional rendering, children is implicit for content between tags
<!-- BAD: Svelte 4 slots --> <div class="card"> <slot /> <!-- Deprecated in Svelte 5 --> <slot name="footer" /> <!-- Use snippets instead --> </div>
Why bad:
<slot> is deprecated in Svelte 5, no type safety, less composable than snippets
Pattern 7: Event Handling
Svelte 5 uses native event attributes (
onclick, onsubmit) instead of Svelte 4's on:click directive. Component events use callback props.
Element Events
<script lang="ts"> let count = $state(0); function handleClick(event: MouseEvent) { count += 1; } function handleSubmit(event: SubmitEvent) { event.preventDefault(); // handle form } </script> <button onclick={handleClick}>Clicked {count} times</button> <!-- Inline handlers are fine for simple logic --> <button onclick={() => count = 0}>Reset</button> <form onsubmit={handleSubmit}> <input name="query" /> <button type="submit">Search</button> </form>
Component Events via Callback Props
<!-- color-picker.svelte --> <script lang="ts"> interface Props { color: string; onchange?: (color: string) => void; onreset?: () => void; } let { color, onchange, onreset }: Props = $props(); const COLORS = ['red', 'green', 'blue', 'purple'] as const; </script> {#each COLORS as c} <button onclick={() => onchange?.(c)} class={{ selected: color === c }} > {c} </button> {/each} {#if onreset} <button onclick={onreset}>Reset</button> {/if}
<!-- parent.svelte --> <script lang="ts"> import ColorPicker from './color-picker.svelte'; let selectedColor = $state('red'); </script> <ColorPicker color={selectedColor} onchange={(c) => selectedColor = c} onreset={() => selectedColor = 'red'} />
Why good: Type-safe callback props, optional with
?. call, parent controls event handling, no indirection through dispatcher
<!-- BAD: Svelte 4 event dispatcher --> <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); function handleClick() { dispatch('change', { color: 'red' }); // Deprecated pattern } </script>
Why bad:
createEventDispatcher is deprecated in Svelte 5, no type safety, requires manual event typing
</patterns>
<integration>
Integration Guide
Styling integration:
- Scoped
blocks are the default — styles don't leak to other components<style> - Use
for global styles or CSS custom properties for parent-to-child styling:global() - Any CSS approach (CSS Modules, utility-first, preprocessors) works with Svelte
State management:
for component-local state$state- Context API (
) for subtree-scoped statecreateContext - Reactive classes with
fields for shared state modules ($state
).svelte.ts - Meta-framework load functions for server state
TypeScript integration:
- Full TypeScript support in
blocks<script lang="ts">
for typed snippet propsSnippet<[ParamType]>- Interface-based prop typing with
$props()
type fromClassValue
for type-safe class props (Svelte 5.19+)svelte/elements
<red_flags>
RED FLAGS
High Priority:
- Using
for props — useexport let
instead$props() - Using
reactive statements — use$:
or$derived$effect - Using
or<slot>
— use<slot name="x">
and{#snippet}{@render} - Using
— use callback propscreateEventDispatcher - Using
to sync state — use$effect
for computed values$derived - Destructuring
objects — breaks reactivity (values captured at destructure time)$state
Medium Priority:
- Using
directive — useon:click
attributeonclick - Not using
for large API responses — unnecessary proxy overhead$state.raw() - Using
/setContext
with string keys — usegetContext
for type safetycreateContext - Using
— use built-inclass:name={condition}
attribute object/array syntax (since 5.16)class
Gotchas:
proxies are not the original object — use$state
to get a plain copy$state.snapshot()- Destructuring
captures values, not references — access properties directly instead$state
return values are NOT deeply reactive — only$derived
creates deep proxies$state
runs after DOM update — use$effect
for pre-update timing$effect.pre()- Dependencies after
inawait
are not tracked$effect - Context must be set during component init — cannot call
in event handlers orsetContext$effect
For complete decision frameworks and the full anti-patterns list, see reference.md.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use Svelte 5 Runes syntax — NOT Svelte 4 patterns like
, export let
, or stores for component state)$:
(You MUST use
for computed values — NEVER use $derived
to synchronize state)$effect
(You MUST use snippets (
/ {#snippet}
) instead of slots ({@render}
))<slot>
(You MUST use callback props (
, onclick
) instead of onsomething
)createEventDispatcher
(You MUST use
for large objects/arrays that are replaced, not mutated)$state.raw()
(You MUST use
for type-safe context instead of raw createContext
/setContext
with string keys)getContext
Failure to follow these rules will produce outdated Svelte 4 code that is deprecated and will break in future versions.
</critical_reminders>