SuperAPI AI Directory

svelte-runes

Frontend & websites

View repository

Updated 10-16 06:47

About this skill

Svelte Runes Reference (Svelte 5)

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

  1. Complete reference for Svelte 5's explicit reactive system (Runes): $state / $derived / $effect / $props / $bindable / $inspect / $host
  2. Detailed subsections (Proxy behavior, dependency tracking, cleanup, pre/tracking/pending/root, etc.)
  3. Cross-module shared state patterns + .svelte.js file conventions
  4. Svelte 4 → Svelte 5 migration tips (implicit vs explicit reactivity)
  5. Complete Quick Fixes / Gotchas / FAQ quick reference

⚠️ Limitations

  1. This skill focuses on the Svelte 5 Runes system and does not cover Svelte 4 implicit reactivity (let + $:)
  2. Does not include in-depth content on animation / transitions / template syntax (use svelte-template-syntax)
  3. 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)

  1. Svelte 4 implicit reactivity → Not applicable, use Svelte 4 documentation instead
  2. Template block syntax ({#if}/{#each}/{#await}/{#snippet}) → Use svelte-template-syntax
  3. transition/animate → Use svelte-template-syntax
  4. 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.raw Internally 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 clone toJSON() '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 $state fields into get/set methods on the prototype, pointing to private fields.These properties are not enumerable. Note the this binding: passing a method directly to an event handler loses this, 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; // 回滚
  }
}

const derived is read-only; only with let can 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 await synchronous reads after count as dependencies;calling a functioninside await does 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, $effect is 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 as HTMLButtonAttributes)。

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 } or HTMLButtonAttributes。

$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-describedby and 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.name or arr.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

  1. $state is a deep proxy — reactivity is lost after destructuring; access properties of the original object
  2. $effect does not track asynchronous reads — await/setTimeout reads after
  3. In classes, $effect fields — state read inside methods is not tracked as a dependency
  4. $effect should not run during SSR — browser-only; automatically skipped during SSR
  5. $props default values are not proxies — non- $bindable prop fallback values are not reactive objects
  6. Derived returns a new reference — triggers downstream updates even if the content is the same
  7. $inspect is a development tool — compiled to noop in production; cannot be used for business logic
  8. $host is custom element only — using it in a regular component causes a compile error
  9. Map/Set must use the reactive version — native APIs do not trigger updates
  10. Cross-file export $state — export let reassigned $state Not 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):