Svelte 5 introduces runes, a powerful set of primitives for controlling reactivity inside your Svelte components and — for the first time — inside .svelte.js
and .svelte.ts
modules.
Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language.
When you opt in to runes mode, the non-runes features listed in the 'What this replaces' sections are no longer available.
Check out the Introducing runes blog post before diving into the docs!
$statepermalink
Reactive state is declared with the $state
rune:
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
clicks: {count}
</button>
You can also use $state
in class fields (whether public or private):
ts
classTodo {done =$state (false);text =$state ();constructor(text ) {this.text =text ;}}
In this example, the compiler transforms
done
andtext
intoget
/set
methods on the class prototype referencing private fields
Only plain objects and arrays are made deeply reactive by wrapping them with Proxies
:
<script>
let numbers = $state([1, 2, 3]);
</script>
<button onclick={() => numbers.push(numbers.length + 1)}>
push
</button>
<button onclick={() => numbers.pop()}> pop </button>
<p>
{numbers.join(' + ') || 0}
=
{numbers.reduce((a, b) => a + b, 0)}
</p>
What this replacespermalink
In non-runes mode, a let
declaration is treated as reactive state if it is updated at some point. Unlike $state(...)
, which works anywhere in your app, let
only behaves this way at the top level of a component.
$state.frozenpermalink
State declared with $state.frozen
cannot be mutated; it can only be reassigned. In other words, rather than assigning to a property of an object, or using an array method like push
, replace the object or array altogether if you'd like to update it:
<script>
let numbers = $state([1, 2, 3]);
let numbers = $state.frozen([1, 2, 3]);
</script>
<button onclick={() => numbers.push(numbers.length + 1)}>
<button onclick={() => numbers = [...numbers, numbers.length + 1]}>
push
</button>
<button onclick={() => numbers.pop()}> pop </button>
<button onclick={() => numbers = numbers.slice(0, -1)}> pop </button>
<p>
{numbers.join(' + ') || 0}
=
{numbers.reduce((a, b) => a + b, 0)}
</p>
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that frozen state can contain reactive state (for example, a frozen array of reactive objects).
Objects and arrays passed to
$state.frozen
will be shallowly frozen usingObject.freeze()
. If you don't want this, pass in a clone of the object or array instead.
$state.snapshotpermalink
To take a static snapshot of a deeply reactive $state
proxy, use $state.snapshot
:
<script>
let counter = $state({ count: 0 });
function onclick() {
// Will log `{ count: ... }` rather than `Proxy { ... }`
console.log($state.snapshot(counter));
}
</script>
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as structuredClone
.
Note that
$state.snapshot
will clone the data when removing reactivity. If the value passed isn't a$state
proxy, it will be returned as-is.
$state.ispermalink
Sometimes you might need to compare two values, one of which is a reactive $state(...)
proxy. For this you can use $state.is(a, b)
:
<script>
let foo = $state({});
let bar = {};
foo.bar = bar;
console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
console.log($state.is(foo.bar, bar)); // true
</script>
This is handy when you might want to check if the object exists within a deeply reactive object/array.
$state.is
usesObject.is
to check if two values are the same value.
$derivedpermalink
Derived state is declared with the $derived
rune:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<button onclick={() => count++}>
{doubled}
</button>
<p>{count} doubled is {doubled}</p>
The expression inside $derived(...)
should be free of side-effects. Svelte will disallow state changes (e.g. count++
) inside derived expressions.
As with $state
, you can mark class fields as $derived
.
What this replacespermalink
If the value of a reactive variable is being computed it should be replaced with $derived
whether it previously took the form of $: double = count * 2
or $: { double = count * 2; }
There are some important differences to be aware of:
- With the
$derived
rune, the value ofdouble
is always current (for example if you updatecount
then immediatelyconsole.log(double)
). With$:
declarations, values are not updated until right before Svelte updates the DOM - In non-runes mode, Svelte determines the dependencies of
double
by statically analysing thecount * 2
expression. If you refactor it...
...that dependency information is lost, andtsconstdoubleCount = () =>count * 2;$:double =doubleCount ();double
will no longer update whencount
changes. With runes, dependencies are instead tracked at runtime. - In non-runes mode, reactive statements are ordered topologically, meaning that in a case like this...
...ts$:triple =double +count ;$:double =count * 2;double
will be calculated first despite the source order. In runes mode,triple
cannot referencedouble
before it has been declared.
$derived.bypermalink
Sometimes you need to create complex derivations that don't fit inside a short expression. In these cases, you can use $derived.by
which accepts a function as its argument.
<script>
let numbers = $state([1, 2, 3]);
let total = $derived.by(() => {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
});
</script>
<button onclick={() => numbers.push(numbers.length + 1)}>
{numbers.join(' + ')} = {total}
</button>
In essence, $derived(expression)
is equivalent to $derived.by(() => expression)
.
$effectpermalink
To run side-effects when the component is mounted to the DOM, and when values change, we can use the $effect
rune (demo):
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// this will re-run whenever `color` or `size` change
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
The function passed to $effect
will run when the component mounts, and will re-run after any changes to the values it reads that were declared with $state
or $derived
(including those passed in with $props
). Re-runs are batched (i.e. changing color
and size
in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
Values that are read asynchronously — after an await
or inside a setTimeout
, for example — will not be tracked. Here, the canvas will be repainted when color
changes, but not when size
changes (demo):
ts
$effect (() => {constcontext =canvas .getContext ('2d');context .clearRect (0, 0,canvas .width ,canvas .height );// this will re-run whenever `color` changes...context .fillStyle =color ;setTimeout (() => {// ...but not when `size` changescontext .fillRect (0, 0,size ,size );}, 0);});
An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes inside an object at dev time, you can use $inspect
.)
<script>
let state = $state({ value: 0 });
let derived = $derived({ value: state.value * 2 });
// this will run once, because `state` is never reassigned (only mutated)
$effect(() => {
state;
});
// this will run whenever `state.value` changes...
$effect(() => {
state.value;
});
// ...and so will this, because `derived` is a new object each time
$effect(() => {
derived;
});
</script>
<button onclick={() => (state.value += 1)}>
{state.value}
</button>
<p>{state.value} doubled is {derived.value}</p>
You can return a function from $effect
, which will run immediately before the effect re-runs, and before it is destroyed (demo).
<script>
let count = $state(0);
let milliseconds = $state(1000);
$effect(() => {
// This will be recreated whenever `milliseconds` changes
const interval = setInterval(() => {
count += 1;
}, milliseconds);
return () => {
// if a callback is provided, it will run
// a) immediately before the effect re-runs
// b) when the component is destroyed
clearInterval(interval);
};
});
</script>
<h1>{count}</h1>
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>
When not to use $effectpermalink
In general, $effect
is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this...
<script>
let count = $state(0);
let doubled = $state();
// don't do this!
$effect(() => {
doubled = count * 2;
});
</script>
...do this:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
For things that are more complicated than a simple expression like
count * 2
, you can also use$derived.by
.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this (demo):
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
$effect(() => {
left = total - spent;
});
$effect(() => {
spent = total - left;
});
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
Instead, use callbacks where possible (demo):
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(e) {
spent = +e.target.value;
left = total - spent;
}
function updateLeft(e) {
left = +e.target.value;
spent = total - left;
}
</script>
<label>
<input
type="range"
value={spent}
oninput={updateSpent}
max={total}
/>
{spent}/{total} spent
</label>
<label>
<input
type="range"
value={left}
oninput={updateLeft}
max={total}
/>
{left}/{total} left
</label>
If you need to use bindings, for whatever reason (for example when you want some kind of "writable $derived
"), consider using getters and setters to synchronise state (demo):
<script>
let total = 100;
let spent = $state(0);
let left = {
get value() {
return total - spent;
},
set value(v) {
spent = total - v;
}
};
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left.value} max={total} />
{left.value}/{total} left
</label>
If you absolutely have to update $state
within an effect and run into an infinite loop because you read and write to the same $state
, use untrack.
What this replacespermalink
The portions of $: {}
that are triggering side-effects can be replaced with $effect
while being careful to migrate updates of reactive variables to use $derived
. There are some important differences:
- Effects only run in the browser, not during server-side rendering
- They run after the DOM has been updated, whereas
$:
statements run immediately before - You can return a cleanup function that will be called whenever the effect refires
Additionally, you may prefer to use effects in some places where you previously used onMount
and afterUpdate
(the latter of which will be deprecated in Svelte 5). There are some differences between these APIs as $effect
should not be used to compute reactive values and will be triggered each time a referenced reactive variable changes (unless using untrack
).
$effect.prepermalink
In rare cases, you may need to run code before the DOM updates. For this we can use the $effect.pre
rune:
<script>
import { tick } from 'svelte';
let div;
let messages = [];
// ...
$effect.pre(() => {
if (!div) return; // not yet mounted
// reference `messages` so that this code re-runs whenever it changes
messages;
// autoscroll when new messages are added
if (
div.offsetHeight + div.scrollTop >
div.scrollHeight - 20
) {
tick().then(() => {
div.scrollTo(0, div.scrollHeight);
});
}
});
</script>
<div bind:this={div}>
{#each messages as message}
<p>{message}</p>
{/each}
</div>
Apart from the timing, $effect.pre
works exactly like $effect
— refer to its documentation for more info.
What this replacespermalink
Previously, you would have used beforeUpdate
, which — like afterUpdate
— is deprecated in Svelte 5.
$effect.trackingpermalink
The $effect.tracking
rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template (demo):
<script>
console.log('in component setup:', $effect.tracking()); // false
$effect(() => {
console.log('in effect:', $effect.tracking()); // true
});
</script>
<p>in template: {$effect.tracking()}</p> <!-- true -->
This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
$effect.rootpermalink
The $effect.root
rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase.
<script>
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
console.log(count);
});
return () => {
console.log('effect root cleanup');
};
});
</script>
$propspermalink
To declare component props, use the $props
rune:
ts
let {optionalProp = 42,requiredProp } =$props ();
You can use familiar destructuring syntax to rename props, in cases where you need to (for example) use a reserved word like catch
in <MyComponent catch={22} />
:
ts
let {catch :theCatch } =$props ();
To get all properties, use rest syntax:
ts
let {a ,b ,c , ...everythingElse } =$props ();
You can also use an identifier:
ts
letprops =$props ();
If you're using TypeScript, you can declare the prop types:
ts
let { a, b, c, ...everythingElse }: MyProps = $props();
In an earlier preview,
$props()
took a type argument. This caused bugs, since in a case like this...ts
let {x = 42 } =$props <{x : string }>();...TypeScript widens the type of
x
to bestring | number
, instead of erroring.
If you're using JavaScript, you can declare the prop types using JSDoc:
ts
/** @type {{ x: string }} */let {x } =$props ();// or use @typedef if you want to document the properties:/*** @typedef {Object} MyProps* @property {string} y Some documentation*//** @type {MyProps} */let {y } =$props ();
By default props are treated as readonly, meaning reassignments will not propagate upwards and mutations will result in a warning at runtime in development mode. You will also get a runtime error when trying to bind:
to a readonly prop in a parent component. To declare props as bindable, use $bindable()
.
What this replacespermalink
$props
replaces the export let
and export { x as y }
syntax for declaring props. It also replaces $$props
and $$restProps
, and the little-known interface $$Props {...}
construct.
Note that you can still use export const
and export function
to expose things to users of your component (if they're using bind:this
, for example).
$bindablepermalink
To declare props as bindable, use $bindable()
. Besides using them as regular props, the parent can (can, not must) then also bind:
to them.
<script>
let { bindableProp = $bindable() } = $props();
</script>
You can pass an argument to $bindable()
. This argument is used as a fallback value when the property is undefined
.
<script>
let { bindableProp = $bindable('fallback') } = $props();
</script>
Note that the parent is not allowed to pass undefined
to a property with a fallback if it bind:
s to that property.
$inspectpermalink
The $inspect
rune is roughly equivalent to console.log
, with the exception that it will re-run whenever its
argument changes. $inspect
tracks reactive state deeply, meaning that updating something inside an object
or array using fine-grained reactivity will cause it to re-fire. (Demo:)
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // will console.log when `count` or `message` change
</script>
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
$inspect
returns a property with
, which you can invoke with a callback, which will then be invoked instead of console.log
. The first argument to the callback is either "init"
or "update"
, all following arguments are the values passed to $inspect
. Demo:
<script>
let count = $state(0);
$inspect(count).with((type, count) => {
if (type === 'update') {
debugger; // or `console.trace`, or whatever you want
}
});
</script>
<button onclick={() => count++}>Increment</button>
A convenient way to find the origin of some change is to pass console.trace
to with
:
ts
$inspect (stuff ).with (console .trace );
$inspect
only works during development.
$hostpermalink
Retrieves the this
reference of the custom element that contains this component. Example:
<svelte:options customElement="my-element" />
<script>
function greet(greeting) {
$host().dispatchEvent(
new CustomEvent('greeting', { detail: greeting })
);
}
</script>
<button onclick={() => greet('hello')}>say hello</button>
Only available inside custom element components, and only on the client-side
How to opt inpermalink
Current Svelte code will continue to work without any adjustments. Components using the Svelte 4 syntax can use components using runes and vice versa.
The easiest way to opt in to runes mode is to just start using them in your code. Alternatively, you can force the compiler into runes or non-runes mode either on a per-component basis...
<!-- this can be `true` or `false` -->
<svelte:options runes={true} />
...or for your entire app:
ts
export default {compilerOptions : {runes : true}};