dist/plugins/web-framework-svelte/skills/web-framework-svelte/SKILL.md
Svelte 5 Runes reactivity - $state, $derived, $effect, $props, $bindable, components, snippets, event handling, context API
npx skillsauth add agents-inc/skills web-framework-svelteInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Svelte 5 uses Runes for explicit reactivity. Use
$statefor reactive variables,$derivedfor computed values,$effectonly as an escape hatch. Use snippets instead of slots. Use callback props instead of event dispatchers. Keep components small and composable.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use Svelte 5 Runes syntax — NOT Svelte 4 patterns like export let, $:, or stores for component state)
(You MUST use $derived for computed values — NEVER use $effect to synchronize state)
(You MUST use snippets ({#snippet} / {@render}) instead of slots (<slot>))
(You MUST use callback props (onclick, onsomething) instead of createEventDispatcher)
(You MUST use $state.raw() for large objects/arrays that are replaced, not mutated)
(You MUST use createContext for type-safe context instead of raw setContext/getContext with string keys)
</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:
$state and computed values with $derived$bindable propsKey patterns covered:
$state, $derived, $effect, $props, $bindable, $inspect{@render}createContext for type-safe cross-component state$state fields$state vs $state.raw)When NOT to use:
export let, $: reactive statements, <slot>, createEventDispatcher)Detailed Resources:
Runes & Reactivity:
$state, $derived, $effect, $props, $bindable, component patternsComponent Patterns:
{@render}, passing snippets as props, replacing slotsAdvanced:
$inspect, context API, $state.raw, $state.eager, class-based state, shared state modulesSvelte 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:
$state, $derived, $effect) make reactive declarations visible. No hidden compiler transformations.$derived, not $effect. Effects are escape hatches, not primary tools.$state creates deeply reactive proxies for objects/arrays. Mutations are tracked automatically.{#snippet} blocks are more powerful, typed, and composable than <slot> elements.onsomething callback props instead of using createEventDispatcher.When to use Svelte 5 Runes:
.svelte.ts or .svelte.js filesWhen NOT to use:
const or let)export let, $:, stores for component state, <slot>, createEventDispatcherUse $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
$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
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
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
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
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
<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>
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
Svelte 5 uses native event attributes (onclick, onsubmit) instead of Svelte 4's on:click directive. Component events use callback props.
<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>
<!-- 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
Styling integration:
<style> blocks are the default — styles don't leak to other components:global() for global styles or CSS custom properties for parent-to-child stylingState management:
$state for component-local statecreateContext) for subtree-scoped state$state fields for shared state modules (.svelte.ts)TypeScript integration:
<script lang="ts"> blocksSnippet<[ParamType]> for typed snippet props$props()ClassValue type from svelte/elements for type-safe class props (Svelte 5.19+)<red_flags>
High Priority:
export let for props — use $props() instead$: reactive statements — use $derived or $effect<slot> or <slot name="x"> — use {#snippet} and {@render}createEventDispatcher — use callback props$effect to sync state — use $derived for computed values$state objects — breaks reactivity (values captured at destructure time)Medium Priority:
on:click directive — use onclick attribute$state.raw() for large API responses — unnecessary proxy overheadsetContext/getContext with string keys — use createContext for type safetyclass:name={condition} — use built-in class attribute object/array syntax (since 5.16)Gotchas:
$state proxies are not the original object — use $state.snapshot() to get a plain copy$state captures values, not references — access properties directly instead$derived return values are NOT deeply reactive — only $state creates deep proxies$effect runs after DOM update — use $effect.pre() for pre-update timingawait in $effect are not trackedsetContext in event handlers or $effectFor complete decision frameworks and the full anti-patterns list, see reference.md.
</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use Svelte 5 Runes syntax — NOT Svelte 4 patterns like export let, $:, or stores for component state)
(You MUST use $derived for computed values — NEVER use $effect to synchronize state)
(You MUST use snippets ({#snippet} / {@render}) instead of slots (<slot>))
(You MUST use callback props (onclick, onsomething) instead of createEventDispatcher)
(You MUST use $state.raw() for large objects/arrays that are replaced, not mutated)
(You MUST use createContext for type-safe context instead of raw setContext/getContext with string keys)
Failure to follow these rules will produce outdated Svelte 4 code that is deprecated and will break in future versions.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety