Introduction
Svelte has always been different from other frontend frameworks. While React and Vue use a virtual DOM to reconcile changes, Svelte compiles your code to efficient vanilla JavaScript that updates the DOM directly. The result? Blazing-fast applications with minimal bundle sizes.
Svelte 5, released in 2026, represents the biggest evolution of the framework yet. It introduces runes, a new reactivity system that makes state management more intuitive and powerful. It brings snippets for reusable template fragments, improved performance, and a more consistent developer experience.
This comprehensive guide walks you through everything you need to know about Svelte 5. Whether you’re new to Svelte or migrating from Svelte 4, you’ll find practical examples, best practices, and migration strategies.
What Makes Svelte Different
The Compilation Approach
Unlike React or Vue, Svelte compiles your components at build time:
| Framework | Approach | Runtime |
|---|---|---|
| React | Virtual DOM | ~40KB |
| Vue | Reactive proxies | ~25KB |
| Svelte | Direct DOM | ~5KB |
Svelte shifts work from the browser to the build process, resulting in:
- Smaller bundle sizes
- Faster runtime performance
- No framework overhead
Why Svelte 5 Matters
Svelte 5 isn’t just incremental improvementsโit’s a fundamental shift in how you write reactive code:
- Runes: A new primitives-based reactivity system
- Snippets: Reusable template fragments
- Better TypeScript: Improved type inference
- Performance: Even faster updates and smaller bundles
Getting Started with Svelte 5
Installation
Create a new Svelte 5 project:
# Using the new CLI
npm create svelte@latest my-app
cd my-app
npm install
# Select "Svelte 5" when prompted
Project Structure
my-app/
โโโ src/
โ โโโ lib/
โ โ โโโ components/
โ โ โโโ Counter.svelte
โ โโโ routes/
โ โ โโโ +page.svelte
โ โโโ app.html
โโโ static/
โโโ svelte.config.js
โโโ vite.config.js
โโโ package.json
Understanding Runes
Runes are the heart of Svelte 5’s reactivity system. They’re special functions that tell Svelte how to track and update state.
The Core Runes
$state: Reactive Variables
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
Clicks: {count}
</button>
Unlike Svelte 4’s let declarations, $state explicitly marks variables as reactive. This makes the reactivity system more explicit and easier to understand.
$derived: Computed Values
<script>
let count = $state(0);
// Automatically updates when count changes
let doubled = $derived(count * 2);
let isEven = $derived(count % 2 === 0);
</script>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Is Even: {isEven}</p>
$effect: Side Effects
<script>
let count = $state(0);
// Runs when count changes
$effect(() => {
console.log(`Count is now: ${count}`);
// Cleanup function
return () => {
console.log('Cleanup');
};
});
</script>
<button onclick={() => count++}>
Increment
</button>
$props: Component Properties
<script>
let { name = 'World', count = 0, ...rest } = $props();
</script>
<h1>Hello, {name}!</h1>
<p>Count: {count}</p>
$bindable: Two-Way Binding
<!-- Child: Modal.svelte -->
<script>
let { open = $bindable(false), title = '' } = $props();
</script>
{#if open}
<dialog open>
<h2>{title}</h2>
<button onclick={() => open = false}>Close</button>
</dialog>
{/if}
<!-- Parent -->
<script>
import Modal from './Modal.svelte';
let showModal = $state(false);
</script>
<button onclick={() => showModal = true}>
Open Modal
</button>
<Modal bind:open={showModal} title="Hello!" />
Snippets: Reusable Template Fragments
Snippets replace slots and named slots with a more intuitive syntax.
Basic Snippets
<script>
let { items = [] } = $props();
function renderItem(item) {
return `<li>${item}</li>`;
}
</script>
{#snippet itemTemplate(item)}
<li class="item">
<span class="name">{item.name}</span>
<span class="price">${item.price}</span>
</li>
{/snippet}
<ul>
{#each items as item}
{@render itemTemplate(item)}
{/each}
</ul>
Snippets with Parameters
{#snippet card(title, content)}
<div class="card">
<h3>{title}</h3>
<p>{content}</p>
</div>
{/snippet}
{@render card('Feature 1', 'Description 1')}
{@render card('Feature 2', 'Description 2')}
Passing Snippets as Props
<!-- List.svelte -->
<script>
let { items = [], renderItem } = $props();
</script>
<ul>
{#each items as item}
{@render renderItem(item)}
{/each}
</ul>
<!-- Usage -->
<script>
import List from './List.svelte';
let items = $state(['Apple', 'Banana', 'Cherry']);
{#snippet fruitItem(name)}
<li>๐ {name}</li>
{/snippet}
</script>
<List {items} renderItem={fruitItem} />
Migrating from Svelte 4
If you’re coming from Svelte 4, here’s what you need to know.
Key Changes
| Svelte 4 | Svelte 5 |
|---|---|
let count = 0 |
let count = $state(0) |
$: doubled = count * 2 |
let doubled = $derived(count * 2) |
$: { console.log(count) } |
$effect(() => console.log(count)) |
export let name |
let { name } = $props() |
<slot /> |
{@render children()} |
Step-by-Step Migration
1. Update State Declarations
<!-- Svelte 4 -->
<script>
let count = 0;
let items = [];
let user = { name: 'John' };
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0);
let items = $state([]);
let user = $state({ name: 'John' });
</script>
2. Convert Computed Values
<!-- Svelte 4 -->
<script>
let count = 0;
$: doubled = count * 2;
$: names = items.map(i => i.name);
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0);
let items = $state([]);
let doubled = $derived(count * 2);
let names = $derived(items.map(i => i.name));
</script>
3. Update Effects
<!-- Svelte 4 -->
<script>
let count = 0;
$: {
console.log(count);
localStorage.setItem('count', count);
}
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0);
$effect(() => {
console.log(count);
localStorage.setItem('count', count);
});
</script>
4. Migrate Props
<!-- Svelte 4 -->
<script>
export let title = 'Default';
export let count = 0;
</script>
<!-- Svelte 5 -->
<script>
let { title = 'Default', count = 0, ...rest } = $props();
</script>
5. Migrate Slots to Snippets
<!-- Svelte 4 -->
<!-- Button.svelte -->
<button class="btn">
<slot>Click me</slot>
</button>
<!-- Usage -->
<Button>Submit</Button>
<!-- Svelte 5 -->
<!-- Button.svelte -->
<script>
let { children, ...rest } = $props();
</script>
<button class="btn">
{@render children?.() ?? 'Click me'}
</button>
<!-- Usage -->
<Button>Submit</Button>
Using the Migration Tool
Svelte provides a codemod for automatic migration:
npx svelte-migrate@latest svelte-5
Advanced Svelte 5 Patterns
Deep Reactivity with $state
<script>
// Deep reactive - nested changes trigger updates
let user = $state({
profile: {
name: 'John',
settings: {
theme: 'dark'
}
}
});
function updateTheme(theme) {
// This will trigger updates anywhere user is used
user.profile.settings.theme = theme;
}
</script>
$derived.by for Complex Calculations
<script>
let items = $state([
{ name: 'Apple', price: 1.5, quantity: 3 },
{ name: 'Banana', price: 0.5, quantity: 6 }
]);
let summary = $derived.by(() => {
const total = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const count = items.length;
const average = count > 0 ? total / count : 0;
return { total, count, average };
});
</script>
<p>Total: ${summary.total.toFixed(2)}</p>
<p>Items: {summary.count}</p>
<p>Average: ${summary.average.toFixed(2)}</p>
Stores in Svelte 5
While runes are the new default, stores still work:
<script>
import { writable } from 'svelte/store';
const count = writable(0);
</script>
<button onclick={() => count.update(n => n + 1)}>
{$count}
</button>
However, runes are recommended for new code:
<script>
// Preferred in Svelte 5
let count = $state(0);
</script>
Component Patterns
Props with Validation
<script>
import { type ComponentProps } from 'svelte';
let {
name,
age,
email = $bindable()
} = $props();
// Runtime validation
if (age < 0 || age > 150) {
throw new Error('Invalid age');
}
</script>
Event Handling
<script>
let { onClick, onHover, children } = $props();
function handleClick(e) {
onClick?.(e);
}
</script>
<button onclick={handleClick}>
{@render children?.()}
</button>
Conditional Classes
<script>
let { active = false, variant = 'primary' } = $props();
let className = $derived(
['btn', `btn-${variant}`, active && 'active']
.filter(Boolean)
.join(' ')
);
</script>
<button class={className}>
<slot />
</button>
Performance Optimization
Using $effect Carefully
Effects run after every render. Use them sparingly:
<script>
// Good: LocalStorage sync
let theme = $state('dark');
$effect(() => {
localStorage.setItem('theme', theme);
});
// Better: Only run when specific value changes
$effect(() => {
document.documentElement.setAttribute('data-theme', theme);
});
</script>
Fine-Grained Reactivity
Svelte 5’s reactivity is fine-grained:
<script>
let user = $state({
name: 'John',
email: '[email protected]'
});
// Only re-renders when name changes
function updateName(newName) {
user.name = newName;
}
// Only re-renders when email changes
function updateEmail(newEmail) {
user.email = newEmail;
}
</script>
<h1>{user.name}</h1> <!-- Updates independently -->
<p>{user.email}</p> <!-- Updates independently -->
Svelte 5 with TypeScript
TypeScript is Built In
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
let user = $state<User>({
id: 1,
name: 'John',
email: '[email protected]'
});
function greet(name: string): string {
return `Hello, ${name}!`;
}
</script>
Typing Props
<script lang="ts">
interface Props {
title: string;
count?: number;
onSave?: (value: string) => void;
}
let {
title,
count = 0,
onSave,
...rest
}: Props = $props();
</script>
Typing Snippets
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
items: string[];
renderItem: Snippet<[string]>;
}
let { items, renderItem }: Props = $props();
</script>
{#each items as item}
{@render renderItem(item)}
{/each}
External Resources
Official Documentation
Learning Resources
- Joy of Code - Svelte tutorials
- Threlte - 3D with Svelte
- SvelteKit - Full-stack framework
Community
Conclusion
Svelte 5 represents a significant evolution in frontend development. The new runes system makes reactivity more explicit and powerful, while snippets provide a cleaner way to create reusable template fragments.
Key takeaways:
- Runes are the future: Use
$state,$derived,$effect, and$propsfor all new code - Migration is gradual: Svelte 4 code continues to work; migrate when comfortable
- Performance is built in: Fine-grained reactivity means less work for you
- TypeScript is first-class: Full TypeScript support out of the box
Whether you’re building a small widget or a full-stack application, Svelte 5 provides the tools you need to ship faster with smaller, more performant code.
Related Articles
- Qwik Complete Guide: Zero-Hydration Framework
- Astro Complete Guide: The Static-First Framework
- HTMX Complete Guide: Simpler React Alternative
- Modern CSS and Frontend Frameworks
Comments