Skip to main content
Ganesh Joshi
Back to Blogs

Svelte 5 and runes: a quick overview

February 15, 20265 min read
Tutorials
Svelte or frontend code on screen

Svelte 5 introduces runes—a new system for reactive state and effects. Instead of relying on implicit reactivity through assignment, you use explicit primitives like $state, $derived, and $effect. This makes reactivity clearer, more portable, and better integrated with TypeScript.

Why runes?

Svelte 4's reactivity was elegant but had limitations:

Svelte 4 limitation Runes solution
Reactivity only in .svelte files Works in .svelte.ts modules
let vs reactive unclear $state is explicit
$: dual purpose (derived + effect) Separate $derived and $effect
TypeScript inference issues Full type inference

Runes make Svelte's reactivity explicit without losing its simplicity.

$state

$state declares reactive state:

<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>
  Clicked {count} times
</button>

Compare to Svelte 4:

<!-- Svelte 4 -->
<script>
  let count = 0; // Implicitly reactive
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0); // Explicitly reactive
</script>

Objects and arrays

$state works with objects and arrays:

<script>
  let user = $state({
    name: 'Alice',
    age: 30,
  });

  let items = $state(['apple', 'banana']);
</script>

<button onclick={() => user.age++}>
  {user.name} is {user.age}
</button>

<button onclick={() => items.push('cherry')}>
  Items: {items.length}
</button>

Nested updates are reactive automatically.

Deep reactivity

By default, $state creates deep reactivity. For shallow reactivity:

<script>
  import { $state } from 'svelte';

  let data = $state.raw({ nested: { value: 1 } });
  // data.nested.value = 2 won't trigger updates
  // data = { nested: { value: 2 } } will
</script>

$derived

$derived creates computed values:

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

<p>{count} × 2 = {doubled}</p>
<p>{count} × 4 = {quadrupled}</p>

Compare to Svelte 4:

<!-- Svelte 4 -->
<script>
  let count = 0;
  $: doubled = count * 2;
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

Complex derivations

For multi-statement derivations:

<script>
  let items = $state([1, 2, 3, 4, 5]);
  let filter = $state(2);

  let filtered = $derived.by(() => {
    // Complex logic here
    const result = items.filter(item => item > filter);
    return result.sort((a, b) => a - b);
  });
</script>

$effect

$effect runs side effects when dependencies change:

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

  $effect(() => {
    console.log('Count changed to:', count);
    // Automatically tracks count as dependency
  });
</script>

Compare to Svelte 4:

<!-- Svelte 4 -->
<script>
  let count = 0;
  $: console.log('Count changed to:', count);
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  $effect(() => {
    console.log('Count changed to:', count);
  });
</script>

Cleanup

Return a cleanup function:

<script>
  let interval = $state(1000);

  $effect(() => {
    const id = setInterval(() => console.log('tick'), interval);
    return () => clearInterval(id);
  });
</script>

Pre-effects

Run before DOM updates:

<script>
  $effect.pre(() => {
    // Runs before DOM update
  });
</script>

$props

Declare component props:

<!-- Button.svelte -->
<script>
  let { label, onclick, disabled = false } = $props();
</script>

<button {onclick} {disabled}>
  {label}
</button>

With TypeScript:

<script lang="ts">
  interface Props {
    label: string;
    onclick: () => void;
    disabled?: boolean;
  }

  let { label, onclick, disabled = false }: Props = $props();
</script>

$bindable

Make props two-way bindable:

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

<input bind:value />

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

<Input bind:value={name} />

Shared state with runes

Runes work in .svelte.ts files, enabling shared state without stores:

// state.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);

  return {
    get count() { return count; },
    increment() { count++; },
    decrement() { count--; },
  };
}

// Usage in component
<script>
  import { createCounter } from './state.svelte.ts';

  const counter = createCounter(10);
</script>

<button onclick={counter.increment}>
  {counter.count}
</button>

Migration guide

Svelte 4 Svelte 5
let x = 0 let x = $state(0)
$: y = x * 2 let y = $derived(x * 2)
$: { console.log(x) } $effect(() => { console.log(x) })
export let prop let { prop } = $props()
writable(0) $state(0) in .svelte.ts

Gradual migration

Svelte 5 supports both syntaxes during migration:

<script>
  // Old syntax still works
  let oldCount = 0;

  // New syntax
  let newCount = $state(0);
</script>

Event handlers

Svelte 5 uses standard event attributes:

<!-- Svelte 4 -->
<button on:click={handleClick}>Click</button>

<!-- Svelte 5 -->
<button onclick={handleClick}>Click</button>

Snippets

Snippets replace slots for reusable template chunks:

<script>
  let { header, children } = $props();
</script>

{@render header()}
<main>
  {@render children()}
</main>

<!-- Usage -->
<Layout>
  {#snippet header()}
    <h1>Title</h1>
  {/snippet}

  <p>Main content</p>
</Layout>

TypeScript support

Runes have full TypeScript inference:

<script lang="ts">
  let count = $state(0); // Type: number
  let items = $state<string[]>([]); // Explicit generic

  let doubled = $derived(count * 2); // Type: number

  interface User {
    name: string;
    age: number;
  }

  let user = $state<User>({
    name: 'Alice',
    age: 30,
  });
</script>

Performance

Runes maintain Svelte's performance:

Aspect Impact
Bundle size Slightly smaller runtime
Reactivity Same fine-grained updates
Compilation Similar output

The main benefit is developer experience, not performance.

Summary

Svelte 5 runes make reactivity explicit with $state, $derived, and $effect. They work in regular .svelte.ts files, enabling shared reactive state without stores. Props use $props() with full TypeScript support. The old syntax works during migration. Runes improve clarity and TypeScript integration while maintaining Svelte's simplicity and performance.

Frequently Asked Questions

Runes are Svelte 5's new reactivity primitives. They include $state for reactive state, $derived for computed values, and $effect for side effects. They replace the implicit reactivity of Svelte 4.

Runes make reactivity explicit and work consistently inside and outside components. They improve TypeScript support, enable sharing reactive state across modules, and make the reactivity model more predictable.

Replace let declarations with $state(), reactive declarations ($:) with $derived() or $effect(), and stores with $state in shared modules. The old syntax still works during migration.

$derived creates a computed value that automatically updates when its dependencies change. Unlike $: reactive declarations, $derived is explicit about being a computed value and works anywhere.

Yes. Runes work in .svelte.ts and .svelte.js files, enabling shared reactive state without stores. This is a major improvement over Svelte 4 where reactivity only worked in components.

Related Posts