All articles
  • Svelte
  • Svelte 5
  • Reactivity

Svelte 5 – Introducing Runes

In 2019, Svelte 3 turned JavaScript into a reactive language. Svelte 5 changes all that with runes, which unlock universal, fine-grained reactivity — inside and beyond your components.

4 min read
Svelte 5 – Introducing Runes

In 2019, Svelte 3 turned JavaScript into a reactive language. Svelte is a web UI framework that uses a compiler to turn declarative component code like this…

svelte
clicks: {count}

…into tightly optimized JavaScript that updates the document when state like count changes. Because the compiler can ‘see’ where count is referenced, the generated code is highly efficient, and because they’re hijacking syntax like let and = instead of using cumbersome APIs, you can write less code.

A common piece of feedback they get is ‘I wish I could write all my JavaScript like this’. When you’re used to things inside components magically updating, going back to boring old procedural code feels like going from color to black-and-white.

Svelte 5 changes all that with runes, which unlock universal, fine-grained reactivity.

Introducing runes

Even though they’re changing how things work under the hood, Svelte 5 should be a drop-in replacement for almost everyone. The new features are opt-in — your existing components will continue to work.

They don’t yet have a release date for Svelte 5. What they’re showing you here is a work-in-progress that is likely to change!

What are runes?

Runes are symbols that influence the Svelte compiler. Whereas Svelte today uses let, =, the export keyword, and the $: label to mean specific things, runes use function syntax to achieve the same things and more.

For example, to declare a piece of reactive state, you can use the $state rune:

svelte
clicks: {count}

At first glance, this might seem like a step back — perhaps even un-Svelte-like. Isn’t it better if let count is reactive by default?

Well, no. The reality is that as applications grow in complexity, figuring out which values are reactive and which aren’t can get tricky. And the heuristic only works for let declarations at the top level of a component, which can cause confusion. Having code behave one way inside .svelte files and another inside .js can make it hard to refactor code, for example if you need to turn something into a store so that you can use it in multiple places.

Beyond components

With runes, reactivity extends beyond the boundaries of your .svelte files. Suppose you wanted to encapsulate your counter logic in a way that could be reused between components. Today, you would use a custom store in a .js or .ts file:

javascript
import { writable } from 'svelte/store';

export function createCounter() {
    const { subscribe, update } = writable(0);
    return {
        subscribe,
        increment: () => update((n) => n + 1)
    };
}

Because this implements the store contract — the returned value has a subscribe method — you can reference the store value by prefixing the store name with $:

svelte
clicks: {$counter}

This works, but it’s pretty weird! The store API can get rather unwieldy when you start doing more complex things.

With runes, things get much simpler:

javascript
export function createCounter() {
    let count = $state(0);
    return {
        get count() { return count },
        increment: () => count += 1
    };
}
svelte
clicks: {counter.count}

Note that they’re using a get property in the returned object so that counter.count always refers to the current value rather than the value at the time the function was called.

Runtime reactivity

Today, Svelte uses compile-time reactivity. This means that if you have some code that uses the $: label to re-run automatically when dependencies change, those dependencies are determined when Svelte compiles your component.

This works well… until it doesn’t. Suppose you refactored the code above:

javascript
const multiplyByHeight = (width) => width * height;
$: area = multiplyByHeight(width);

Because the $: area = … declaration can only ‘see’ width, it won’t be recalculated when height changes. As a result, code is hard to refactor, and understanding the intricacies of when Svelte chooses to update which values can become rather tricky beyond a certain level of complexity.

Svelte 5 introduces the $derived and $effect runes, which instead determine the dependencies of their expressions when they are evaluated:

As with $state, $derived and $effect can also be used in your .js and .ts files.

Signal boost

Like every other framework, they’ve come to the realization that Knockout was right all along. Svelte 5’s reactivity is powered by signals, which are essentially what Knockout was doing in 2010. More recently, signals have been popularized by Solid and adopted by a multitude of other frameworks.

They’re doing things a bit differently though. In Svelte 5, signals are an under-the-hood implementation detail rather than something you interact with directly. As such, they don’t have the same API design constraints, and can maximize both efficiency and ergonomics. For example, they avoid the type narrowing issues that arise when values are accessed by function call, and when compiling in server-side rendering mode they can ditch the signals altogether, since on the server they’re nothing but overhead.

Signals unlock fine-grained reactivity, meaning that (for example) changes to a value inside a large list needn’t invalidate all the other members of the list. As such, Svelte 5 is ridiculously fast.

Simpler times ahead

Runes are an additive feature, but they make a whole bunch of existing concepts obsolete:

  • the difference between let at the top level of a component and everywhere else
  • export let
  • $:, with all its attendant quirks
Tags:SvelteSvelte 5Reactivity

Work with us

Building something? Let’s talk.

Tell us what you’re working on — we’ll get back to you shortly.

Step 1 of 2