This skill covers Svelte 5's Runes system. Runes are the explicit reactive syntax introduced in Svelte 5, replacing Svelte 4's implicit let declarations and $: statements.
Capability Boundaries
✅ Strengths
- Complete reference for Svelte 5's explicit reactive system (Runes): $state / $derived / $effect / $props / $bindable / $inspect / $host
- Detailed subsections (Proxy behavior, dependency tracking, cleanup, pre/tracking/pending/root, etc.)
- Cross-module shared state patterns +
.svelte.jsfile conventions - Svelte 4 → Svelte 5 migration tips (implicit vs explicit reactivity)
- Complete Quick Fixes / Gotchas / FAQ quick reference
⚠️ Limitations
- This skill focuses on the Svelte 5 Runes system and does not cover Svelte 4 implicit reactivity (
let+$:) - Does not include in-depth content on animation / transitions / template syntax (use
svelte-template-syntax) - Does not include advanced usage of component lifecycle / stores / context (only mentioned where relevant to Svelte Runes)
❌ Out of Scope (scenarios where this skill should not be used)
- Svelte 4 implicit reactivity → Not applicable, use Svelte 4 documentation instead
- Template block syntax ({#if}/{#each}/{#await}/{#snippet}) → Use
svelte-template-syntax - transition/animate → Use
svelte-template-syntax - Stores(writable/derived) → Use the corresponding section of the Svelte 5 documentation (complementary to Runes)
Data Privacy
This skill does not collect, store, or transmit any user data. All code examples are for local development reference only.
Workflow
Step 1. Identify the Rune the user needs — $state / $derived / $effect / $props / $bindable / $inspect / $host
Step 2. Look up the corresponding section — SKILL.md gives an overview, in-depth details are in references/$X-deep.md
Step 3. Check examples when necessary — examples/$X-patterns.md provides copyable code
Step 4. Check Quick Fixes / Gotchas / FAQ — three quick reference tables covering common errors
Step 5. Svelte 4 migration scenarios — focus on the "Runes Overview" comparison section
When to use this skill
Use this skill when the user needs to understand or use $state、$derived、$effect、$props、$bindable、$inspect、$host and other runes, or needs to migrate Svelte 4 code to Svelte 5.
Critical: Runes Overview
Runes are symbols prefixed with $ , similar to function call syntax:
let message = $state('hello');
Key differences:
- No import needed — Runes are built-in language keywords
- Not values — cannot be assigned to variables or used as function arguments
- Position-sensitive — only valid in specific positions (the compiler will report an error)
| Rune | Purpose |
|---|---|
$state |
Create reactive state |
$derived |
Declare derived computed values |
$effect |
Declare side effects |
$props |
Declare component props |
$bindable |
Bindable prop |
$inspect |
Development debugging |
$host |
Custom element access |
Critical: $state
Create reactive state; the UI updates automatically when the state changes.
let count = $state(0);
let user = $state({ name: 'Ada', age: 30 });
Deep reactive proxy
$state Automatically creates a deep Proxy for arrays and plain objects, and property changes automatically trigger updates:
let todos = $state([
{ done: false, text: 'task' }
]);
todos[0].done = true; // ✅ 触发更新
todos.push({ done: false }); // ✅ 触发更新
$state.raw (avoid deep proxying)
Suitable for large arrays and scenarios that do not require deep changes:
let list = $state.raw([]);
// 只能重新赋值,不能 .push()
list = [...list, newItem]; // ✅
list.push(newItem); // ❌ 无效
$state.rawInternally it can still contain reactive state (e.g. proxy objects placed in a raw array).
$state.snapshot (static snapshot)
Get a read-only snapshot of a deep proxy (for passing to external APIs):
console.log($state.snapshot(proxyObject));
If the value has a
toJSON()method, snapshot will clonetoJSON()'s return value.
$state.eager (immediate UI update)
Used to immediately update the UI in await expressions:
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
</nav>
$state in class fields
class Counter {
count = $state(0); // 公共字段
#value = $state(0); // 私有字段
constructor(start = 0) {
this.value = $state(start); // constructor 中初始化
}
// 箭头函数字段 → this 自动绑定
reset = () => { this.count = 0; };
}
The compiler transforms
$statefields intoget/setmethods on the prototype, pointing to private fields.These properties are not enumerable. Note thethisbinding: passing a method directly to an event handler losesthis, use an arrow function field or inline() => todo.reset()。
Built-in reactive classes (svelte/reactivity)
Plain Map/Set/Date/URL is not reactive — use the reactive version instead:
import { SvelteSet, SvelteMap, SvelteDate, SvelteURL } from 'svelte/reactivity';
const tags = new SvelteSet(['a', 'b']);
const cache = new SvelteMap();
tags.add('c'); // ✅ 触发更新
cache.set('a', 1); // ✅ 触发更新
Destructuring pitfall
Reactivity is lost after destructuring (consistent with normal JS behavior):
let { name, age } = $state({ name: 'Ada', age: 30 });
name = 'Bob'; // ❌ 不会触发更新,原对象不变
Passing state across modules
.svelte.js/.svelte.ts Runes can be used in files, but you cannot directly export let reassign $state:
// ❌ 不可行:另一文件读到的会是 Signal 对象
export let count = $state(0);
// ✅ 方案1:不重新赋值整个对象
export const counter = $state({ count: 0 });
export function increment() { counter.count += 1; }
// ✅ 方案2:模块内私有 + 函数导出
let _count = $state(0);
export function getCount() { return _count; }
export function setCount(n) { _count = n; }
Passing state into functions — the getter pattern
JavaScript ispass-by-value,$state likewise. If a function needs to read the latest value, pass in a getter:
/** @param {() => number} getA @param {() => number} getB */
function add(getA, getB) {
return () => getA() + getB();
}
let a = $state(1);
let b = $state(2);
const total = add(() => a, () => b);
console.log(total()); // 3
a = 3; b = 4;
console.log(total()); // 7
You can also achieve "live reading" via proxy properties or get/set properties. Documentation: "Note that 'functions' is broad — it encompasses properties of proxies and get/set properties."
Critical: $derived
Declare derived values — read-only computed values based on existing state.
let count = $state(0);
let doubled = $derived(count * 2);
expressionmust be pure — it cannot modify
$stateinternally (the compiler reports an error).
$derived.by (complex derivation)
let total = $derived.by(() => {
let sum = 0;
for (const item of items) sum += item.price;
return sum;
});
Derived value override (Svelte 5.25+, optimistic UI)
let likes = $derived(post.likes);
async function onclick() {
likes += 1; // 即时乐观更新
try {
await like();
} catch {
likes -= 1; // 回滚
}
}
constderived is read-only; only withletcan it be reassigned. Documentation: "Prior to Svelte 5.25, deriveds were read-only."
Understanding dependency tracking
$derived All $state/$derived read synchronously inside are dependencies:
let a = Promise.resolve(1);
let b = 2;
let sum = $derived(await a + b);
// a 和 b 都是依赖(await 之后的同步读取也会追踪)
Onlythe expression itself's
awaitsynchronous reads after count as dependencies;calling a functioninsideawaitdoes not count.
Use untrack to exclude non-dependency values.
Push-pull reactivity
Derived valuesonly when readrecompute (pull), but when state changes theyimmediately notifyall dependencies (push). If the reference returned by the derived value is unchanged, downstream does not update:
let count = $state(0);
let large = $derived(count > 10); // 布尔
// 大文本节点只在 large 变化时重渲染,而非 count
Avoid
() => ({})、[...].map(...)inside derived values, which returns a new reference — it will cause downstream to always recompute.
Derived values do not destructure proxies
$derived does not wrap the return value in a Proxy — modifying properties of the object returned by the derived value affects the underlying $state:
let items = $state([...]);
let selected = $derived(items[0]);
selected.name = 'new'; // ✅ 影响 items[0].name
Derived destructuring
let { first, last } = $derived(user);
// 等价于:
// let first = $derived(user.first);
// let last = $derived(user.last);
Critical: $effect
Declare side effects — DOM operations, third-party library calls, network requests, etc.should notuse $effect to synchronize state.
$effect(() => {
document.title = `count: ${count}`;
return () => { /* 清理函数 */ };
});
Lifecycle
- after mountfirst execution
- when state changes microtask scheduling(batched)
- DOM updateafterexecute
- Browser only (SSR automatically skipped)
- Can be called anywhere in the component, as long as it's during the parent effect's run
Dependency tracking
Automatically tracks synchronous reads of $state/$derived :
$effect(() => {
// color 和 size 是依赖
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
});
Async not tracked
await after and reads inside setTimeout arenottracked:
$effect(() => {
ctx.fillStyle = color; // ✅ 追踪
setTimeout(() => {
ctx.fillRect(0, 0, size, size); // ❌ size 不追踪
}, 0);
});
Conditional branches determine dependencies
effect Only tracks state actually read during the last run:
$effect(() => {
if (condition) {
confetti({ colors: [color] });
} else {
confetti();
}
});
// condition=false 时 color 不再是依赖
$effect.pre (runs before DOM update)
$effect.pre(() => {
if (!div) return; // 挂载前跳过
messages.length; // 显式依赖追踪
// DOM 更新前执行(如滚动位置计算)
});
$effect.tracking (determine tracking context)
console.log($effect.tracking()); // false(组件初始化)
$effect(() => {
console.log($effect.tracking()); // true(effect 内)
});
Used for tools like
createSubscriber: only subscribe when state is tracked, not in event handlers.
$effect.pending() (number of pending Promises)
Returnsthe number of pending Promises in the current boundary(excluding child boundaries):
<script>
let a = $state(1);
let b = $state(2);
async function add(a, b) {
await new Promise(f => setTimeout(f, 500));
return a + b;
}
</script>
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
<p>{a} + {b} = {await add(a, b)}</p>
{#if $effect.pending()}
<span>pending: {$effect.pending()}</span>
{/if}
$effect.root() (independent scope)
Creates anon-trackingscope that is not automatically cleaned up when the component is destroyed:
import { flushSync } from 'svelte';
const destroy = $effect.root(() => {
let count = $state(0);
$effect(() => console.log(count));
return () => { /* cleanup */ };
});
count = 1;
flushSync(); // 立即跑 pending effects
// later...
destroy();
Applies to: testing tools, creating effects outside components, module-level effects. Documentation: "This rune also allows for the creation of effects outside of the component initialisation phase."
Cleanup function (Teardown)
$effect(() => {
const interval = setInterval(() => count += 1, 1000);
return () => clearInterval(interval); // 清理
});
teardown runs (a) before the effect re-runs, and (b) when the component is destroyed.
When not to use $effect
| Requirement | Correct approach |
|---|---|
| Derived computed value | $derived |
| Two-way sync | Function binding bind:value={() => v, setter} |
| Infinite loop | untrack wrapped read |
| await in template | Direct await |
Documentation: "In general,
$effectis best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently."
Critical: $props
Declare component properties (props):
let { name, age = 18, ...rest } = $props();
Type safety
<script lang="ts">
// 方式 1:内联注解
let { title }: { title: string } = $props();
// 方式 2:接口分离
interface Props { adjective: string; }
let { adjective }: Props = $props();
</script>
JSDoc style:
<script>
/** @type {{ adjective: string }} */
let { adjective } = $props();
</script>
Property types for native DOM elements are in
svelte/elements(such asHTMLButtonAttributes)。
prop default values
let { adjective = 'happy', count = 0 } = $props();
Default valuesare notreactive proxies — modifying their properties when the parent component doesn't pass them does not trigger updates. Documentation: "Fallback values are not turned into reactive state proxies."
prop renaming
let { super: hero = 'default' } = $props();
let { 'class': className, 'for': htmlFor } = $props();
Update props
Props automatically update when the parent component changes, but child componentsshould not directly modify a prop (except $bindable ):
// ❌ 不要修改
let { object } = $props();
object.count += 1; // 警告:ownership_invalid_mutation
// ✅ 用 callback props 或 $bindable
Child componentscan temporarily reassign a prop (unsaved state), without affecting the parent.
Rest Props
let { variant = 'primary', size = 'md', ...rest } = $props();
<button class="btn btn-{variant} btn-{size}" {...rest}>
<slot />
</button>
Type:
{ variant?: 'primary' | 'secondary'; [key: string]: any }orHTMLButtonAttributes。
$props.id()(Svelte 5.20+)
Generatesan ID unique to the current component instance, consistent between server/client during SSR:
<script>
const uid = $props.id();
</script>
<form>
<label for="{uid}-firstname">First Name:</label>
<input id="{uid}-firstname" type="text" />
</form>
Applies to:
for/aria-labelledby/aria-describedbyand other scenarios requiring ID pairing.
Generic components
<script lang="ts" generics="T extends { id: number }">
import type { Snippet } from 'svelte';
let {
items,
renderItem
}: {
items: T[];
renderItem: Snippet<[T]>;
} = $props();
</script>
{#each items as item (item.id)}
{@render renderItem(item)}
{/each}
Critical: $bindable
Allows a prop type where child components can modify parent component state — two-way data flow:
<!-- FancyInput.svelte -->
<script>
let { value = $bindable(), ...props } = $props();
</script>
<input bind:value={value} {...props} />
<!-- 父组件 -->
<script>
import FancyInput from './FancyInput.svelte';
let message = $state('hello');
</script>
<FancyInput bind:value={message} />
<p>{message}</p>
Usage
// 必须放在解构的默认值位置
let {
value = $bindable(), // 必填
other = $bindable('hello'), // 带 fallback
} = $props();
Two ways for the parent component to use it
<!-- 双向 -->
<FancyInput bind:value={message} />
<!-- 单向(父不监听子) -->
<FancyInput value={message} />
Documentation: "The parent component doesn't have to use
bind:— it can just pass a normal prop. Some parents don't want to listen to what their children have to say."
When to use
| Scenario | Recommended |
|---|---|
| Controlled form components | $bindable |
| Child-to-parent event notification | callback props |
| Cross-component sharing | Context / store |
Original documentation: "This isn't something you should do often — overuse can make your data flow unpredictable and your components harder to maintain."
Combining with function binding
<script>
let { value = $bindable(0), min = 0, max = 100 } = $props();
function clamp(v: number) { return Math.max(min, Math.min(max, v)); }
</script>
<input
type="range"
bind:value={() => value, (v) => (value = clamp(v))}
{min} {max}
/>
Critical: $inspect / $inspect.trace
Print state changes during development (noop in production):
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // 变化时自动 console.log
</script>
Deep tracking — modifying
user.nameorarr.push(x)both trigger. Stack trace is printed synchronously (except in playground).
$inspect(...).with() — custom handling
$inspect(count).with((type, count) => {
if (type === 'update') {
debugger; // 或 console.trace、埋点等
}
});
Signature:(type: 'init' | 'update', ...values) => void。
$inspect.trace()(Svelte 5.14+)
Makes the containing function tracked, and prints the state that triggered it when it re-runs:
<script>
$effect(() => {
// ⚠️ 必须是函数体的第一条语句
$inspect.trace();
doSomeWork();
});
</script>
Optional label:
$effect(() => {
$inspect.trace('resize-handler');
layout();
});
Documentation: "Any time the function re-runs as part of an effect or a derived, information will be printed to the console about which pieces of reactive state caused the effect to fire."
Critical: $host
Only used when compiled as acustom element, to access the host element:
<svelte:options customElement="my-stepper" />
<script>
function dispatch(type) {
$host().dispatchEvent(new CustomEvent(type));
}
</script>
<button onclick={() => dispatch('decrement')}>-</button>
<button onclick={() => dispatch('increment')}>+</button>
<!-- App.svelte -->
<script>
import './Stepper.svelte';
let count = $state(0);
</script>
<my-stepper
ondecrement={() => (count -= 1)}
onincrement={() => (count += 1)}
></my-stepper>
Typical use cases
- Dispatch custom events:
$host().dispatchEvent(new CustomEvent('x', { detail, bubbles: true, composed: true })) - Access host properties:
$host().getAttribute('foo')/setAttribute(...) - Call host methods:
$host().focus(), etc. - Expose an imperative API: use
export function
Custom element configuration
<svelte:options
customElement="my-tag"
customElementShadow="open" <!-- 或 "closed" / "none" -->
customElementEvents={['change']} <!-- 声明可冒泡事件 -->
customElementObservers={{ attr: ['data-foo'] }} <!-- 观察的 attribute -->
/>
Incorrect usage
<!-- ❌ 普通组件(非 custom element)中调用 -->
<script>
$host(); // 编译错误
</script>
<!-- ❌ 用 $host().querySelector 找内部元素 -->
<script>
$host().querySelector('.child'); // 改用 bind:this
</script>
Quick Fixes
| Problem | Solution |
|---|---|
| State changes don't update the UI | Confirm you used $state(not a plain let) |
$derived doesn't take effect |
Dependencies must be read synchronously (not after await ) |
$effect infinite loop |
Don't directly modify $stateinside it; use $derived |
in class methods, this is lost |
Use arrow function fields or inline functions |
| prop mutation warning | Use $bindable or callback props |
| Reactivity is lost after destructuring | Access properties of the original object instead of destructured variables |
| Map/Set modifications don't trigger updates | Switch to SvelteMap / SvelteSet |
Cross-file $state reads a Signal object |
Use a non-reassignable object + exported functions |
Gotchas
$stateis a deep proxy — reactivity is lost after destructuring; access properties of the original object$effectdoes not track asynchronous reads —await/setTimeoutreads after- In classes,
$effectfields — state read inside methods is not tracked as a dependency $effectshould not run during SSR — browser-only; automatically skipped during SSR$propsdefault values are not proxies — non-$bindableprop fallback values are not reactive objects- Derived returns a new reference — triggers downstream updates even if the content is the same
$inspectis a development tool — compiled to noop in production; cannot be used for business logic$hostis custom element only — using it in a regular component causes a compile error- Map/Set must use the reactive version — native APIs do not trigger updates
- Cross-file export
$state—export letreassigned$stateNot feasible
FAQ
Q: $derived and $state difference?
A: $state creates mutable state;$derived creates read-only derived values, automatically derived from dependencies. Available in 5.25+ let overrides derived implementation for optimistic UI.
Q: When to use $effect?
A: Only for side effects: DOM manipulation, third-party library calls, network requests. Derived values synchronizing state should never use $effect。
Q: $state.raw and regular $state difference?
A: $state.raw does not create deep proxies for arrays/objects, better performance, but can only be updated by reassignment.
Q: $props can it be destructured?
A: Yes, and it supports default values, renaming, rest destructuring, $bindable, $props.id().
Q: $bindable and function binding difference?
A: $bindable declares a prop as two-way; function binding bind:value={() => v, setter} is custom read/write logic. The two can be combined.
Q: When to use $bindable vs callback props?
A: Controlled forms use $bindable; event notifications use callback props. Avoid overusing $bindable causing data flow confusion.
Q: $inspect does it run in production?
A: No, production builds compile to noop.
Q: $effect.pending() what is it used for?
A: Displays the number of pending Promises in the current boundary, can be used for global loading state.
Q: $effect.root() when to use?
A: Testing tools, creating effects outside components, module-level effects. Need to manually call the returned destroy() to clean up.
Q: For cross-module shared state use .svelte.js or Context?
A: For true global singletons use .svelte.js; for request-level isolation/component tree sharing prefer Context.
Examples
For executable code examples, see examples/ directory:
| File | Content |
|---|---|
state-patterns.md |
$state basics, deep proxies,$state.raw/snapshot/eager, class fields, built-in reactive classes, cross-module |
derived-patterns.md |
$derived basics,$derived.by, optimistic UI, destructured derived, update propagation, async |
effect-patterns.md |
$effect basics, cleanup,$effect.pre/tracking/pending/root, taboos, untrack |
props-patterns.md |
$props destructuring, type safety, Rest Props,$bindable、$props.id(), generic components |
bindable-patterns.md |
$bindable all usages: two-way binding, fallback, function binding, array sharing |
inspect-patterns.md |
$inspect/$inspect.trace/$inspect.with debug mode |
References
For in-depth technical reference, see references/ directory:
| File | Content |
|---|---|
runes-overview.md |
Runes overall architecture, vs Legacy comparison, lifecycle |
$state-deep.md |
Proxy behavior,$state.raw/snapshot/eager, in classes $state, built-in reactive classes, cross-module |
$derived-deep.md |
expression vs $derived.by, dependency tracking, writable derived, Push-pull, destructuring |
$effect-deep.md |
pre/tracking/cleanup/pending/root, tracking rules, common mistakes |
$props-deep.md |
destructuring, Rest Props, Type safety,$props.id(), generics |
$bindable-deep.md |
complete $bindable reference: two-way binding, fallback, function binding composition |
$inspect-deep.md |
$inspect、$inspect.with、$inspect.trace complete reference |
$host-deep.md |
custom element $host usage,<svelte:options> configuration, typical patterns |
context-deep.md |
createContext vs setContext, type safety, SSR |
Reference Library
In-depth technical reference (loaded on demand):
- references/runes-overview.md — Runes overall architecture, vs Legacy comparison, lifecycle
- references/$state-deep.md — Proxy behavior,
$state.raw/snapshot/eager, in classes$state, built-in reactive classes, cross-module - references/$derived-deep.md — expressions vs
$derived.by, dependency tracking, writable derived, Push-pull, destructuring - references/$effect-deep.md — pre/tracking/cleanup/pending/root, tracking rules, common mistakes
- references/$props-deep.md — destructuring, Rest Props, Type safety,
$props.id(), generics - references/$bindable-deep.md — complete
$bindablereference: two-way binding, fallback, function binding composition - references/$inspect-deep.md —
$inspect、$inspect.with、$inspect.tracecomplete reference - references/$host-deep.md — custom element
$hostusage,<svelte:options>configuration, typical patterns - references/context-deep.md — createContext vs setContext, type safety, SSR