---
title: 'Svelte 5 Runes Explained: $state, $derived, $effect (2026)'
description: 'Svelte 5 runes explained with runnable examples: when to use $state, $derived and $effect, when not to use $effect, and how to migrate from Svelte 4 stores.'
url: https://nerdleveltech.com/svelte-5-runes-explained
language: en
date: '2026-06-19'
category: frontend
tags:
  - Svelte
  - Svelte 5
  - runes
  - $state
  - $derived
  - $effect
  - reactivity
  - frontend
markdown_url: https://nerdleveltech.com/svelte-5-runes-explained.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/svelte-5-runes-explained'
terms: https://nerdleveltech.com/llms.txt
---

# Svelte 5 Runes Explained: $state, $derived, $effect (2026)

Runes are Svelte 5's explicit reactivity primitives, written with a `$` prefix. Use `$state` to declare reactive values, `$derived` to compute values from other state, and `$effect` only for side effects like network calls or DOM work. Reach for `$derived` before `$effect`.

## TL;DR

Svelte 5 (stable since October 19, 2024; 5.56.x as of June 2026) replaced the compiler's implicit reactivity with **runes** — function-like symbols such as `$state`, `$derived`, and `$effect`.[^alive][^june] The mental shift is simple: `$state` holds values, `$derived` computes values, and `$effect` runs side effects. A common mistake is using `$effect` to sync one value to another — the docs explicitly advise against it; use `$derived` instead.[^effect] Svelte 4 stores still work, so migration can be gradual.[^runes]

## What you'll learn

- What runes are and why Svelte 5 introduced them
- How `$state` works, including deep reactivity, `$state.raw`, and `$state.snapshot`
- How `$derived` and `$derived.by` replace `$:` computed values
- What `$effect` is for — and the cases where you should *not* use it
- A decision rule for `$derived` vs `$effect`
- How `$props` and `$bindable` replace `export let`
- Whether Svelte stores still work, and how to migrate Svelte 4 reactivity to runes

## What are runes in Svelte 5?

Runes are special symbols, prefixed with `$`, that tell the Svelte compiler how reactivity works. They look like function calls but are compiler keywords — you don't import them, and they only have meaning inside `.svelte`, `.svelte.js`, and `.svelte.ts` files.[^runes] Svelte 5 ships seven runes: `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `$inspect`, and `$host`.[^state]

Before runes, Svelte 4 made every top-level `let` in a component reactive and used the `$:` label for computed values. That was concise but magical: reactivity depended on where code lived. Runes make reactivity explicit and portable — the same `$state` works inside a component or inside a plain `.svelte.js` module — which is why you can now keep shared reactive logic outside components entirely.[^runes]

```svelte
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

<button onclick={() => count++}>
  clicked {count} times — doubled is {doubled}
</button>
```

## How does $state work in Svelte 5?

`$state` declares a reactive value: when it changes, the UI that reads it updates. It is the direct replacement for a plain reactive `let` from Svelte 4.[^state]

```svelte
<script>
  let count = $state(0);
  let user = $state({ name: 'Ada', tags: ['admin'] });
</script>

<button onclick={() => count++}>{count}</button>
<button onclick={() => user.tags.push('editor')}>add tag</button>
```

The important detail: objects and arrays passed to `$state` become **deeply reactive proxies**. Mutating a nested property — `user.tags.push(...)` above — triggers updates, with no reassignment needed.[^derived] That is a real change from Svelte 4, where you had to reassign (`user = user`) to force an update.

Three `$state` variants handle edge cases:

- **`$state.raw`** creates *shallow* state. It is not made deeply reactive and only updates when you **replace** it (reassign the whole value), not when you mutate it. This avoids the cost of proxying large objects or arrays you never mutate.[^stateraw]
- **`$state.snapshot`** returns a detached, plain (non-proxy) copy of a deep `$state` object. Mutating the snapshot does not affect the original, and reading it does not register a dependency — useful when passing data to a non-Svelte library that chokes on proxies.[^stateraw]
- **`$state.eager`** forces the UI to update immediately when the state changes, even when that state is read inside an `await` expression (which otherwise defers the UI update until the async work resolves).[^stateraw]

You can also mark class fields as `$state`, which is how you build reusable reactive classes.[^derived]

## How does $derived work? ($derived vs $:)

`$derived` declares a value computed from other reactive state, recomputed automatically when its dependencies change. It replaces Svelte 4's `$:` computed declarations.[^derived]

```svelte
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>
```

The expression inside `$derived(...)` **must be free of side effects** — Svelte disallows state changes like `count++` inside a derived.[^derived] For logic that doesn't fit in one expression, use `$derived.by`, which takes a function:

```svelte
<script>
  let numbers = $state([1, 2, 3]);
  let total = $derived.by(() => {
    let sum = 0;
    for (const n of numbers) sum += n;
    return sum;
  });
</script>
```

`$derived(expr)` is exactly equivalent to `$derived.by(() => expr)`.[^derived] Three behaviors are worth knowing because they explain why deriveds are cheap:

- **Lazy and memoized.** Svelte uses *push-pull* reactivity: when state changes, dependents are notified (push), but a derived is not recalculated until it is actually read (pull). A derived that is never accessed never runs.[^derived]
- **Referential short-circuiting.** If a recomputed derived is referentially identical to its previous value, downstream updates are skipped.[^derived]
- **Overridable.** Since Svelte 5.25, you can temporarily reassign a derived (unless it's `const`) — handy for optimistic UI where you show an immediate change before the server confirms it.[^derived]

## When should you use $effect — and when not to?

`$effect` runs a function when its reactive dependencies change. It is for **side effects**: calling third-party libraries, drawing on a `<canvas>`, making network requests, or analytics. Effects run in the **browser only** — never during server-side rendering.[^effect]

```svelte
<script>
  let theme = $state('dark');

  $effect(() => {
    document.documentElement.dataset.theme = theme;
  });
</script>
```

Key behaviors from the docs:[^effect]

- Effects run **after** the component mounts to the DOM, then in a **microtask** after state changes. Re-runs are batched and happen after DOM updates apply.
- An effect tracks reactive values (`$state`, `$derived`, `$props`) read **synchronously** in its body. Values read **asynchronously** — after `await` or inside `setTimeout` — are *not* tracked.
- An effect can return a **teardown** function, which runs immediately before the effect re-runs and when the component is destroyed (perfect for `clearInterval`):

```svelte
<script>
  let ms = $state(1000);
  let count = $state(0);

  $effect(() => {
    const id = setInterval(() => count++, ms);
    return () => clearInterval(id); // teardown
  });
</script>
```

Now the part worth emphasizing. The official guidance is that `$effect` is **an escape hatch**, not a default tool. In particular, **do not use it to synchronise state**.[^effect] This is the anti-pattern:

```svelte
<script>
  let count = $state(0);
  let doubled = $state();

  // ❌ don't do this
  $effect(() => {
    doubled = count * 2;
  });
</script>
```

Replace it with a derived, which is simpler, lazier, and avoids extra render passes:

```svelte
<script>
  let count = $state(0);
  let doubled = $derived(count * 2); // ✅ do this
</script>
```

If you ever *must* write to `$state` inside an effect and hit an infinite loop because you read and write the same state, wrap the read in `untrack` (imported from `svelte`) so it isn't registered as a dependency.[^effect] For linking two inputs together, prefer function bindings or `oninput` callbacks over effects.[^effect]

Svelte also exposes advanced sub-runes: `$effect.pre` (runs *before* the DOM updates, e.g. autoscroll via `tick()`), `$effect.tracking()` (whether you're in a tracking context), `$effect.pending()` (count of pending promises when using `await` in components — part of the experimental async feature you opt into with `experimental.async`), and `$effect.root()` (a manually managed, non-auto-cleaning effect scope).[^effect]

## $derived vs $effect: which rune should you use?

Use this decision rule: **if you are producing a value, use `$derived`; if you are performing an action, use `$effect`.** The table below maps common tasks.

| You want to… | Use | Why |
|---|---|---|
| Compute a value from state | `$derived` | Pure, lazy, memoized; recomputed only when read |
| Multi-statement computation | `$derived.by` | Same as `$derived` but with a function body |
| Mirror one state value into another | `$derived` | Never `$effect` — the docs single this out as an anti-pattern[^effect] |
| Call an API / log / touch the DOM | `$effect` | True side effect, browser-only |
| Set up and tear down a subscription/timer | `$effect` + teardown | Return a cleanup function |
| Run code before the DOM updates | `$effect.pre` | Runs before DOM updates |

A simple gut check: if your effect body ends in an assignment to another `$state`, you almost certainly want `$derived` instead.

## How do $props and $bindable work?

`$props` reads a component's inputs via destructuring, with default values, replacing Svelte 4's `export let`:[^props]

```svelte
<!-- Greeting.svelte -->
<script>
  let { name = 'world', count = 0 } = $props();
</script>

<p>Hello {name} ({count})</p>
```

By default props flow one way (parent to child). To allow two-way binding, mark a prop `$bindable`:[^bindable]

```svelte
<!-- TextInput.svelte -->
<script>
  let { value = $bindable('') } = $props();
</script>

<input bind:value />
```

```svelte
<!-- Parent.svelte -->
<script>
  import TextInput from './TextInput.svelte';
  let name = $state('');
</script>

<TextInput bind:value={name} />
<p>{name}</p>
```

## Do Svelte stores still work in Svelte 5?

Yes. Stores from `svelte/store` — `writable`, `readable`, `derived`, and the `$store` auto-subscription syntax — still work in Svelte 5 and are **not deprecated**.[^runes] The official recommendation is to prefer runes for most component and shared state, but stores remain useful, especially for existing code and for cases like RxJS-style external subscriptions. Because both coexist, you never need a big-bang rewrite.

A common pattern for shared reactive state in Svelte 5 is to move it into a `.svelte.js` module using `$state`, then export accessors:

```js
// counter.svelte.js
let count = $state(0);

export function getCount() {
  return count;
}
export function increment() {
  count++;
}
```

This works because runes are valid outside components in `.svelte.js`/`.svelte.ts` files.[^runes]

## How do you migrate Svelte 4 reactivity to runes?

Migration is mechanical, and Svelte ships an automated migration script, but the core mappings are:[^runes]

- Reactive `let x = 0` → `let x = $state(0)`
- `$: doubled = x * 2` (computed) → `let doubled = $derived(x * 2)`
- `$: { sideEffect(x); }` (side effect) → `$effect(() => { sideEffect(x); })`
- `export let name` → `let { name } = $props()`
- `export let value` with `bind:` → `let { value = $bindable() } = $props()`

The one judgment call is the old `$:` label, which Svelte 4 used for *both* computed values and side effects. Split it: anything that produces a value becomes `$derived`; anything that performs an action becomes `$effect`. When in doubt, choose `$derived` — it's the cheaper, safer default.

## FAQ

**What are runes in Svelte 5?**
Runes are `$`-prefixed compiler symbols (`$state`, `$derived`, `$effect`, `$props`, `$bindable`, `$inspect`, `$host`) that declare reactivity explicitly. They aren't imported and only work in `.svelte`, `.svelte.js`, and `.svelte.ts` files.[^runes][^state]

**What's the difference between $state and a normal `let`?**
A plain `let` is a one-time value; `$state` is tracked, so the UI updates when it changes. Objects and arrays in `$state` become deeply reactive proxies, so mutating nested properties triggers updates.[^derived]

**When should I use $effect vs $derived?**
Use `$derived` to compute a value from other state, and `$effect` only for side effects (API calls, DOM, analytics). Never use `$effect` just to copy one state value into another.[^effect]

**When should you NOT use $effect?**
Don't use it to synchronise state, mirror one value into another, or link two inputs. Use `$derived` for values and function bindings for linked inputs. Effects are an escape hatch for genuine side effects.[^effect]

**What is $derived.by?**
`$derived.by` takes a function for computations too complex for a single expression; `$derived(expr)` equals `$derived.by(() => expr)`.[^derived]

**Do Svelte stores still work in Svelte 5?**
Yes — `svelte/store` and `$store` syntax still work and aren't deprecated, though runes are recommended for most new component state.[^runes]

**Can I use runes outside components?**
Yes. Runes work in `.svelte.js` and `.svelte.ts` modules, which is the idiomatic way to share reactive state across components in Svelte 5.[^runes]

## Bottom line and next steps

Svelte 5 runes turn reactivity from compiler magic into three explicit tools: `$state` for values, `$derived` for computed values, and `$effect` for side effects — with `$props`/`$bindable` for component inputs.[^state][^props] A simple rule that avoids a whole class of bugs is to reach for `$derived` before `$effect`, and to treat `$effect` as an escape hatch for things like network requests and DOM work rather than a way to keep state in sync.[^effect]

If you're new to the framework, start with the fundamentals in our [guide to building fast apps with Svelte](https://nerdleveltech.com/learn-svelte-the-modern-guide-to-building-lightningfast-web-apps), then put runes to work in a real feature with [type-safe forms using Astro Actions](https://nerdleveltech.com/astro-actions-type-safe-forms-tutorial). For broader frontend state and styling patterns, [Tailwind v4 dark mode](https://nerdleveltech.com/tailwind-v4-dark-mode) pairs well with a `$state`-driven theme toggle like the `$effect` example above.

[^alive]: Svelte — "Svelte 5 is alive" (stable release, October 19, 2024). https://svelte.dev/blog/svelte-5-is-alive
[^june]: Svelte — "What's new in Svelte: June 2026" (current 5.56.x line; language-tools TypeScript 6.0 support). https://svelte.dev/blog/whats-new-in-svelte-june-2026
[^runes]: Svelte Docs — "What are runes?" and migration/stores references. https://svelte.dev/docs/svelte/what-are-runes
[^state]: Svelte Docs — "$state" (rune list in left navigation; reactive state). https://svelte.dev/docs/svelte/$state
[^stateraw]: Svelte Docs — "$state" ($state.raw shallow state; $state.snapshot detached copy). https://svelte.dev/docs/svelte/$state
[^derived]: Svelte Docs — "$derived" (derived state, $derived.by, push-pull reactivity, overriding since 5.25, deep-proxy note). https://svelte.dev/docs/svelte/$derived
[^effect]: Svelte Docs — "$effect" (lifecycle, dependency tracking, teardown, $effect.pre/tracking/pending/root, "When not to use $effect", untrack). https://svelte.dev/docs/svelte/$effect
[^props]: Svelte Docs — "$props" (component props via destructuring with defaults). https://svelte.dev/docs/svelte/$props
[^bindable]: Svelte Docs — "$bindable" (two-way bindable props). https://svelte.dev/docs/svelte/$bindable
