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.
<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:
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.
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:
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.
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.
<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.
// 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:
- Replace
export letwith$props(). - Replace top-level reactive
letdeclarations with$state. - Replace
$:derivations with$derived, and$:side effects with$effect. - Replace
on:clickand friends with plainonclickattributes. - Replace slots with snippets and the
@rendertag.
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.
