Svelte 5 changes how reactivity is declared. Instead of letting the compiler infer it from a top-level let or a $: label, you say what you mean with runes: small functions whose names start with a dollar sign. They are not imports. They are instructions to the compiler, and they work the same way in components and in plain .svelte.ts modules.

This post walks through the runes you will use every day, with short examples you can paste into a project and change.

Why runes exist

In Svelte 4, reactivity only existed inside components. A top-level let was reactive, a $: statement re-ran when something it used changed, and that was it. It was elegant for small components, but it had limits. You could not share reactive logic in a plain file, the order of $: statements was sometimes surprising, and it was hard to tell at a glance which variables were reactive and which were not.

Runes make this explicit. When you see $state, you know the value is reactive. When you see $derived, you know it is computed. The rules are the same everywhere, so moving logic out of a component into a shared file no longer means rewriting it.

State with $state

$state declares a reactive value. Reading it works like reading any variable, and assigning to it updates everything that depends on it.

svelte
<script lang="ts">
	let count = $state(0);
</script>

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

Objects and arrays are deeply reactive, which means you can mutate them directly instead of replacing them:

typescript
let todos = $state([{ text: "Write a post", done: false }]);

function add(text: string) {
	todos.push({ text, done: false });
}

Under the hood Svelte wraps these values in proxies, so a change to one property only updates the parts of the page that read that property. For large data you never mutate, such as a big list fetched from an API, $state.raw skips the proxy and is cheaper.

Derived values with $derived

When a value can be computed from other state, derive it instead of storing it.

typescript
let count = $state(0);
let doubled = $derived(count * 2);

doubled updates whenever count changes, and you cannot assign to it by accident. If the calculation needs more than one expression, use $derived.by with a function:

typescript
let items = $state([3, 1, 2]);

let sorted = $derived.by(() => {
	return [...items].sort((a, b) => a - b);
});

Side effects with $effect

Effects run after the DOM has updated, and they re-run when any state they read changes. Use them to talk to the outside world: timers, subscriptions, analytics, or code that touches the DOM directly.

typescript
let seconds = $state(0);

$effect(() => {
	const id = setInterval(() => seconds++, 1000);
	return () => clearInterval(id);
});

The function you return is the cleanup. It runs before the effect re-runs and when the component is destroyed, so you never leak a timer.

If you can compute it, derive it. Reach for an effect only when you need to reach outside your own state.

A common mistake is using $effect to copy one piece of state into another. That is exactly what $derived is for, and it avoids a whole class of update-order bugs.

Props with $props

Components receive data through $props, which replaces export let.

svelte
<script lang="ts">
	interface Props {
		title: string;
		count?: number;
	}

	let { title, count = 0 }: Props = $props();
</script>

<h2>{title} ({count})</h2>

Destructuring gives you defaults, renaming and rest props in one familiar syntax, and the interface gives you proper TypeScript checking without any extra tooling.

Sharing state between components

Because runes work in .svelte.ts files, shared state is just a module. One catch: you cannot export a reassigned $state primitive directly, because the compiler cannot track it across the module boundary. Expose it through an object with a getter instead.

typescript
// counter.svelte.ts
let count = $state(0);

export const counter = {
	get value() {
		return count;
	},
	increment() {
		count++;
	}
};

Any component that imports counter and reads counter.value updates when increment is called. This pattern is all you need for things like a cart, a theme setting or the current user.

A short migration checklist

If you are moving an existing project, work through it in this order:

  1. Replace export let with $props().
  2. Replace top-level reactive let declarations with $state.
  3. Replace $: derivations with $derived, and $: side effects with $effect.
  4. Replace on:click and friends with plain onclick attributes.
  5. Replace slots with snippets and the @render tag.

Svelte 5 can still run components written in the old syntax, so in most projects you can migrate one component at a time.

Runes can feel like extra typing at first. What you get in return is code that reads from top to bottom, behaves the same in every file and tells you where the reactivity is. After a day or two of using them, going back feels like reading code with half the labels removed.