Svelte 5 and Runes: The Reactivity Revolution in JavaScript in 2025
Hello HaWkers, Svelte 5 has arrived with a paradigmatic change that is making the JavaScript community rethink how we approach reactivity in web applications. Runes represent a new way of thinking about state and reactivity, and if you do not know them yet, you are missing one of the most interesting innovations in the frontend ecosystem.
Have you ever wondered why we need so many hooks and boilerplate to manage state in React? Svelte 5 offers an elegant alternative.
What Are Runes?
Runes are a new reactivity primitive introduced in Svelte 5. They replace the previous reactivity system based on reactive declarations ($:) with a more explicit and powerful model.
The Main Runes
List of available Runes:
$state- Declares reactive state$derived- Computes derived values$effect- Executes reactive side effects$props- Receives props in components$bindable- Props that can be two-way bound$inspect- Debug reactive values
Comparison: Svelte 4 vs Svelte 5
Let us see in practice how the syntax changed and why it is a significant improvement.
Simple State
Svelte 4 (old):
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Clicks: {count}
</button>Svelte 5 (Runes):
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
Clicks: {count}
</button>The difference seems subtle, but $state makes reactivity explicit and predictable.
Derived Values
Svelte 4 (old):
<script>
let firstName = 'John';
let lastName = 'Doe';
// Reactive declaration - can be confusing
$: fullName = `${firstName} ${lastName}`;
$: greeting = `Hello, ${fullName}!`;
</script>Svelte 5 (Runes):
<script>
let firstName = $state('John');
let lastName = $state('Doe');
// Much clearer and predictable
let fullName = $derived(`${firstName} ${lastName}`);
let greeting = $derived(`Hello, ${fullName}!`);
</script>
Effects (Side Effects)
Svelte 4 (old):
<script>
let count = 0;
// Implicit effect - hard to know when it executes
$: {
console.log(`Count changed to: ${count}`);
localStorage.setItem('count', count);
}
</script>Svelte 5 (Runes):
<script>
let count = $state(0);
// Explicit effect - clear when and why it executes
$effect(() => {
console.log(`Count changed to: ${count}`);
localStorage.setItem('count', count.toString());
});
</script>Practical Example: Todo App with Runes
Let us build a complete task application using Runes:
<script>
// Main state
let todos = $state([]);
let newTodo = $state('');
let filter = $state('all'); // 'all' | 'active' | 'completed'
// Derived values
let filteredTodos = $derived(() => {
switch (filter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
});
let remainingCount = $derived(
todos.filter(t => !t.completed).length
);
let allCompleted = $derived(
todos.length > 0 && todos.every(t => t.completed)
);
// Action functions
function addTodo() {
if (newTodo.trim()) {
todos.push({
id: Date.now(),
text: newTodo.trim(),
completed: false
});
newTodo = '';
}
}
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
function removeTodo(id) {
const index = todos.findIndex(t => t.id === id);
if (index !== -1) {
todos.splice(index, 1);
}
}
function toggleAll() {
const newState = !allCompleted;
todos.forEach(t => t.completed = newState);
}
function clearCompleted() {
todos = todos.filter(t => !t.completed);
}
// Effect for persistence
$effect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
});
// Load from localStorage on initialization
$effect(() => {
const saved = localStorage.getItem('todos');
if (saved) {
todos = JSON.parse(saved);
}
});
</script>
<div class="todo-app">
<h1>My Tasks</h1>
<form onsubmit={(e) => { e.preventDefault(); addTodo(); }}>
<input
type="text"
bind:value={newTodo}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
{#if todos.length > 0}
<div class="controls">
<button onclick={toggleAll}>
{allCompleted ? 'Uncheck' : 'Check'} All
</button>
<div class="filters">
<button
class:active={filter === 'all'}
onclick={() => filter = 'all'}
>
All
</button>
<button
class:active={filter === 'active'}
onclick={() => filter = 'active'}
>
Active
</button>
<button
class:active={filter === 'completed'}
onclick={() => filter = 'completed'}
>
Completed
</button>
</div>
</div>
<ul class="todo-list">
{#each filteredTodos as todo (todo.id)}
<li class:completed={todo.completed}>
<input
type="checkbox"
checked={todo.completed}
onchange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onclick={() => removeTodo(todo.id)}>X</button>
</li>
{/each}
</ul>
<footer>
<span>{remainingCount} task(s) remaining</span>
{#if todos.some(t => t.completed)}
<button onclick={clearCompleted}>
Clear Completed
</button>
{/if}
</footer>
{/if}
</div>
Why Runes Are Better?
1. Explicit Reactivity
In Svelte 4, any let variable was automatically reactive, which could cause confusion:
<!-- Svelte 4: When is this reactive? -->
<script>
let data = fetch('/api').then(r => r.json()); // Reactive?
let config = { theme: 'dark' }; // Reactive?
const MAX = 100; // Not reactive (const)
</script>With Runes, you explicitly declare what is reactive:
<!-- Svelte 5: Total clarity -->
<script>
let data = $state(null); // Reactive
let config = $state({ theme: 'dark' }); // Reactive
const MAX = 100; // Not reactive (intentional)
</script>2. Better TypeScript Support
Runes work perfectly with TypeScript:
<script lang="ts">
interface Todo {
id: number;
text: string;
completed: boolean;
}
let todos = $state<Todo[]>([]);
let newTodo = $state('');
// Types correctly inferred
let completedCount = $derived(
todos.filter(t => t.completed).length
);
</script>3. Universal Reactivity
Runes work outside .svelte components:
// stores/counter.svelte.ts
export function createCounter(initial = 0) {
let count = $state(initial);
return {
get value() { return count; },
increment() { count += 1; },
decrement() { count -= 1; },
reset() { count = initial; }
};
}
// In any component
import { createCounter } from './stores/counter.svelte';
const counter = createCounter(10);
Comparison with React
Let us compare the same functionality in React and Svelte 5:
React (Hooks):
import { useState, useMemo, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
// useMemo for derived values
const doubled = useMemo(() => count * 2, [count]);
// useEffect for side effects
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count} (doubled: {doubled})</p>
<input
type="number"
value={step}
onChange={e => setStep(Number(e.target.value))}
/>
<button onClick={() => setCount(c => c + step)}>
Increment by {step}
</button>
</div>
);
}Svelte 5 (Runes):
<script>
let count = $state(0);
let step = $state(1);
// Derived - no dependency array
let doubled = $derived(count * 2);
// Effect - no dependency array
$effect(() => {
document.title = `Count: ${count}`;
});
</script>
<div>
<p>Count: {count} (doubled: {doubled})</p>
<input type="number" bind:value={step} />
<button onclick={() => count += step}>
Increment by {step}
</button>
</div>Notable differences:
- No dependency arrays (fewer bugs)
- More concise syntax
- Native two-way binding
- Less boilerplate
Svelte 5 Performance
Svelte 5 brings significant performance improvements:
Benchmarks (js-framework-benchmark)
| Metric | React 19 | Vue 3.5 | Svelte 5 |
|---|---|---|---|
| Create 1000 rows | 45ms | 38ms | 32ms |
| Update 1000 rows | 38ms | 32ms | 28ms |
| Select row | 3.2ms | 2.8ms | 2.1ms |
| Remove row | 35ms | 30ms | 25ms |
| Create 10000 rows | 450ms | 380ms | 320ms |
| Bundle size (gzip) | 45KB | 35KB | 3KB |
⚡ Highlight: Svelte compiles to vanilla JavaScript, resulting in significantly smaller bundles.
Migrating from Svelte 4 to Svelte 5
Migration Strategy
- Update Svelte:
npm install svelte@5 - Existing code works: Svelte 5 is backward compatible
- Migrate gradually: Convert components one at a time
- Use legacy mode:
<svelte:options runes={false} />
Migration Tool
Svelte offers an automatic migration tool:
# Migrate files automatically
npx sv migrate svelte-5
# Check compatibility issues
npx svelte-checkMigration Patterns
Stores to Runes:
// Svelte 4: Stores
import { writable, derived } from 'svelte/store';
export const count = writable(0);
export const doubled = derived(count, $count => $count * 2);
// Svelte 5: Runes (stores still work!)
// But Runes are recommended for new code
let count = $state(0);
let doubled = $derived(count * 2);When to Use Svelte 5?
Use Svelte 5 when:
- Starting a new frontend project
- Needing maximum performance
- Wanting less boilerplate
- Preferring syntax closer to vanilla JavaScript
- Needing smaller bundles
Consider other options when:
- Team is very experienced in React/Vue
- Library ecosystem is crucial
- Needing complex SSR (SvelteKit is still younger)
Conclusion
Svelte 5 with Runes represents a significant evolution in how we think about reactivity in JavaScript. The explicit syntax, better TypeScript support, and superior performance make it an attractive choice for new projects.
If you already work with React or Vue, Runes concepts will be familiar, but the implementation is more elegant and straightforward. It is worth experimenting on a personal project to feel the difference.
The JavaScript community continues to innovate, and frameworks like Svelte show there is still much room for improvement in how we build web applications.
If you want to explore more about modern JavaScript frameworks, I recommend checking out the article on React vs Vue in 2025: Which Framework to Choose where we compare the main market options.
Let's go! 🦅
📚 Want to Deepen Your JavaScript Knowledge?
This article covered Svelte 5 and Runes, but there is much more to explore in modern development.
Developers who invest in solid, structured knowledge tend to have more opportunities in the market.
Complete Study Material
If you want to master JavaScript from basics to advanced, I have prepared a complete guide:
Investment options:
- 1x of $4.90 on card
- or $4.90 at sight
👉 Learn About JavaScript Guide
💡 Material updated with industry best practices

