Best forFormatting, validating, shrinking, or inspecting code-adjacent text.
125 snippets
Runes (20)
$state — reactive variable
Runes
<script lang="ts">
let count = $state(0);
let user = $state({ name: 'Lei', age: 30 });
</script>
<button onclick={() => count++}>{count}</button>
<input bind:value={user.name} />
What it does:$state turns a let-binding into a reactive value. Reading it (in markup or another rune) subscribes; writing it triggers re-render. Works on primitives AND objects — the returned object is a Proxy that tracks deep mutation, so `user.name = "Hong"` updates without you reassigning the whole object. Replace every Svelte 4 `let` that should react with `$state`.
Svelte 4 equivalent
<script>
let count = 0;
let user = { name: 'Lei', age: 30 };
</script>
$derived — cached computed value
Runes
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
let label = $derived(`count is ${count}`);
</script>
<p>{count} × 2 = {doubled}</p>
What it does:$derived takes a single expression and returns a value that re-computes whenever any reactive dependency it reads changes — and caches the result, so repeated reads inside the same render are free. It is the runes-mode replacement for Svelte 4 `$: doubled = count * 2`. The expression must be pure; for multi-statement logic use $derived.by.
Svelte 4 equivalent
$: doubled = count * 2;
$derived.by — multi-statement derived
Runes
<script lang="ts">
let items = $state<number[]>([1, 2, 3]);
let stats = $derived.by(() => {
let sum = 0;
let max = -Infinity;
for (const n of items) {
sum += n;
if (n > max) max = n;
}
return { sum, max, avg: sum / items.length };
});
</script>
What it does:$derived.by takes a FUNCTION instead of an expression, so you can run loops, declare locals, and build the value step by step. Same caching and dependency tracking as $derived. Reach for it whenever the derivation needs more than one statement; reach for plain $derived when the value fits in one line.
$effect — side effect with auto-tracking
Runes
<script lang="ts">
let url = $state('/api/me');
let data = $state<unknown>(null);
$effect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal })
.then(r => r.json())
.then(d => { data = d; });
return () => ctrl.abort(); // cleanup
});
</script>
What it does:$effect runs after the component is mounted and whenever any reactive value it READ changes. Return a function to clean up (cancel timers, abort fetches, remove listeners) before the next run and on unmount. It is the runes replacement for Svelte 4 `$: { … }` reactive blocks AND for onMount + onDestroy combined.
<script lang="ts">
let items = $state<string[]>([]);
let listEl: HTMLDivElement;
let shouldAutoScroll = false;
$effect.pre(() => {
// read items so this re-runs when they change
items;
if (!listEl) return;
shouldAutoScroll =
listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight - 5;
});
$effect(() => {
items;
if (shouldAutoScroll) listEl?.scrollTo({ top: listEl.scrollHeight });
});
</script>
What it does:$effect.pre runs BEFORE Svelte updates the DOM, so you can read the pre-update layout (scroll position, focus, selection) and act on it after the update completes. The classic use case is "keep the chat scrolled to bottom only if the user was already there". Plain $effect runs AFTER the DOM is updated — you usually want $effect; reach for .pre when you need pre-update measurements.
What it does:$props is how a component receives its inputs in runes mode. You destructure with defaults and a type annotation in one go — no separate `export let` per prop, no `interface $$Props`. It returns a Proxy so the destructured values stay reactive when the parent updates them. Use `let { ...rest } = $props()` to capture every other attribute (good for wrapper components).
Svelte 4 equivalent
<script>
export let title;
export let count = 0;
</script>
$bindable — make a prop two-way bindable
Runes
<!-- Input.svelte -->
<script lang="ts">
let { value = $bindable('') }: { value?: string } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script lang="ts">
let text = $state('');
</script>
<Input bind:value={text} />
What it does:Props are read-only by default in Svelte 5. Wrapping a prop in $bindable opts it into two-way binding so the parent can `bind:value={…}` and writes inside the child propagate back. Without $bindable, `bind:` on that prop is a compile error. Pass a default value to $bindable as the fallback when the parent does not bind.
$inspect — log reactive changes
Runes
<script lang="ts">
let count = $state(0);
let user = $state({ name: 'Lei' });
$inspect(count, user);
// custom handler
$inspect(count).with((type, val) => {
console.log(`[${type}]`, val);
});
</script>
What it does:$inspect is a dev-only rune that logs whenever any value it tracks changes, including deep mutations. It is the runes-friendly answer to "where did this value get changed?" — much better than scattering console.log calls because it survives reactivity changes. Stripped from production builds. Chain .with(fn) for a custom handler instead of console.log.
$host — custom element host
Runes
<svelte:options customElement="my-counter" />
<script lang="ts">
let count = $state(0);
function emit() {
$host().dispatchEvent(
new CustomEvent('change', { detail: count })
);
}
</script>
<button onclick={() => { count++; emit(); }}>{count}</button>
What it does:$host is only meaningful when the component is compiled to a custom element (`<svelte:options customElement="…" />`). It returns the host HTMLElement so you can dispatch real DOM CustomEvents that consumers can listen to with `addEventListener`. Outside a custom element it has no use — for normal Svelte components, pass callback props instead.
$state.raw — opt out of deep tracking
Runes
<script lang="ts">
// huge dataset — Proxy overhead is wasteful
let scene = $state.raw({ vertices: bigArray, faces: bigFaces });
function loadScene(next: typeof scene) {
scene = next; // reassignment DOES trigger
}
// scene.vertices.push(…) // mutation does NOT trigger
</script>
What it does:$state.raw stores the value without proxying — only WHOLESALE reassignment triggers updates, nested mutations do not. Use it for large, hot, frozen-shape data structures (Three.js scenes, big Maps, parser ASTs) where the per-property Proxy overhead is real. The trade-off: you must reassign the whole value to update.
$derived: reassign to override
Runes
<script lang="ts">
let total = $state(100);
// optimistic UI: show a guessed value, let the real one win later
let display = $derived(total);
function bump() {
display = total + 1; // temporary override
}
// next time `total` changes, `display` snaps back to deriving from it
</script>
<button onclick={bump}>{display}</button>
What it does:A $derived value is writable in Svelte 5.25+. Assigning to it sets a temporary override that holds until the next time one of its dependencies changes, at which point it reverts to the computed value. The pattern fits optimistic UI: show an instant guess, then let the real derivation overwrite it when fresh data lands. Do not reach for this when a plain $state plus $effect is clearer.
$state.snapshot: plain copy of a proxy
Runes
<script lang="ts">
let form = $state({ name: 'Lei', tags: ['a', 'b'] });
function save() {
// structuredClone / JSON / 3rd-party libs choke on the Proxy
const plain = $state.snapshot(form);
localStorage.setItem('draft', JSON.stringify(plain));
// `plain` is a non-reactive deep clone, safe to pass anywhere
}
</script>
What it does:$state.snapshot(value) returns a static deep clone of a $state proxy, stripped of all reactivity. Use it when handing state to code that does not expect a Proxy: structuredClone, JSON.stringify of nested data, third-party libraries, or postMessage. Reading the proxy directly in those contexts can throw or behave oddly because of the Proxy traps.
$effect.root: manually scoped effect
Runes
<script lang="ts">
// create effects OUTSIDE the component lifecycle, clean up by hand
const cleanup = $effect.root(() => {
let n = $state(0);
$effect(() => console.log('n is', n));
const id = setInterval(() => n++, 1000);
return () => clearInterval(id);
});
// later, when you decide: cleanup();
</script>
What it does:$effect.root(fn) creates an effect scope that is NOT tied to the component lifecycle. Effects created inside it live until you call the returned dispose function yourself. Use it for effects that must outlive a component, or to set up reactivity in non-component code (a shared module). With great power: forget to call the disposer and you leak.
$effect.tracking: am I inside reactive context
Runes
<script lang="ts">
let count = $state(0);
// false at the top level of <script>
console.log($effect.tracking()); // false
$effect(() => {
count;
console.log($effect.tracking()); // true — inside an effect
});
</script>
What it does:$effect.tracking() returns true when the code runs inside a tracking context (an $effect, a $derived, or template). It is a low-level helper for library authors who want a function to behave reactively when read inside an effect but cheaply when called once outside. Most app code never needs it.
untrack: read without subscribing
Runes
<script lang="ts">
import { untrack } from 'svelte';
let a = $state(0);
let b = $state(0);
// re-runs only when `a` changes, ignores `b`
$effect(() => {
a;
const snapshotB = untrack(() => b);
console.log('a changed, b was', snapshotB);
});
</script>
What it does:untrack(fn) reads reactive values inside fn WITHOUT registering them as dependencies of the surrounding effect or derived. Use it when an effect should react to some values but only peek at others, or to break an accidental dependency that causes an infinite loop. Imported from `svelte`, not a rune.
What it does:Files ending in `.svelte.ts` (or `.svelte.js`) can use runes, so you can build reactive logic outside components and import it. Expose $state through a getter — returning the bare value would copy a primitive and lose reactivity. This is the Svelte 5 way to write composable shared state without stores, a close cousin of "composables".
$props.id(): stable unique id
Runes
<script lang="ts">
// one consistent id across SSR and hydration
const uid = $props.id();
</script>
<label for={`${uid}-email`}>Email</label>
<input id={`${uid}-email`} type="email" />
<p id={`${uid}-hint`}>We never share it.</p>
<input aria-describedby={`${uid}-hint`} />
What it does:$props.id() (Svelte 5.20+) returns a unique string that is stable across server render and client hydration, so it never causes a mismatch. Use it to wire `for`/`id` pairs and `aria-describedby` without hardcoding ids that could collide when a component renders more than once. Replaces ad-hoc Math.random() ids that break SSR.
What it does:A plain $state wraps the value in a deep Proxy, so mutations at any nesting level are tracked: assigning a cell, pushing to a nested array, splicing, sorting in place. You almost never need to reassign the whole tree to trigger an update. The exceptions are $state.raw (shallow) and values that are not plain objects/arrays (Map, Set, class instances) which need svelte/reactivity helpers.
SvelteMap / SvelteSet: reactive collections
Runes
<script lang="ts">
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
let seen = new SvelteSet<string>();
let cache = new SvelteMap<string, number>();
function visit(id: string) {
seen.add(id); // triggers re-render
cache.set(id, Date.now());
}
</script>
<p>{seen.size} visited</p>
What it does:A raw `new Map()` or `new Set()` inside $state is NOT deeply reactive — mutations like .set/.add do not trigger updates. Import SvelteMap and SvelteSet from `svelte/reactivity` for drop-in reactive versions with the same API. Their `.size`, iteration, and `.has()` all participate in reactivity. Same idea for SvelteDate and SvelteURL.
untracked write: avoid effect-write loops
Runes
<script lang="ts">
let query = $state('');
let results = $state<string[]>([]);
// effect that writes a DIFFERENT state it does not read — safe
$effect(() => {
const q = query.trim();
fetchResults(q).then((r) => {
results = r; // writes results, never reads it → no loop
});
});
</script>
What it does:An $effect only loops when it READS and WRITES the same reactive value. Writing a value the effect does not read (here `results`) is fine and common — fetch on `query` change, store into `results`. The mental model: list your reads (dependencies) and your writes (outputs); a loop only happens when those sets overlap.
Svelte 4 → 5 (12)
let → $state
Svelte 4 → 5
// Svelte 4 (legacy)
let count = 0;
// Svelte 5 (runes)
let count = $state(0);
What it does:In Svelte 4, every top-level `let` in a `<script>` was automatically reactive (compiler magic). In Svelte 5 runes mode, `let` is plain JavaScript and you opt in with $state. Plus side: explicit, no surprises, works the same in `.svelte.ts` modules. Migration: wrap every reactive `let` in $state(…); leave non-reactive locals alone.
What it does:Svelte 4 had ONE syntax `$:` that meant "derived value" when assigning and "side effect" when not. Svelte 5 splits these: $derived for cached values, $effect for side effects. The split is the whole point — you can now SEE which is which at the call site, and you can extract either into a normal function.
What it does:Svelte 5 uses plain HTML attribute names for DOM events: `onclick`, `oninput`, `onsubmit`, `onkeydown`. The Svelte 4 `on:` prefix still works in legacy mode but is deprecated for new code. Same handler signature, same event object — just the attribute name changed. Same reasoning as React: align with the DOM, lose one piece of framework-specific syntax.
What it does:Children passed to a component become a snippet named `children`. To render them, destructure children from $props and call {@render children()}. Named slots become named snippet props: `let { header, children } = $props()`. Snippets can take arguments — `{@render row(item)}` is the breakthrough — so they are strictly more powerful than slots.
export let → $props()
Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<script lang="ts">
export let title: string;
export let count = 0;
</script>
<!-- Svelte 5 -->
<script lang="ts">
let { title, count = 0 }: { title: string; count?: number } =
$props();
</script>
What it does:Svelte 4 declared props one per `export let` statement. Svelte 5 packs them into a single $props() destructure with a type annotation. Defaults move from `= 0` after the name to `= 0` in the destructure. Type-only props use the type annotation; runtime defaults still use destructure syntax. One line per component instead of one per prop.
What it does:In Svelte 5 you do not dispatch custom events anymore — you accept a callback function as a prop and call it. The callback signature IS the contract, so types flow without `CustomEvent<…>` gymnastics. The parent passes a normal handler `onSelect={(id) => …}` instead of `on:select={(e) => …e.detail}`. createEventDispatcher still works for backward compatibility but is deprecated.
// Svelte 4 (legacy)
import { beforeUpdate, afterUpdate } from 'svelte';
beforeUpdate(() => { /* before DOM */ });
afterUpdate(() => { /* after DOM */ });
// Svelte 5 (runes)
$effect.pre(() => { items; /* before DOM */ });
$effect(() => { items; /* after DOM */ });
What it does:Svelte 4 beforeUpdate/afterUpdate ran on EVERY update with no way to scope which change triggered them. Svelte 5 replaces them with $effect.pre (before DOM) and $effect (after DOM), and these only re-run when the specific reactive values you READ inside them change. Reference the values you care about so the effect tracks them.
Svelte 4 equivalent
import { beforeUpdate, afterUpdate } from 'svelte';
What it does:Svelte 4 exposed `$$props` (all props) and `$$restProps` (props you did not declare). Svelte 5 has neither — destructure named props and use `...rest` from $props() to capture the remainder, then spread it onto the element. The rest object is typed and reactive, and you can see exactly what is captured.
What it does:Svelte 4 used `$$slots.name` to check whether a named slot was provided. In Svelte 5 a slot is a snippet prop; check whether the parent passed it with a plain truthiness test (`{#if footer}`) before rendering. Declaring the snippet as optional (`footer?`) makes "not provided" the natural undefined case.
Svelte 4 equivalent
{#if $$slots.footer}<slot name="footer" />{/if}
svelte:component → just {expr}
Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<svelte:component this={Selected} {...props} />
<!-- Svelte 5: components are values, render directly -->
<script lang="ts">
import A from './A.svelte';
import B from './B.svelte';
let Selected = $state(A);
</script>
<Selected {...props} />
What it does:In Svelte 5 components are just values, so a variable holding a component renders directly as `<Selected />` — `<svelte:component>` is no longer needed for dynamic components (it still works but is deprecated). The tag name must start with a capital letter or contain a dot so the compiler treats it as a component, not an HTML element.
Svelte 4 equivalent
<svelte:component this={Selected} />
svelte:fragment → snippets remove the wrapper
Svelte 4 → 5
<!-- Svelte 4 (legacy) — fragment to avoid an extra <div> -->
<svelte:fragment slot="list">
<li>a</li>
<li>b</li>
</svelte:fragment>
<!-- Svelte 5 — a snippet can hold multiple elements directly -->
{#snippet list()}
<li>a</li>
<li>b</li>
{/snippet}
What it does:`<svelte:fragment>` existed to pass multiple elements into a named slot without an extra wrapper element. Snippets hold multiple top-level nodes natively, so the fragment tag is obsolete in Svelte 5. Define a snippet with several elements and render it where you want them.
<script lang="ts">
import { tick } from 'svelte';
let text = $state('');
let el: HTMLInputElement;
async function append(s: string) {
text += s;
await tick(); // wait for the DOM to flush
el.scrollLeft = el.scrollWidth; // now the width is up to date
}
</script>
What it does:tick() returns a promise that resolves once pending state changes have been applied to the DOM. It survives unchanged from Svelte 4 to 5 and pairs well with runes: mutate $state, `await tick()`, then read freshly-laid-out DOM. Use it instead of setTimeout(…, 0) when you need to act after Svelte has updated the page.
What it does:Add `lang="ts"` to the script tag and the whole component is type-checked. Import `Snippet<[args]>` from `svelte` to type snippet props. Combined with $props destructuring you get autocomplete on every prop the parent passes, and you catch typos at build time. Works the same in plain Svelte and SvelteKit projects.
What it does:Styles in a `<style>` block are scoped to the component — the compiler appends a hash class so `.card` here will not collide with `.card` in another component. Use `:global(…)` to escape the scope when you need to style something outside (body, third-party widgets). Unused selectors get a compile-time warning, which catches refactor leftovers.
class: directive — conditional classes
Component
<script lang="ts">
let active = $state(false);
let count = $state(0);
</script>
<button
class="btn"
class:active
class:has-count={count > 0}
onclick={() => { active = !active; count++; }}
>
{count}
</button>
What it does:class:name={cond} toggles a class based on a boolean. `class:active` is shorthand for `class:active={active}` when the variable matches the class. Multiple class: directives are independent and stack with the static `class="…"` attribute. Cleaner than templating `class={"btn " + (active ? "active" : "")}` and Svelte de-duplicates for you.
style: directive — inline styles
Component
<script lang="ts">
let x = $state(0);
let color = $state('tomato');
</script>
<div
style:transform={`translateX(${x}px)`}
style:color
style:--accent={color}
>
hello
</div>
What it does:style:prop={value} sets a single inline style declaratively, including CSS custom properties: `style:--accent={color}` exposes a variable that descendant CSS can read with `var(--accent)`. Shorthand `style:color` is `style:color={color}`. Cleaner than building a `style={…}` string and lets Svelte set each property efficiently without parsing.
What it does:Destructure individual props with defaults, type them with an interface, and use `...rest` to capture every other attribute. Spread `{...rest}` onto the underlying element so the consumer can pass `id`, `data-…`, `aria-…` without you enumerating each one. This is the wrapper-component recipe — one line of `...rest` saves dozens of prop forwardings.
children snippet — default content
Component
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
<div class="card">{@render children()}</div>
<!-- usage -->
<Card>
<h2>Hi</h2>
<p>Body text</p>
</Card>
What it does:When the parent writes `<Card>…</Card>`, everything between the tags becomes a snippet bound to a prop named `children`. Inside Card, destructure `children` from $props and call `{@render children()}` where the old `<slot />` would go. The Snippet type from `svelte` gives you proper type-checking on the rendered content.
What it does:The `generics` attribute on `<script lang="ts">` declares type parameters for the whole component, written exactly like a TypeScript generic clause (constraints with `extends` allowed). The parent gets full inference: pass `options: User[]` and `selected`, `label` are typed as `User`. This is how you build reusable Select/List/Table components without `any`.
What it does:Svelte 4 used `<svelte:self>` for recursion. In Svelte 5 a component can simply import its own file and reference itself by name — the same trick works for any framework. This renders nested tree structures, comment threads, nested menus. Guard the recursion with an `{#if}` on a terminating condition so it does not loop forever.
Svelte 4 equivalent
<svelte:self node={child} />
svelte:window: bind size, scroll, keys
Component
<script lang="ts">
let width = $state(0);
let scrollY = $state(0);
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') console.log('esc');
}
</script>
<svelte:window
bind:innerWidth={width}
bind:scrollY
onkeydown={onKey}
/>
<p>{width}px wide, scrolled {scrollY}px</p>
What it does:`<svelte:window>` attaches event listeners and two-way bindings to the global window without manual addEventListener. Bindable: innerWidth, innerHeight, scrollX, scrollY, online, devicePixelRatio. Events use the same `onkeydown`/`onresize` attribute form. Svelte adds and removes the listeners with the component, so there is nothing to clean up.
What it does:`<svelte:head>` injects elements into the document `<head>` from inside a component, and they update reactively as props change. During SSR these render into the served HTML, which is what search engines and social cards read. The standard place to set per-page `<title>`, meta description, Open Graph tags, and canonical links.
What it does:`<svelte:boundary>` (Svelte 5.3+) contains errors thrown while rendering or in effects inside it, so one broken widget does not blank the whole page. Provide a `failed` snippet to show a fallback — it receives the error and a `reset` function to retry. The `onerror` prop is the place to log to your error tracker. The React error-boundary analogue.
svelte:element: dynamic tag name
Component
<script lang="ts">
let { level = 2, children }:
{ level?: 1 | 2 | 3 | 4; children: import('svelte').Snippet } =
$props();
let tag = $derived(`h${level}` as const);
</script>
<svelte:element this={tag}>
{@render children()}
</svelte:element>
What it does:`<svelte:element this={tag}>` renders an HTML element whose tag name is decided at runtime from a string. Perfect for a heading component that picks h1–h4 by a `level` prop, or a polymorphic component that can be a `<div>`, `<section>`, or `<a>`. If `this` is nullish nothing renders. Void elements (img, br) cannot have children.
What it does:Svelte 5 event handlers are plain HTML attributes (`onclick`, `oninput`, `onsubmit`, `onkeydown`, …) accepting a function. The function gets the real DOM Event object with full TypeScript typing — `MouseEvent`, `KeyboardEvent`, `SubmitEvent`. No on: prefix, no `event.detail`, no synthetic event pool. Use arrow functions for one-liners and named handlers for anything reusable.
onkeydown — keyboard handler
Events
<script lang="ts">
let value = $state('');
function handle(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
console.log('submit', value);
}
if (e.key === 'Escape') value = '';
}
</script>
<textarea bind:value onkeydown={handle} />
What it does:Svelte 5 dropped the event modifier syntax (`on:keydown|preventDefault`) — you call `e.preventDefault()` / `e.stopPropagation()` inside the handler yourself. Test modifier keys with `e.shiftKey`, `e.metaKey`, `e.ctrlKey`, `e.altKey`. The trade-off is verbosity for explicitness: nothing happens by magic, you can SEE every effect in the handler body.
What it does:Without the on: modifier syntax, factor the pattern you used most into a tiny wrapper. `preventDefault(fn)` returns a handler that calls preventDefault then your function. Same for stopPropagation, once, self. One five-line utility brings back the Svelte 4 ergonomics without bringing back the framework-specific syntax.
What it does:In Svelte 5 the idiomatic way to surface "the user did something" is a callback prop. The component declares `onChange?` in $props and calls it; the parent passes a normal function. Pairs naturally with $bindable when you also expose the value. No createEventDispatcher, no string event names, no `e.detail` unpacking — just typed function calls.
What it does:Append `capture` to any DOM event attribute to attach during the capturing phase instead of bubbling. `onclickcapture`, `onkeydowncapture`, `onfocusincapture`. Useful for parent components that need to see the event BEFORE child handlers run (analytics, modal escape handlers, focus traps).
onsubmit: form handling without reload
Events
<script lang="ts">
let email = $state('');
function handle(e: SubmitEvent) {
e.preventDefault();
if (!email.includes('@')) return;
console.log('submit', email);
}
</script>
<form onsubmit={handle}>
<input type="email" bind:value={email} required />
<button type="submit">Send</button>
</form>
What it does:Attach a plain `onsubmit` handler to the form and call `e.preventDefault()` to stop the browser navigating. The event is a typed `SubmitEvent`. Keep `type="submit"` on the button and `required`/`type="email"` on inputs so native validation still fires before your handler runs. For full progressive enhancement in SvelteKit, prefer form actions + use:enhance.
oninput vs onchange: live vs commit
Events
<script lang="ts">
let live = $state('');
let committed = $state('');
</script>
<!-- fires on every keystroke -->
<input oninput={(e) => live = e.currentTarget.value} />
<!-- fires on blur / Enter for text inputs -->
<input onchange={(e) => committed = e.currentTarget.value} />
<p>live: {live} · committed: {committed}</p>
What it does:`oninput` fires on every keystroke as the user types — use it for live search, character counters, instant validation. `onchange` fires once the control commits its value (blur or Enter for text inputs, immediately for checkboxes/selects). Reach for input when you want every change, change when you only care about the settled value. `e.currentTarget` is correctly typed to the element.
event delegation: handle list clicks once
Events
<script lang="ts">
let items = $state(['a', 'b', 'c']);
function onListClick(e: MouseEvent) {
const btn = (e.target as HTMLElement).closest('button[data-id]');
if (btn) console.log('clicked', btn.getAttribute('data-id'));
}
</script>
<ul onclick={onListClick}>
{#each items as id}
<li><button data-id={id}>{id}</button></li>
{/each}
</ul>
What it does:Attach one handler on the container and use `e.target.closest(selector)` to find which child was clicked. Svelte 5 already delegates common events (click, input) internally for performance, but explicit delegation still helps when you have many rows and want a single handler reading `data-` attributes. The handler stays attached as the list grows or shrinks.
pointer events: unify mouse and touch
Events
<script lang="ts">
let dragging = $state(false);
let x = $state(0);
function down(e: PointerEvent) {
dragging = true;
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
}
function move(e: PointerEvent) { if (dragging) x = e.clientX; }
function up() { dragging = false; }
</script>
<div onpointerdown={down} onpointermove={move} onpointerup={up}
style:transform={`translateX(${x}px)`}>drag</div>
What it does:Pointer events (onpointerdown/move/up) cover mouse, touch, and pen with one code path, so you do not write parallel mouse and touch handlers. `setPointerCapture(e.pointerId)` routes all further events to your element even when the pointer leaves it — essential for drag handles and sliders. Each event carries `pointerType` ("mouse" | "touch" | "pen") if you need to branch.
What it does:A component can expose several typed callback props, each named for the action it reports (onIncrement, onDecrement). Call them with `?.()` so a parent that does not care can simply omit them. This replaces multiple createEventDispatcher event names with a discoverable, autocompleted prop list — the parent sees exactly which events exist.
Directive (14)
bind:value — two-way binding
Directive
<script lang="ts">
let name = $state('');
let agreed = $state(false);
let pick = $state('a');
</script>
<input bind:value={name} placeholder="name" />
<input type="checkbox" bind:checked={agreed} />
<select bind:value={pick}>
<option value="a">A</option>
<option value="b">B</option>
</select>
What it does:bind: creates a two-way data binding between a reactive variable and a form control. `bind:value` for text inputs and selects, `bind:checked` for checkboxes and radios, `bind:files` for file inputs, `bind:group` for radio/checkbox groups. Saves the manual `value={v} oninput={(e) => v = e.target.value}` boilerplate and handles type coercion (`<input type="number" bind:value={n}>` gives you a number).
What it does:bind:this={ref} writes the DOM element (or component instance) into the variable AFTER mount. Until then the variable is undefined. Combine with $effect to act when the ref becomes available — `$effect(() => inputEl?.focus())` autofocuses an input. The Svelte equivalent of React useRef or Vue template refs.
use:action — DOM lifecycle hook
Directive
<script lang="ts">
import type { Action } from 'svelte/action';
const clickOutside: Action<HTMLElement, () => void> = (node, callback) => {
function handle(e: MouseEvent) {
if (!node.contains(e.target as Node)) callback();
}
document.addEventListener('click', handle, true);
return { destroy() { document.removeEventListener('click', handle, true); } };
};
let open = $state(true);
</script>
{#if open}
<div use:clickOutside={() => open = false}>menu</div>
{/if}
What it does:An action is a function called when an element is mounted, receiving the element and an optional parameter. Return an object with `destroy()` for cleanup and `update(newParam)` for parameter changes. Perfect for tooltips, click-outside, intersection observers, third-party DOM libraries — anywhere you need to wire DOM behavior to an element lifecycle without writing a wrapper component.
transition:fade — enter & leave
Directive
<script lang="ts">
import { fade } from 'svelte/transition';
let show = $state(true);
</script>
<button onclick={() => show = !show}>toggle</button>
{#if show}
<div transition:fade={{ duration: 200 }}>hello</div>
{/if}
What it does:transition:fn applies the transition both on enter (when the element is added to the DOM) and on leave (when it is removed). Built-ins include fade, fly, slide, scale, blur, draw, crossfade. Parameters go in the directive expression. Use `in:` and `out:` for asymmetric animations (different fly direction on enter and leave).
in: / out: — separate enter and leave
Directive
<script lang="ts">
import { fly, fade } from 'svelte/transition';
let show = $state(true);
</script>
{#if show}
<div
in:fly={{ y: 20, duration: 200 }}
out:fade={{ duration: 150 }}
>panel</div>
{/if}
What it does:Use `in:` for enter-only animation and `out:` for leave-only. They can be different functions with different parameters. Common pattern: fly in from below, fade out in place — feels lively coming on and graceful going off. The transition is local by default; add `|global` to play it even when a parent block is toggling.
What it does:animate:fn runs when items in a KEYED {#each} block are reordered. The flip helper (First-Last-Invert-Play) measures positions before and after and tweens between them — you get smooth list reordering without computing CSS by hand. Requires a key expression `(item.id)` so Svelte can match identities across renders.
bind:group — radio / checkbox group
Directive
<script lang="ts">
let pick = $state<'a' | 'b' | 'c'>('a');
let tags = $state<string[]>([]);
</script>
<!-- radio: one value -->
<label><input type="radio" bind:group={pick} value="a" /> A</label>
<label><input type="radio" bind:group={pick} value="b" /> B</label>
<!-- checkbox: array of values -->
<label><input type="checkbox" bind:group={tags} value="svelte" /> svelte</label>
<label><input type="checkbox" bind:group={tags} value="kit" /> kit</label>
What it does:bind:group ties multiple form controls to a single reactive variable. With radios you get the value of the checked one; with checkboxes you get an array of every checked value. The variable type matches automatically — string for radios, string[] for checkbox groups. Saves the "loop over inputs and aggregate" boilerplate.
bind:value for <textarea> and number
Directive
<script lang="ts">
let note = $state('');
let age = $state(0);
let pick = $state('a');
</script>
<textarea bind:value={note}></textarea>
<!-- number input coerces to a number automatically -->
<input type="number" bind:value={age} min="0" />
<!-- range slider -->
<input type="range" bind:value={age} min="0" max="120" />
What it does:`bind:value` works on textarea and every input type. For `type="number"` and `type="range"` Svelte coerces the bound value to a number for you, so `age` is a real number, not a string — no parseInt needed. An empty number input binds to `null`. The same binding drives a range slider and a number box pointing at the same state.
bind: clientWidth / contentRect
Directive
<script lang="ts">
let w = $state(0);
let h = $state(0);
</script>
<div bind:clientWidth={w} bind:clientHeight={h} class="box">
{w} × {h}
</div>
What it does:Svelte offers read-only dimension bindings: `clientWidth`, `clientHeight`, `offsetWidth`, `offsetHeight`, `contentRect`, `contentBoxSize`. They update reactively as the element resizes (backed by a ResizeObserver), so layout-dependent logic re-runs without manual observers. They are one-way — you read the size, you cannot set it.
use:action with update()
Directive
<script lang="ts">
import type { Action } from 'svelte/action';
const tooltip: Action<HTMLElement, string> = (node, text) => {
node.title = text ?? '';
return {
update(next) { node.title = next ?? ''; }, // param changed
destroy() { node.removeAttribute('title'); },
};
};
let label = $state('hello');
</script>
<button use:tooltip={label}>hover me</button>
What it does:When an action receives a parameter that is a reactive value, Svelte calls the returned `update(newParam)` whenever it changes — you do not rebuild the action. Implement `update` to apply the new parameter (here, set a new title), and `destroy` to clean up on unmount. Type the action with `Action<ElementType, ParamType>` from `svelte/action`.
transition with custom CSS function
Directive
<script lang="ts">
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
function spin(node: Element, { duration = 400 } = {}): TransitionConfig {
return {
duration,
easing: cubicOut,
css: (t) => `transform: scale(${t}) rotate(${t * 360}deg); opacity: ${t}`,
};
}
let show = $state(true);
</script>
{#if show}<div transition:spin>hi</div>{/if}
What it does:A custom transition is a function returning `{ duration, easing, css }`. The `css(t, u)` callback receives `t` going 0→1 on enter (and 1→0 on leave) and returns a CSS string; Svelte generates a keyframe animation from it, so it runs off the main thread. Prefer the `css` form over `tick` for performance — only use `tick(t)` when an effect cannot be expressed in pure CSS.
crossfade: shared-element transitions
Directive
<script lang="ts">
import { crossfade } from 'svelte/transition';
const [send, receive] = crossfade({ duration: 300 });
let todos = $state([{ id: 1, t: 'a' }]);
let done = $state<{ id: number; t: string }[]>([]);
</script>
{#each todos as item (item.id)}
<li in:receive={{ key: item.id }} out:send={{ key: item.id }}>{item.t}</li>
{/each}
{#each done as item (item.id)}
<li in:receive={{ key: item.id }} out:send={{ key: item.id }}>{item.t}</li>
{/each}
What it does:crossfade() returns a `[send, receive]` pair. When an element leaves one list with `out:send={{ key }}` and another with the same key enters via `in:receive={{ key }}`, Svelte animates it flying from the old position to the new one. The classic use is a todo moving between "active" and "done" lists. Keys must match across the two lists for the morph to connect.
transition |global modifier
Directive
<script lang="ts">
import { fade } from 'svelte/transition';
let outer = $state(true);
let inner = $state(true);
</script>
{#if outer}
{#if inner}
<!-- plays only when `inner` itself toggles (local, default) -->
<p transition:fade>local</p>
<!-- plays even when `outer` is what mounted/unmounted -->
<p transition:fade|global>global</p>
{/if}
{/if}
What it does:By default transitions are LOCAL: they play only when the element itself is added or removed, not when a parent block toggles. Add `|global` to make a transition play even when an ancestor block is what mounted or unmounted it. The default flipped between Svelte 3 and 4 — in Svelte 5 local is the default, so reach for `|global` deliberately when you want page-level enter animations.
bind:open for <details>
Directive
<script lang="ts">
let open = $state(false);
</script>
<button onclick={() => open = !open}>toggle from outside</button>
<details bind:open>
<summary>More info</summary>
<p>Content shown when open is {String(open)}</p>
</details>
What it does:`bind:open` two-way binds the native `<details>` disclosure state to a reactive variable, so you can read whether it is expanded and toggle it programmatically from elsewhere on the page. The element stays fully accessible and keyboard-operable because it is the real `<details>`, not a div reimplementation.
Control Flow (10)
{#if} {:else if} {:else}
Control Flow
<script lang="ts">
let status = $state<'loading' | 'ok' | 'error'>('loading');
</script>
{#if status === 'loading'}
<p>Loading…</p>
{:else if status === 'error'}
<p class="err">Failed</p>
{:else}
<p>Ready</p>
{/if}
What it does:Conditional rendering. The block is mounted / unmounted as the condition flips — no virtual DOM diff, the elements really come and go. Pair with `transition:` for enter/leave animations. For pure visibility toggles where you want to KEEP the DOM (preserve scroll, video playback, focus), apply a CSS class instead of #if.
What it does:List rendering. The optional `(item.id)` is the KEY — Svelte uses it to match items across renders, so reorders move existing DOM nodes instead of destroying and recreating them. The `, i` second binding gives you the index. The `{:else}` clause renders when the list is empty. Without a key, animations and form-state preservation break on reorder.
What it does:Render different content while a promise is pending, fulfilled, or rejected — no state machine to wire by hand. Re-assigning the promise re-runs the block from the pending state. Shorthand `{#await promise then data}` skips the loading state when you already know it resolved fast (cached).
What it does:{#key value} unmounts and remounts the inner content whenever the value changes. Useful when a child component holds internal state (form draft, animation) that should reset on context switch, or when a third-party widget does not handle prop changes well. The Svelte equivalent of React `key={…}` on a component.
{@html} — raw HTML (XSS warning)
Control Flow
<script lang="ts">
import DOMPurify from 'dompurify';
let untrusted = $state('<p>hi <img src=x onerror="alert(1)"></p>');
let safe = $derived(DOMPurify.sanitize(untrusted));
</script>
<!-- NEVER do this with user input: -->
<!-- {@html untrusted} -->
<!-- SAFE: sanitize first -->
{@html safe}
What it does:{@html expr} interpolates the value as RAW HTML — no escaping. If `expr` contains anything that came from a user, an API, or any external source, you have an XSS hole. Sanitize first with DOMPurify (or similar) before passing to {@html}. Safe uses: rendering trusted markdown you compiled at build time, server-rendered HTML you control end to end.
{#each} destructuring and index
Control Flow
<script lang="ts">
let pairs = $state<[string, number][]>([['a', 1], ['b', 2]]);
let users = $state([{ id: 1, name: 'Lei', role: 'admin' }]);
</script>
{#each pairs as [name, value], i}
<li>{i}: {name} = {value}</li>
{/each}
{#each users as { id, name, role } (id)}
<li>{name} ({role})</li>
{/each}
What it does:The each-item binding accepts full destructuring — array patterns `[a, b]` and object patterns `{ id, name }`, with an optional `, i` index after them and a `(key)` after that. Destructure to pull exactly the fields you render. Keep the `(key)` even when destructuring; it still drives identity matching for reorders and animations.
{#each n} over a count
Control Flow
<script lang="ts">
let rating = $state(3);
</script>
<!-- render `rating` filled stars out of 5 -->
{#each { length: 5 }, i}
<span class:filled={i < rating}>★</span>
{/each}
What it does:Svelte 5 lets `{#each}` iterate an array-like with a `length` property, so `{#each { length: 5 }, i}` runs five times with `i` going 0..4 — no need to build a throwaway `Array.from({ length: 5 })`. Handy for star ratings, pagination dots, fixed grids. The `i` index is the value you actually use.
{#await} shorthand: then-only
Control Flow
<script lang="ts">
let ready = $state<Promise<string>>(Promise.resolve('cached'));
</script>
<!-- no pending block: assumes it resolves fast -->
{#await ready then value}
<p>{value}</p>
{/await}
<!-- catch-only: ignore the value, only show errors -->
{#await ready catch err}
<p class="err">{err.message}</p>
{/await}
What it does:When a promise is already resolved or you do not need a loading state, use `{#await promise then value}` to skip the pending block. The mirror form `{#await promise catch err}` renders only on rejection. Both are syntactic sugar over the full three-branch form — reach for them to cut visual noise when one branch is irrelevant.
What it does:{@const name = expr} declares a local constant scoped to the current block (#each, #if, #snippet, #await branch). Use it to compute a value once and reuse it across the markup of that iteration instead of repeating the expression or polluting the script with a derived for every row. It can only appear as a direct child of a block, not at the top level of the template.
{@debug}: pause and log values
Control Flow
<script lang="ts">
let user = $state({ name: 'Lei', score: 0 });
</script>
<!-- with devtools open, execution pauses here when `user` changes -->
{@debug user}
<p>{user.name}: {user.score}</p>
What it does:{@debug var1, var2} logs the listed values whenever they change and, if devtools are open, hits a `debugger` breakpoint so you can inspect them in context. Bare `{@debug}` with no arguments breaks on every state change. A focused alternative to scattering console.log in the template — remove it before shipping.
What it does:A snippet is a reusable chunk of markup defined inside the template. Like a function, it can take typed arguments. Render with {@render name(args)} as many times as you need. Snippets see variables from the surrounding scope (lexical), so they are great for extracting repeated markup without prop-drilling a wrapper component. They are STRICTLY local — pass as props to share across components.
What it does:{@render snippet(...args)} executes a snippet at that position in the markup. Snippet props are typed with `Snippet<[arg1, arg2]>` from `svelte`. Guard optional snippets with #if before rendering. This pattern is the new way to write headless / wrapper components: each "slot" is a snippet prop, and the parent decides the markup.
What it does:The defining feature of snippets vs slots: they can take ARGUMENTS. A List component renders `{@render row(item, i)}` per item; the parent passes its own snippet defining how each row looks. The parent now owns the markup AND benefits from the iteration logic — true headless composition. Combine with `generics="T"` for type-safe item handlers.
What it does:The component body — everything between `<Modal>…</Modal>` — automatically becomes a snippet bound to the prop `children`. Render it with `{@render children()}` where the content should appear. This is the direct replacement for `<slot />`. Named slots become named snippet props: `<Modal>{#snippet header()}…{/snippet}…</Modal>`.
What it does:A snippet can call itself, which makes recursive structures (trees, nested comments, file explorers) trivial without a separate recursive component. Render the node, then for each child `{@render branch(child)}` again. Guard with `{#if node.children}` so the recursion terminates at leaves. The snippet sees its lexical scope, so helpers defined in `<script>` are in reach.
What it does:Optional snippet props give you slot defaults: render the passed snippet when present, otherwise fall back to default markup in the `{:else}`. The parent overrides just the parts it cares about. This is how you build components with sensible defaults that stay fully customizable — the pattern behind most headless UI kits.
pass data UP via snippet args
Snippets
<!-- Resource.svelte (render-prop style) -->
<script lang="ts">
import type { Snippet } from 'svelte';
let { url, children }:
{ url: string; children: Snippet<[{ loading: boolean; data: unknown }]> } =
$props();
let loading = $state(true);
let data = $state<unknown>(null);
$effect(() => {
loading = true;
fetch(url).then(r => r.json()).then(d => { data = d; loading = false; });
});
</script>
{@render children({ loading, data })}
What it does:Because snippets take arguments, a component can pass internal state OUT to the parent through `{@render children(payload)}` — the render-prop pattern. Here a data-fetching component exposes `{ loading, data }` and lets the parent decide how to display each state. The parent owns presentation, the component owns logic. Clean inversion of control without prop-drilling callbacks.
What it does:A snippet declared at the top level of a `.svelte` file can be exported from a `<script module>` block and imported into other components. This is how you share markup chunks (icon sets, formatted badges) across files. Note `<script module>` is the Svelte 5 spelling of the old `<script context="module">`.
What it does:writable creates a store with `set(v)`, `update(fn)`, and `subscribe(cb)`. Inside a `.svelte` file, prefix with `$` to auto-subscribe and unsubscribe — `$count` reads the current value reactively. Stores are still the right primitive for app-global state shared across unrelated components (auth, theme), even in Svelte 5 where you have $state for everything local.
readable — derived from external source
Stores
import { readable } from 'svelte/store';
export const now = readable(new Date(), (set) => {
const id = setInterval(() => set(new Date()), 1000);
return () => clearInterval(id);
});
// usage
<script>
import { now } from './stores/now';
</script>
<p>{$now.toLocaleTimeString()}</p>
What it does:readable creates a store whose value comes from an external source — a timer, a WebSocket, a Subscription. The start function runs on first subscribe and returns a stop function called when the last subscriber leaves. Perfect for "expensive to maintain" sources that should only run while something is watching.
What it does:derived takes one store or an array of stores and a function that computes a value from their current contents. The result re-computes whenever any input store changes and is itself a store you subscribe to. Supports async derivations: pass `set` as the second callback argument and call `set(v)` from inside async work.
get — read a store value once
Stores
import { get } from 'svelte/store';
import { user } from './stores/user';
export async function checkout() {
const current = get(user); // synchronous, no subscription
await fetch('/checkout', {
method: 'POST',
body: JSON.stringify({ userId: current.id }),
});
}
What it does:get(store) reads the current value synchronously WITHOUT subscribing — Svelte subscribes, reads, unsubscribes for you. Use it in event handlers, fetch payloads, anywhere you need the value but do not want a reactive dependency. Inside `.svelte` files prefer `$store` for reactive reads; reach for get only when you specifically do not want reactivity.
$store auto-subscription
Stores
<script lang="ts">
import { user } from './stores/user';
// `user` is a writable<User | null>
// Read & write reactively — no subscribe/unsubscribe code
function logout() { $user = null; }
</script>
{#if $user}
<p>Hi {$user.name}</p>
<button onclick={logout}>logout</button>
{/if}
What it does:Inside `.svelte` files only, prefix any store with `$` to read its current value reactively. The compiler inserts subscribe + unsubscribe automatically when the component mounts / unmounts. Writing `$store = v` calls `store.set(v)`. This is the single biggest ergonomic win of Svelte stores — no boilerplate, no `useStore()` hook.
What it does:A custom store is any object that exposes `subscribe(cb)` — Svelte does not care how you built it. Wrap a writable, expose only the methods you want, hide the raw set/update. This is how you build domain stores that look like services (`counter.inc()` instead of `counter.update(n => n + 1)`) while keeping the `$counter` reactive read syntax.
store vs $state: when to pick which
Stores
// app-global, cross-route, lives in a .ts file → store
// stores/theme.ts
import { writable } from 'svelte/store';
export const theme = writable<'light' | 'dark'>('dark');
// local component state, or .svelte.ts module → $state
// Counter.svelte
<script lang="ts">
let count = $state(0); // not shared, just this component
</script>
What it does:Rule of thumb in Svelte 5: use $state for state owned by a component or a `.svelte.ts` module; use stores when you need a value in plain `.ts`/`.js` files, or a subscribe-based contract that third-party code expects. Stores are not deprecated — they remain the interop primitive. Do not convert working stores to runes just to be trendy.
derived store with set (async)
Stores
import { writable, derived } from 'svelte/store';
export const query = writable('');
// async derived: second arg is `set`, return a cleanup
export const results = derived(
query,
($query, set) => {
const ctrl = new AbortController();
fetch(`/api/search?q=${$query}`, { signal: ctrl.signal })
.then((r) => r.json())
.then(set);
return () => ctrl.abort();
},
[] as string[], // initial value while first fetch is pending
);
What it does:When a derived callback takes a second `set` argument it becomes asynchronous: the new value is whatever you pass to `set()`, possibly later. Return a cleanup function that runs before the next recomputation (abort the in-flight fetch). The third argument to derived is the initial value shown until the first set fires. This is debounced-search territory.
What it does:Wrap writable to read an initial value from localStorage and write back on every change via subscribe. Guard `localStorage` with a typeof check so the module does not crash during SSR, where it does not exist. The returned object is a normal store — `$settings.dark` reads and `$settings = …` writes, and the disk stays in sync automatically.
context API: scoped dependency injection
Stores
// Parent.svelte
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
const theme = writable('dark');
setContext('theme', theme); // available to all descendants
</script>
// DeepChild.svelte
<script lang="ts">
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
const theme = getContext<Writable<string>>('theme');
</script>
<p class={$theme}>themed</p>
What it does:setContext(key, value) makes a value available to every descendant component via getContext(key), without passing props through each level. It is scoped to the component subtree and resolved at component init, so it is not reactive on its own — pass a store (or a $state-backed object) as the value when descendants need to react to changes. Perfect for themes, form state, and i18n.
store.update with previous value
Stores
import { writable } from 'svelte/store';
export const cart = writable<{ id: string; qty: number }[]>([]);
export function addItem(id: string) {
cart.update((items) => {
const found = items.find((i) => i.id === id);
if (found) return items.map((i) =>
i.id === id ? { ...i, qty: i.qty + 1 } : i);
return [...items, { id, qty: 1 }];
});
}
What it does:store.update(fn) gives you the current value and expects you to return the next one — the right tool when the new value depends on the old (counters, toggles, list edits). Return a NEW array/object rather than mutating the argument so the store notifies subscribers. Use set(v) instead when the new value is independent of the previous one.
SvelteKit (15)
+layout.server.ts: shared layout data
SvelteKit
// src/routes/(app)/+layout.server.ts
import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: LayoutServerLoad = async ({ locals, url }) => {
if (!locals.user) throw redirect(302, `/login?from=${url.pathname}`);
return { user: locals.user };
};
// any +page.svelte under this layout:
// let { data } = $props(); → data.user is available
What it does:A `+layout.server.ts` load runs for every page under that layout and its data merges into each page's `data`. Use it for things every nested page needs — the signed-in user, navigation, feature flags — and to guard a whole section behind auth with a single redirect. Child page loads run in parallel with the layout load, not after it.
load: depends + invalidate
SvelteKit
// +page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, depends }) => {
depends('app:feed'); // custom invalidation key
const res = await fetch('/api/feed');
return { items: await res.json() };
};
// elsewhere, after posting:
import { invalidate } from '$app/navigation';
await invalidate('app:feed'); // re-runs the load above
What it does:`depends(id)` registers a custom dependency on a load function. Calling `invalidate(id)` later re-runs every load that declared that id, without changing the URL. Use a namespaced string like `app:feed` for dependencies that are not a real fetched URL. This is how you refresh exactly the right loads after a mutation instead of `invalidateAll()`.
What it does:`use:enhance` from `$app/forms` upgrades a real `<form>` so submitting it does not reload the page: SvelteKit posts the data, applies the action result, and invalidates loads — all client-side. The callback runs before submit (set a pending flag); the returned function runs after, where `update()` applies the result. Without JS the form still works as a plain POST.
What it does:A page can expose several named actions in its `actions` export; the form chooses one with `action="?/name"`. Use `default` for the single-action case, named actions when one page handles multiple operations (create, update, delete). Each receives the same `{ request, locals, cookies }` and can `fail(status, data)` or `redirect`. A `button formaction="?/name"` lets two buttons target two actions in one form.
$app/state: page rune (replaces stores)
SvelteKit
<script lang="ts">
// SvelteKit 2.12+ — runes instead of $app/stores
import { page } from '$app/state';
</script>
<!-- no $ prefix; `page` is a reactive object -->
<h1>{page.url.pathname}</h1>
<p>route id: {page.route.id}</p>
{#if page.error}<p class="err">{page.error.message}</p>{/if}
What it does:`$app/state` (SvelteKit 2.12+) exposes `page`, `navigating`, and `updated` as runes-powered reactive objects — read `page.url`, `page.params`, `page.data` directly with NO `$` prefix. It supersedes `$app/stores` for new code in runes mode. The store version still works for backward compatibility, but prefer `$app/state` going forward.
error and not-found pages
SvelteKit
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/state';
</script>
<h1>{page.status}</h1>
<p>{page.error?.message ?? 'Something went wrong'}</p>
<a href="/">Go home</a>
<!-- thrown from a load: -->
// import { error } from '@sveltejs/kit';
// throw error(404, 'Post not found');
What it does:A `+error.svelte` renders when a `load` throws or a route is not found; read `page.status` and `page.error` to show the right message. Place it at the route level you want to catch — a top-level `+error.svelte` is the catch-all 404/500 page, a nested one handles errors only within its subtree. Throw `error(status, message)` from `@sveltejs/kit` to trigger it.
+server.ts: JSON API endpoints
SvelteKit
// src/routes/api/todos/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const tag = url.searchParams.get('tag');
const todos = await db.todos.where({ tag });
return json(todos);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
if (!body.title) throw error(400, 'title required');
return json(await db.todos.create(body), { status: 201 });
};
What it does:A `+server.ts` file defines an HTTP endpoint by exporting functions named after methods: GET, POST, PUT, PATCH, DELETE. Return a `Response` — the `json(data, init)` helper from `@sveltejs/kit` builds a JSON one. These run only on the server and are how you expose a REST/JSON API from the same SvelteKit app your pages live in.
What it does:The `handle` hook in `hooks.server.ts` wraps every request: read cookies/headers, populate `event.locals` (then available to all loads and actions), call `resolve(event)` to run the route, and post-process the response (security headers, logging). Compose multiple hooks with `sequence()` from `@sveltejs/kit/hooks`. This is SvelteKit's server middleware layer.
page options: prerender / ssr / csr
SvelteKit
// +page.ts or +page.server.ts or +layout.ts
export const prerender = true; // build to static HTML at build time
export const ssr = true; // server-render on first load (default)
export const csr = true; // hydrate + client-side nav (default)
// a fully static marketing page:
// prerender = true, ssr = true, csr = false → zero JS shipped
What it does:Export these constants from a `+page`/`+layout` file to control rendering per route. `prerender = true` bakes the page to static HTML at build time (great for docs, blogs). `ssr = false` makes it client-only (an SPA island). `csr = false` ships no client JS, yielding a fully static page. They cascade through layouts, and you tune each route independently.
What it does:The `cookies` API on server `event` reads and writes cookies safely: `cookies.get(name)`, `cookies.set(name, value, opts)`, `cookies.delete(name, opts)`. Always pass `path` (usually "/") and set `httpOnly`, `secure`, `sameSite` for session cookies so they are not readable by JS and not sent cross-site. SvelteKit attaches the Set-Cookie header to the response for you.
+page.server.ts — server load
SvelteKit
// src/routes/posts/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, locals }) => {
const post = await locals.db.posts.findUnique({
where: { slug: params.slug },
});
if (!post) throw error(404, 'not found');
return { post };
};
// +page.svelte
<script lang="ts">
let { data } = $props();
</script>
<h1>{data.post.title}</h1>
What it does:A `+page.server.ts` `load` runs ONLY on the server — perfect for DB queries, secrets, private APIs. The returned object becomes the `data` prop of the page component. SvelteKit generates `$types` for full type safety end to end. Pair with `+page.ts` (runs on server during SSR, then on client during navigation) when you need the same load on both sides.
What it does:Form actions handle POST submissions on the server, returning JSON or redirecting. The form works WITHOUT JS — a real `<form method="POST">` is the baseline. Add `use:enhance` from `$app/forms` for client-side enhancement: no full reload, automatic data invalidation, optimistic UI hooks. Progressive enhancement done right.
+page.ts — universal load
SvelteKit
// src/routes/feed/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, url }) => {
const tag = url.searchParams.get('tag') ?? 'all';
const res = await fetch(`/api/feed?tag=${tag}`);
if (!res.ok) throw new Error('feed failed');
const items = await res.json();
return { items, tag };
};
What it does:A `+page.ts` load runs on the server during SSR and then on the client during navigation. Use it when the load can run anywhere — calls to your own `/api/*` routes, public APIs, content the client could fetch directly. Use `+page.server.ts` instead when the load needs server-only access (DB, env, sessions). The two can coexist; results are merged.
$app/stores — page, navigating, updated
SvelteKit
<script lang="ts">
import { page, navigating } from '$app/stores';
</script>
<h1>You are at {$page.url.pathname}</h1>
<p>params: {JSON.stringify($page.params)}</p>
{#if $navigating}
<p>Loading {$navigating.to?.url.pathname}…</p>
{/if}
What it does:SvelteKit exposes built-in stores from `$app/stores`. `page` holds the current url, params, data, route info — useful for breadcrumbs and active-link highlighting. `navigating` is non-null during a route change (the `to` and `from` URLs) — show a loading bar based on it. `updated` flips to true when a new app version is deployed so you can prompt a refresh.
goto / invalidate — programmatic navigation
SvelteKit
<script lang="ts">
import { goto, invalidate, invalidateAll } from '$app/navigation';
async function refresh() {
await invalidate('/api/feed'); // re-run loads that fetched this URL
}
async function reset() {
await invalidateAll(); // re-run every load on the page
await goto('/?tab=home', { replaceState: true });
}
</script>
What it does:`goto(url, opts)` navigates programmatically — same as a `<a href>` click but from JS. `invalidate(urlOrFn)` tells SvelteKit "any load that depended on this is now stale, re-run it" without changing URL. `invalidateAll()` re-runs every load on the current page. Use invalidate after mutations so the UI reflects fresh data without a full reload.
Pitfall (13)
pitfall: destructured props lose reactivity
Pitfall
<script lang="ts">
let { count } = $props();
// BAD — `double` is computed once, never updates
// let double = count * 2;
// GOOD — derive so it tracks `count`
let double = $derived(count * 2);
</script>
<p>{count} → {double}</p>
What it does:Destructuring `{ count } = $props()` keeps `count` reactive in the template, but if you compute a plain `let double = count * 2`, that runs once and never updates — you captured the value, not the reactive source. Wrap derivations in $derived so they re-run when the prop changes. The same trap hits any `let x = someReactive` snapshot.
pitfall: reactive value in closure goes stale
Pitfall
<script lang="ts">
let count = $state(0);
// BAD — captures count=0 forever
// setTimeout(() => console.log(count), 0) at module level
// GOOD — read count when the callback actually runs
function later() {
setTimeout(() => console.log(count), 1000); // reads current value
}
</script>
<button onclick={() => count++}>{count}</button>
<button onclick={later}>log in 1s</button>
What it does:Reading a $state value inside a callback reads it AT CALL TIME, which is usually what you want. The trap is capturing the value into a variable once (at module load or in a stale closure) and reusing that snapshot — it never updates. Read the reactive variable as late as possible, inside the function that runs when you need the value, not into a const earlier.
pitfall: missing key recreates DOM on reorder
Pitfall
<script lang="ts">
let items = $state([{ id: 1, t: 'a' }, { id: 2, t: 'b' }]);
</script>
<!-- BAD — no key: inputs lose focus/value when list reorders -->
{#each items as item}
<input value={item.t} />
{/each}
<!-- GOOD — keyed by stable id: DOM nodes move, state survives -->
{#each items as item (item.id)}
<input value={item.t} />
{/each}
What it does:Without a `(key)`, Svelte matches each-block items by index, so reordering or removing an item shuffles which DOM node holds which data — inputs lose focus, transitions break, component state attaches to the wrong row. Always key with a STABLE unique id (not the array index, which defeats the purpose). The key is how Svelte preserves identity across renders.
pitfall: $effect for derived state
Pitfall
<script lang="ts">
let first = $state('Li');
let last = $state('Lei');
// BAD — extra state, runs after render, can flash stale
// let full = $state('');
// $effect(() => { full = first + ' ' + last; });
// GOOD — derived computes synchronously, no extra state
let full = $derived(`${first} ${last}`);
</script>
<p>{full}</p>
What it does:A common anti-pattern is using $effect to write one piece of state from others. Use $derived instead: it computes synchronously before render (no stale flash), needs no extra $state, and cannot cause the write-after-read loops effects can. Rule: if you are setting a variable from other reactive values, you want $derived, not $effect. Effects are for OUT-of-graph work (DOM, network, storage).
pitfall: legacy and runes mode mixing
Pitfall
<script lang="ts">
// Once you use ANY rune, the component is in runes mode.
let count = $state(0);
// BAD — $: is legacy reactivity, illegal in runes mode
// $: doubled = count * 2; // compile error
// GOOD — use the rune equivalent
let doubled = $derived(count * 2);
</script>
What it does:A component is in runes mode the moment it uses any rune, and in runes mode the legacy `$:` reactive statements and auto-reactive top-level `let` no longer work — mixing them is a compile error. Migrate the whole component at once: every `$:` becomes $derived or $effect, every reactive `let` becomes $state. You cannot half-convert a single file.
pitfall: derived must be pure
Pitfall
<script lang="ts">
let items = $state([3, 1, 2]);
// BAD — sort() mutates in place, side effect inside derived
// let sorted = $derived(items.sort());
// GOOD — copy first, then sort the copy
let sorted = $derived([...items].sort((a, b) => a - b));
</script>
<p>{sorted.join(', ')}</p>
What it does:A $derived expression must be pure: it should compute a value, not mutate anything. `items.sort()` sorts in place, mutating the source array as a side effect — wrong here and a source of subtle bugs. Copy first (`[...items]`, `structuredClone`) then transform the copy. Same caution for `.reverse()` and `.splice()`, which also mutate.
pitfall: each-block index as fallback only
Pitfall
<script lang="ts">
let rows = $state<string[]>(['a', 'b', 'c']);
</script>
<!-- the `, i` index is fine to DISPLAY -->
{#each rows as row, i (row)}
<li>{i + 1}. {row}</li>
{/each}
<!-- but never key BY the index: (i) defeats identity tracking -->
<!-- {#each rows as row, i (i)} ← bad -->
What it does:The second binding `, i` gives you the index for display (row numbers, "first"/"last" logic) and is perfectly fine to use. The mistake is keying BY the index — `(i)` — which makes the key change exactly when items move, so Svelte cannot track identity and you get the same DOM-recreation bugs as having no key. Key by stable data, display with the index.
pitfall: $state object — mutate or reassign
Pitfall
let user = $state({ name: 'Lei', tags: ['a'] });
// BOTH WORK — $state returns a deep Proxy:
user.name = 'Hong'; // tracked
user.tags.push('b'); // tracked
user = { name: 'X', tags: [] }; // tracked (reassignment)
// BUT $state.raw is shallow — only reassignment triggers:
let big = $state.raw({ items: [] });
big.items.push(1); // NOT tracked
big = { items: [...big.items, 1] }; // tracked
What it does:Plain $state proxies deeply, so both mutation (`user.name = "…"`) and reassignment (`user = {…}`) trigger updates. $state.raw is shallow — only reassignment works. Most "my state did not update" bugs in Svelte 5 are an accidental $state.raw, or capturing a primitive into a local (which is a copy, not a binding). Default to plain $state; reach for .raw only for huge frozen-shape data.
pitfall: runes only at top level
Pitfall
// WRONG — runes cannot be inside if / for / try
if (someCondition) {
let count = $state(0); // compile error
}
for (let i = 0; i < 3; i++) {
let item = $state(0); // compile error
}
// RIGHT — declare at top, gate the USE
let count = $state(0);
if (someCondition) {
count++; // fine
}
// For per-item state, use an object array:
let items = $state(initial.map(x => ({ value: x, selected: false })));
What it does:Runes are compile-time transformations, not runtime function calls — they can only appear at the top level of a `<script>` or `.svelte.ts` module, not inside conditionals, loops, or try/catch. The fix is to declare the rune at the top and gate USES of the variable, or move per-item state into an array of objects whose properties are reactive via the proxy.
pitfall: $effect infinite loop
Pitfall
let count = $state(0);
// BAD — reads count, then writes count → re-runs → infinite loop
$effect(() => {
count = count + 1;
});
// GOOD — write inside an event handler, not an effect
function inc() { count++; }
// GOOD — use untrack() to read without subscribing
import { untrack } from 'svelte';
$effect(() => {
const c = untrack(() => count);
console.log('component re-rendered, count was', c);
});
What it does:If an $effect reads a reactive value AND writes the same (or any other reactive) value it depends on, you get an infinite loop. Two fixes: (1) move the write into an event handler — effects are for synchronizing OUT (to DOM, network, localStorage), not for state transitions; (2) use `untrack(() => …)` to read without subscribing when you genuinely need the current value but should not re-run on its change.
pitfall: class binding conflict
Pitfall
<!-- BAD — both class= and class: are present, behavior is confusing -->
<div class={dynamic} class:active>...</div>
<!-- BETTER — pick ONE style: -->
<!-- option A: pure class:* directives -->
<div
class="base"
class:active
class:large={size === 'lg'}
></div>
<!-- option B: build the string yourself -->
<div class="base {active ? 'active' : ''} {size === 'lg' ? 'large' : ''}"></div>
What it does:Mixing a dynamic `class={…}` expression with `class:foo` directives works, but the order of precedence has bitten people: the static `class="…"` is merged, but `class={dynamic}` REPLACES the static class. Pick one style per element: all-directives is the cleanest in Svelte 5. If you need both, put `class:*` AFTER `class={…}` so the toggles win.
pitfall: SSR vs client-only code
Pitfall
<script lang="ts">
import { browser } from '$app/environment';
// BAD — window does not exist on the server
// const w = window.innerWidth;
// GOOD option 1: guard with browser flag
let w = $state(0);
$effect(() => {
if (browser) w = window.innerWidth;
});
// GOOD option 2: read in onMount / $effect (only runs on client)
$effect(() => {
const onResize = () => w = window.innerWidth;
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
});
</script>
What it does:SvelteKit components run on BOTH server (during SSR) and client. `window`, `document`, `localStorage` only exist on the client — reading them at module top level crashes SSR. Fix: read them inside $effect (only runs on the client) or guard with `import { browser } from "$app/environment"`. The `onMount` lifecycle from `svelte` is also client-only and is fine to use.
pitfall: $ prefix only inside .svelte
Pitfall
// stores/count.ts
import { writable, get } from 'svelte/store';
export const count = writable(0);
// WRONG — `$count` is invalid in a plain .ts file
// export function double() { return $count * 2; }
// RIGHT — use get(), or subscribe manually
export function double() { return get(count) * 2; }
// In Counter.svelte:
<script>
import { count } from './stores/count';
</script>
<p>{$count}</p> <!-- works -->
What it does:The `$store` auto-subscribe magic is a compiler transform that only runs in `.svelte` and `.svelte.ts` files. Inside a plain `.ts` or `.js` module the `$` syntax does not exist — use `get(store)` for a one-shot read or `store.subscribe(cb)` for ongoing reads. Mirror move: $state outside a `.svelte` / `.svelte.ts` file also fails to compile. The file extension matters.
What this tool does
A searchable Svelte 5 cheat sheet, 60+ real snippets across ten
categories. Runes — the new reactivity system: $state for
reactive variables, $derived and $derived.by for cached
computations, $effect and $effect.pre for side effects with
automatic dependency tracking, $props and $bindable for typed
component inputs, $inspect for reactive debugging, $host for
custom-element internals. Svelte 4 → 5 migration: `let` →
`$state`, `$:` reactive blocks → `$derived` / `$effect`,
`on:click` → `onclick`, `<slot />` → `{@render children()}`,
`export let` → `$props()`. Component anatomy: `<script lang="ts">`,
scoped `<style>`, prop destructuring with defaults and rest,
class: and style: directives. Events: native DOM handlers,
keyboard modifiers, custom events through callback props
(the Svelte 5 way, replacing `createEventDispatcher`).
Directives: `bind:value` two-way binding, `bind:this` element
refs, `use:action` for DOM lifecycle hooks, `transition:fade`,
`in:fly` / `out:slide` enter/leave animations,
`animate:flip` for list reordering. Control flow: `{#if}`,
`{#each}` with keyed `(item.id)`, `{#await}` for promises,
`{#key}` to force remount. Snippets (the new slot system):
`{#snippet}` definition, `{@render}` invocation, snippet
props for headless components. Stores (still useful in 5):
`writable`, `readable`, `derived`, `get`, auto-subscription
with the `$` prefix. SvelteKit: the `load` function in
`+page.server.ts` and `+page.ts`, form actions, page data,
`$app/stores` and `$app/navigation`. Common pitfalls: deep
object reactivity (mutate vs reassign), class binding
conflicts, SSR vs client-only code, store auto-subscription
gotchas, runes only at top level (no conditionals or loops).
Every entry has bilingual EN/ZH descriptions written
separately (no machine translation), copyable code, and
where it applies a Svelte 4 equivalent so people upgrading
see both forms side by side. Pure client-side, no upload.
Tool details
Input
Text + Numbers
The page exposes text boxes, numeric controls, file pickers, or structured inputs depending on the tool.
Output
Live result + Copy + Preview
The result area focuses on usable output, with copy, download, or preview actions when supported.
Privacy
May use a live lookup
A network call is detected in the component, so redact sensitive data when appropriate.
Save / share
Local preference storage
Preferences, history, or drafts are saved in this browser without an account.
Performance budget
Initial JS <= 28 KB
No WASM budget is declared, keeping the tool quick to open on mobile.
Best fit
Developer & DevOps · Developer
Category and role tags drive related tools, internal links, and quick fit checks.
How to use
1
1. Input
Paste or drop your content into the tool panel.
2
2. Process
Click the button. All processing is local in your browser.
3
3. Copy / Download
Copy the result or download to disk in one click.
How Svelte Cheatsheet fits into your work
Use it in the small gaps between coding, reviewing, debugging, and shipping.
Developer jobs
Formatting, validating, shrinking, or inspecting code-adjacent text.
Preparing snippets for documentation, tickets, commits, or handoff.
Checking a small payload quickly without switching tools.
Developer checks
Run irreversible transforms like minify or obfuscate on a copy.
Keep secrets out of pasted snippets unless the tool explicitly stays local.
Use your normal tests or linter before shipping transformed code.
Good next steps
These links move the current task into a more complete workflow.
Migrating a Svelte 4 component to runes during a real upgrade
You inherit a 200-line component using `export let`, `$:` blocks,
and `createEventDispatcher`. Open the migration category, find each
old form next to its runes equivalent, and convert in place:
`export let value` becomes `let { value } = $props()`, the three
`$:` lines become two `$derived` and one `$effect`, the dispatcher
becomes an `onChange` callback prop. Twenty minutes instead of an hour.
Settling a code-review argument about reactive mutation
A teammate claims `state.items.push(x)` cannot be reactive and wants
to reassign the whole array. Pull up the `$state` proxy entry and the
deep-reactivity pitfall: the push DOES trigger updates because $state
returns a Proxy. You paste the two snippets in the PR thread, the
reviewer sees `$state.raw` is the real opt-out, and the unnecessary
spread-rewrite gets dropped.
Building a headless List component with snippet props
You want a `List` that owns keyboard nav and selection but lets the
parent render each row. Search "snippet", copy the `{#snippet row(item)}`
definition and the `{@render row(item)}` call, and wire a snippet prop
so the parent passes per-item markup. The cheat sheet shows the exact
`let { row } = $props()` signature, so you skip the trial-and-error of
guessing how snippet props are typed.
Teaching a React developer Svelte 5 in one screen
A React dev joins the team Friday and needs to be productive Monday.
Send them the callback-props FAQ ("the same pattern React always used")
and the stores-vs-$state entry that maps store to React Context and
$state to useState. The side-by-side Svelte 4 forms become irrelevant
to them, so they read only the runes column and ship a real component
on day one.
Common pitfalls
Writing a rune inside an `if` or `for` block. Runes only work at the top level of `<script>` or a `.svelte.ts` module; `if (cond) let x = $state(0)` throws at compile time.
Reaching for `createEventDispatcher` in new code out of habit. Svelte 5 wants callback props, so declare `let { onSave } = $props()` and call `onSave(value)` instead of dispatching a string-keyed event.
Expecting `$state.raw(obj)` to track field mutations. `.raw` opts OUT of deep tracking on purpose, so `raw.count++` does nothing; use plain `$state` when you mutate nested fields.
FAQ
Related tools
Hand-picked utilities that pair well with this one.