Svelte 5 bringt das bisher tiefgreifendste Redesign seit dem Start des Frameworks: Runes. Was auf den ersten Blick wie „noch mehr Sigils" wirkt, ist in Wahrheit ein vollständig neues, explizites Reaktivitätssystem — weg von magischem Compiler-Zustand hin zu transparenten, kompositionsfähigen Primitiven. Dieses Tutorial zeigt anhand von Claude-Code-generierten Beispielen, wie du die sechs Kern-Runes einsetzt und bestehende Svelte-4-Komponenten Schritt für Schritt migrierst.
Grundkenntnisse in Svelte 4 oder SvelteKit. Svelte ≥ 5.0 (npm create svelte@latest). Node ≥ 20. Claude Code als KI-Pair-Programmer im Terminal.
1 $state — Das neue Herzstück der Reaktivität
In Svelte 4 war reaktiver Zustand implizit: jede let-Variable
im Skript-Block war automatisch reaktiv. In Svelte 5 ist das explizit —
du musst $state() aufrufen, um reaktiven Zustand zu deklarieren.
Das mag zunächst nach mehr Boilerplate klingen, schafft aber massive Klarheit
für Tooling, TypeScript-Integration und Komposierbarkeit.
Grundlegende Verwendung
<script>
// Svelte 4 (alt): let count = 0; // implizit reaktiv
// Svelte 5 (neu): explizit mit $state()
let count = $state(0);
function increment() {
count++;
}
</script>
<button onclick={increment}>
Klicks: {count}
</button>
Deep Reactivity — Objekte und Arrays
Ein besonderer Vorteil von $state() gegenüber einfachen Stores:
verschachtelte Objekte und Arrays sind automatisch tief reaktiv.
Du musst nicht mehr manuell todos = [...todos] schreiben.
<script>
let todos = $state([
{ id: 1, text: 'Svelte 5 lernen', done: false },
{ id: 2, text: 'Claude Code einrichten', done: true },
]);
// Direkte Mutation — kein Spread nötig!
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
function addTodo(text) {
todos.push({ id: Date.now(), text, done: false });
}
</script>
Mit $state.raw() erzeugst du einen reaktiven Zeiger auf ein
Objekt, dessen innere Eigenschaften nicht beobachtet werden.
Nützlich für große unveränderliche Datensätze (z. B. JSON-Config,
Icon-Definitionen), bei denen du kein Deep-Proxy-Overhead möchtest.
Mutationen am Objekt selbst lösen keine Updates aus —
nur eine Neuzuweisung des Zeigers.
<script>
// Nur Neuzuweisung löst Re-Render aus, Mutation der inneren Felder nicht
let config = $state.raw({ theme: 'dark', lang: 'de' });
function switchTheme() {
// Erzeugt neues Objekt → Reaktivität ausgelöst
config = { ...config, theme: config.theme === 'dark' ? 'light' : 'dark' };
}
</script>
$state.snapshot() — Nicht-reaktive Kopie erstellen
Manchmal brauchst du einen "eingefrorenen" Snapshot des aktuellen
Zustands — etwa für JSON-Serialisierung, Logging oder den Vergleich
zweier Zeitpunkte. $state.snapshot() gibt dir ein
einfaches POJO ohne Proxy zurück.
<script>
let form = $state({ name: '', email: '' });
function submit() {
const data = $state.snapshot(form);
// data ist jetzt ein einfaches Objekt, sicher für JSON.stringify
fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data),
});
}
</script>
| Svelte 4 | Svelte 5 ($state) |
|---|---|
let count = 0 (implizit reaktiv) |
let count = $state(0) (explizit) |
todos = [...todos] für Array-Updates |
todos.push(item) funktioniert direkt |
writable(0) aus svelte/store |
$state(0) — kein Store-Import nötig |
| Nicht komposierbar außerhalb von .svelte | In .svelte.ts/.svelte.js Dateien nutzbar |
2 $derived — Berechnete Werte ohne Redundanz
$derived() ist der Nachfolger der reaktiven Deklarationen
($: doubled = count * 2) aus Svelte 4. Der entscheidende
Unterschied: $derived ist lazy und
gecacht. Der Wert wird erst bei Lesezugriff neu berechnet,
wenn sich eine abhängige $state-Variable geändert hat.
Einfache Ableitungen
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let isEven = $derived(count % 2 === 0);
// Mehrere State-Abhängigkeiten
let width = $state(100);
let height = $state(80);
let area = $derived(width * height);
</script>
<p>{count} × 2 = {doubled} ({isEven ? 'gerade' : 'ungerade'})</p>
<p>Fläche: {area} px²</p>
$derived.by() — Komplexe Ableitungen mit Funktionskörper
Wenn die Ableitung mehr als ein einfacher Ausdruck ist — z. B. ein
Array-Filter mit mehreren Schritten — nutzt du $derived.by().
Das ist das Äquivalent zu $: { ... }-Blöcken, aber sauber
als Funktion verpackt.
<script>
let todos = $state([...]);
let filter = $state('all'); // 'all' | 'active' | 'done'
let searchTerm = $state('');
let filteredTodos = $derived.by(() => {
let result = todos;
if (filter === 'active') {
result = result.filter(t => !t.done);
} else if (filter === 'done') {
result = result.filter(t => t.done);
}
if (searchTerm.trim()) {
const q = searchTerm.toLowerCase();
result = result.filter(t => t.text.toLowerCase().includes(q));
}
return result.sort((a, b) => +a.done - +b.done);
});
let stats = $derived.by(() => ({
total: todos.length,
done: todos.filter(t => t.done).length,
pct: todos.length
? Math.round(todos.filter(t => t.done).length / todos.length * 100)
: 0,
}));
</script>
<p>{stats.done}/{stats.total} erledigt ({stats.pct}%)</p>
Eine normale Funktion im Template ({compute()}) würde bei
jedem Render erneut ausgeführt. $derived dagegen
ist memoized: Die Berechnung läuft nur, wenn sich eine
der genutzten $state-Quellen wirklich geändert hat.
Bei teuren Berechnungen (Sortierung großer Listen, komplexe Aggregationen)
macht das einen messbaren Performance-Unterschied.
3 $effect — Seiteneffekte transparent deklarieren
$effect() ersetzt onMount, afterUpdate
und die reaktiven $: { sideEffect() }-Blöcke.
Der wichtigste Unterschied zu React's useEffect:
Svelte trackt die Abhängigkeiten automatisch —
kein Dependency-Array notwendig.
<script>
let count = $state(0);
let theme = $state('dark');
// Läuft nach jedem Render wenn count oder theme sich ändern
$effect(() => {
document.title = `Klicks: ${count} | Theme: ${theme}`;
});
// Mit Cleanup-Funktion
$effect(() => {
const handler = (e) => console.log('resize', e);
window.addEventListener('resize', handler);
// Cleanup: automatisch aufgerufen bevor Effect erneut läuft
return () => {
window.removeEventListener('resize', handler);
};
});
</script>
$effect.pre() — Vor DOM-Updates
Standardmäßig läuft $effect() nach DOM-Updates
(äquivalent zu afterUpdate). $effect.pre() läuft
vor dem DOM-Update — nützlich für Scroll-Position-Preservation
oder Animationen, die den aktuellen DOM-Zustand lesen müssen.
<script>
let messages = $state([]);
let listEl;
// Scrollposition VOR Update merken (sonst ist DOM schon verändert)
$effect.pre(() => {
messages; // Abhängigkeit registrieren
if (!listEl) return;
const atBottom =
listEl.scrollHeight - listEl.scrollTop - listEl.clientHeight < 50;
if (atBottom) {
return () => {
listEl.scrollTop = listEl.scrollHeight;
};
}
});
</script>
<ul bind:this={listEl}>
{#each messages as msg}
<li>{msg}</li>
{/each}
</ul>
| Svelte 4 API | Svelte 5 Äquivalent |
|---|---|
onMount(() => { ... }) |
$effect(() => { ... }) |
afterUpdate(() => { ... }) |
$effect(() => { ... }) (nach DOM) |
beforeUpdate(() => { ... }) |
$effect.pre(() => { ... }) |
onDestroy(() => cleanup()) |
return () => cleanup() in $effect |
$: { sideEffect(value) } |
$effect(() => { sideEffect(value) }) |
$effect darf keinen State schreiben, der
zugleich als Abhängigkeit gelesen wird — sonst entsteht eine Endlosschleife.
Wenn du State aus einem Effect heraus setzen musst, nutze
untrack(() => state = newVal) aus svelte.
4 $props und $bindable — Props mit Typsicherheit
Statt export let prop für jede einzelne Prop musst du in
Svelte 5 nur noch $props() destrukturieren.
Das Ergebnis: weniger Boilerplate, perfekte TypeScript-Integration
und klare Unterscheidung zwischen read-only Props und zwei-Wege-Bindungen.
Einfache Props
<!-- Button.svelte -->
<script>
// Svelte 4: export let label; export let disabled = false;
// Svelte 5:
let { label, disabled = false, variant = 'primary' } = $props();
</script>
<button class="btn btn-{variant}" {disabled}>
{label}
</button>
TypeScript Props Interface
<script lang="ts">
interface Props {
label: string;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
onclick?: (e: MouseEvent) => void;
}
let {
label,
disabled = false,
variant = 'primary',
onclick,
}: Props = $props();
</script>
$bindable — Zwei-Wege-Bindung explizit machen
In Svelte 4 konnte der Parent jede Prop mit bind:prop binden.
In Svelte 5 muss die Komponente explizit deklarieren, welche
Props bindbar sind — über $bindable() als Standardwert.
<!-- CustomInput.svelte -->
<script lang="ts">
let {
value = $bindable(''), // Zwei-Wege-Bindung möglich
label, // Nur read-only
placeholder = '',
} = $props();
</script>
<label>
{label}
<input bind:value {placeholder} />
</label>
<!-- Verwendung im Parent -->
<script>
let email = $state('');
</script>
<CustomInput label="E-Mail" bind:value={email} />
<p>Eingabe: {email}</p>
Mit let { label, ...rest } = $props() fängst du alle
unbekannten Props in rest auf und kannst sie via
{...rest} an ein DOM-Element weitergeben —
praktisch für vollständig typisierte Wrapper-Komponenten.
5 Snippets und Render Tags — Slot-Ersatz mit Superkräften
Svelte 5 ersetzt <slot> durch Snippets —
benannte, parametrisierbare Template-Blöcke. Sie sind mächtiger als Slots,
weil sie Parameter empfangen können und als Props weitergegeben werden dürfen.
Grundlegende Snippet-Syntax
<!-- Im Parent: Snippet definieren und an Kind übergeben -->
<script>
import DataTable from './DataTable.svelte';
let users = $state([...]);
</script>
<DataTable items={users}>
{#snippet header()}
<tr><th>Name</th><th>E-Mail</th><th>Rolle</th></tr>
{/snippet}
{#snippet row(user)}
<tr>
<td>{user.name}</td>
<td>{user.email}</td>
<td><span class="badge">{user.role}</span></td>
</tr>
{/snippet}
</DataTable>
Snippets in der Komponente empfangen
<!-- DataTable.svelte -->
<script>
import { type Snippet } from 'svelte';
let {
items,
header, // Snippet<[]>
row, // Snippet<[item: typeof items[number]]>
empty, // optionaler Fallback-Snippet
} = $props();
</script>
<table>
<thead>{@render header()}</thead>
<tbody>
{#each items as item}
{@render row(item)}
{/each}
{#if items.length === 0 && empty}
{@render empty()}
{/if}
</tbody>
</table>
children-Snippet — Default Slot
<!-- Card.svelte — Default-Slot über children-Snippet -->
<script>
import { type Snippet } from 'svelte';
let { title, children }: { title: string; children: Snippet } = $props();
</script>
<div class="card">
<h3>{title}</h3>
{@render children()}
</div>
<!-- Verwendung -->
<Card title="Meine Karte">
<p>Alles was hier steht landet in children</p>
</Card>
Slots hatten einen fundamentalen Nachteil: Sie konnten keine Daten
aus der Kind-Komponente nach oben reichen (abgesehen vom unhandlichen
let:item-Pattern). Snippets dagegen sind einfache Funktionen
mit Parametern — vollständig typisierbar und komposierbar.
6 Migration von Svelte 4 — Schritt-für-Schritt-Guide
Svelte 5 bietet einen Kompatibilitätsmodus: du kannst Svelte-4-Komponenten und Svelte-5-Komponenten im selben Projekt mischen. Eine schrittweise Migration ist also möglich. Das Svelte-Team empfiehlt, mit neuen Komponenten zu starten und alte iterativ zu migrieren.
npx sv migrate svelte-5 migriert einfache Komponenten
vollautomatisch. Komplexe Stores, use:-Actions und
benutzerdefinierte Store-Logik erfordern manuelle Anpassung.
1. Stores → $state (in .svelte.ts)
// Svelte 4 — stores/counter.ts
import { writable, derived } from 'svelte/store';
export const count = writable(0);
export const doubled = derived(count, $c => $c * 2);
// Svelte 5 — counter.svelte.ts (Dateiendung beachten!)
export function createCounter() {
let count = $state(0);
let doubled = $derived(count * 2);
return {
get count() { return count; },
get doubled() { return doubled; },
increment() { count++; },
reset() { count = 0; },
};
}
// In Komponenten:
const counter = createCounter();
// counter.count, counter.doubled sind reaktiv ✓
2. Reactive Declarations → $derived
// Svelte 4
let firstName = 'Max';
let lastName = 'Mustermann';
$: fullName = `${firstName} ${lastName}`;
$: initials = `${firstName[0]}.${lastName[0]}.`;
$: {
// reaktiver Block
console.log('Name geändert:', fullName);
}
// Svelte 5
let firstName = $state('Max');
let lastName = $state('Mustermann');
let fullName = $derived(`${firstName} ${lastName}`);
let initials = $derived(`${firstName[0]}.${lastName[0]}.`);
$effect(() => {
console.log('Name geändert:', fullName);
});
3. Lifecycle → $effect
// Svelte 4
import { onMount, onDestroy, afterUpdate } from 'svelte';
onMount(() => {
analytics.track('page_view');
});
let interval;
onMount(() => {
interval = setInterval(tick, 1000);
});
onDestroy(() => clearInterval(interval));
// Svelte 5 — alles in einem $effect
$effect(() => {
analytics.track('page_view');
const interval = setInterval(tick, 1000);
return () => clearInterval(interval); // onDestroy
});
4. Slots → Snippets
<!-- Svelte 4 -->
<div class="card">
<slot name="header" />
<slot />
<slot name="footer" />
</div>
<!-- Svelte 5 -->
<script>
let { header, children, footer } = $props();
</script>
<div class="card">
{#if header}{@render header()}{/if}
{@render children()}
{#if footer}{@render footer()}{/if}
</div>
- ✅
let x = 0→let x = $state(0)für reaktiven Zustand - ✅
$: derived = expr→let derived = $derived(expr) - ✅
$: { block }→$effect(() => { block }) - ✅
export let prop→let { prop } = $props() - ✅
export let bindable→let { bindable = $bindable() } = $props() - ✅
<slot />→{@render children()} - ✅
<slot name="x" />→{@render x()}(x als Prop) - ✅
onMount/onDestroy→$effectmit Return-Cleanup - ✅
writable()Store →$state()in .svelte.ts - ✅
derived()Store →$derived()in .svelte.ts
KI-gestützte Svelte-5-Entwicklung mit Claude Code
Lass Claude Code deine Svelte-4-Komponenten automatisch in Runes migrieren, Snippets generieren und TypeScript-Props-Interfaces ableiten — direkt im Terminal, ohne Kontextwechsel.
Jetzt kostenlos testen →7 Fazit — Warum Runes ein Schritt nach vorne sind
Svelte 5 Runes fühlen sich zunächst wie zusätzliche Syntax an —
aber das Gegenteil ist wahr: Das System ist kleiner und
konsistenter als das Svelte-4-Reaktivitätsmodell.
Keine geheimen Compiler-Tricks mehr, keine $:-Regeln die
außerhalb von .svelte-Dateien nicht funktionieren,
keine Store-Subscribe/Unsubscribe-Fehler mehr.
Die sechs Runes ($state, $derived,
$effect, $props, $bindable,
$inspect) ersetzen ein halbes Dutzend unterschiedlicher
Konzepte aus Svelte 4 — und lassen sich dank der
.svelte.ts-Dateien nun auch außerhalb von Komponenten nutzen.
Das ermöglicht echte Kompositions-Patterns à la React Hooks,
aber ohne deren Fallen (Rules of Hooks, Closure-Stale-State etc.).
Mit Claude Code als KI-Pair-Programmer lassen sich Migrationen
beschleunigen: Claude erkennt automatisch $:-Deklarationen,
schlägt das passende Rune-Äquivalent vor und generiert TypeScript-Types
für Props-Interfaces. Ein Svelte-4-Projekt von 50 Komponenten kann
so in wenigen Stunden auf Runes umgestellt werden.