Skip to main content

Svelte Cheatsheet — Svelte 5 Runes, Snippets, Props, Stores, with Svelte 4 Comparison

Svelte 5 cheat sheet — runes ($state, $derived, $effect), snippets, props, stores, with Svelte 4 comparison.

  • Runs locally
  • Category Developer & DevOps
  • Best for Formatting, 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.

Svelte 4 equivalent
import { onDestroy } from 'svelte';
$: {
  const ctrl = new AbortController();
  fetch(url, { signal: ctrl.signal })...
  onDestroy(() => ctrl.abort());
}

$effect.pre — runs before DOM update

Runes
<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.

$props — typed component props

Runes
<script lang="ts">
  interface Props {
    title: string;
    count?: number;
    onSelect?: (id: string) => void;
  }
  let { title, count = 0, onSelect }: Props = $props();
</script>

<h2>{title} ({count})</h2>

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.

.svelte.ts module: reactivity outside components

Runes
// counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);
  return {
    get value() { return count; },
    increment: () => count++,
    reset: () => { count = initial; },
  };
}

// Counter.svelte
<script lang="ts">
  import { createCounter } from './counter.svelte.ts';
  const counter = createCounter();
</script>
<button onclick={counter.increment}>{counter.value}</button>

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.

deep reactivity: nested arrays and objects

Runes
<script lang="ts">
  let board = $state({
    rows: [
      { id: 1, cells: [0, 0, 0] },
      { id: 2, cells: [0, 0, 0] },
    ],
  });

  // every level is reactive through the Proxy:
  board.rows[0].cells[1] = 5;     // tracked
  board.rows.push({ id: 3, cells: [] }); // tracked
  board.rows[0].cells.splice(0, 1);      // tracked
</script>

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.

$: doubled = … → $derived

Svelte 4 → 5
// Svelte 4 (legacy)
$: doubled = count * 2;
$: console.log(doubled);

// Svelte 5 (runes)
let doubled = $derived(count * 2);
$effect(() => console.log(doubled));

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.

on:click → onclick

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<button on:click={handle}>Click</button>
<input on:input={(e) => value = e.currentTarget.value} />

<!-- Svelte 5 -->
<button onclick={handle}>Click</button>
<input oninput={(e) => value = e.currentTarget.value} />

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.

<slot /> → {@render children()}

Svelte 4 → 5
<!-- Svelte 4 (legacy) Modal.svelte -->
<div class="modal">
  <slot />
</div>

<!-- Svelte 5 Modal.svelte -->
<script lang="ts">
  let { children } = $props();
</script>
<div class="modal">
  {@render children()}
</div>

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.

createEventDispatcher → callback prop

Svelte 4 → 5
<!-- Svelte 4 (legacy) Picker.svelte -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher<{ select: string }>();
</script>
<button on:click={() => dispatch('select', 'a')}>A</button>

<!-- Parent: <Picker on:select={(e) => console.log(e.detail)} /> -->

<!-- Svelte 5 Picker.svelte -->
<script lang="ts">
  let { onSelect }: { onSelect: (id: string) => void } = $props();
</script>
<button onclick={() => onSelect('a')}>A</button>

<!-- Parent: <Picker onSelect={(id) => console.log(id)} /> -->

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.

beforeUpdate / afterUpdate → $effect.pre / $effect

Svelte 4 → 5
// 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';

$$props / $$restProps → $props() rest

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<script>
  export let href;
</script>
<a {href} {...$$restProps}><slot /></a>

<!-- Svelte 5 -->
<script lang="ts">
  let { href, children, ...rest } = $props();
</script>
<a {href} {...rest}>{@render children()}</a>

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.

Svelte 4 equivalent
<a {href} {...$$restProps}><slot /></a>

$$slots → optional snippet props

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
{#if $$slots.footer}
  <footer><slot name="footer" /></footer>
{/if}

<!-- Svelte 5 -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { footer }: { footer?: Snippet } = $props();
</script>
{#if footer}
  <footer>{@render footer()}</footer>
{/if}

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.

Svelte 4 equivalent
<svelte:fragment slot="list">...</svelte:fragment>

tick() stays the same

Svelte 4 → 5
<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.

Component (12)

<script lang="ts"> — typed component

Component
<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    items: string[];
    row?: Snippet<[string, number]>;
  }
  let { items, row }: Props = $props();
</script>

<ul>
  {#each items as item, i}
    <li>{row ? '' : item}{#if row}{@render row(item, i)}{/if}</li>
  {/each}
</ul>

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.

<style> — scoped per component

Component
<div class="card">
  <h3>Hello</h3>
</div>

<style>
  .card {
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 1rem;
  }
  h3 { margin: 0; }
  /* unscope with :global */
  :global(body) { font-family: system-ui; }
</style>

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.

props with defaults and rest

Component
<script lang="ts">
  interface Props {
    href: string;
    target?: '_blank' | '_self';
    children: import('svelte').Snippet;
  }
  let {
    href,
    target = '_self',
    children,
    ...rest
  }: Props & Record<string, unknown> = $props();
</script>

<a {href} {target} {...rest}>{@render children()}</a>

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.

Svelte 4 equivalent
<!-- Svelte 4 Card.svelte -->
<div class="card">
  <slot />
</div>

generics="T": type-safe generic component

Component
<!-- Select.svelte -->
<script lang="ts" generics="T extends { id: string }">
  import type { Snippet } from 'svelte';
  let { options, selected = $bindable(), label }:
    {
      options: T[];
      selected?: T;
      label: Snippet<[T]>;
    } = $props();
</script>

{#each options as opt}
  <button onclick={() => selected = opt}>{@render label(opt)}</button>
{/each}

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`.

svelte:self → import the file itself

Component
<!-- Tree.svelte — recursive component -->
<script lang="ts">
  import Tree from './Tree.svelte'; // import yourself
  let { node }: { node: { name: string; children?: any[] } } = $props();
</script>

<li>
  {node.name}
  {#if node.children?.length}
    <ul>
      {#each node.children as child}
        <Tree node={child} />
      {/each}
    </ul>
  {/if}
</li>

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.

svelte:head: per-page title and meta

Component
<script lang="ts">
  let { post }: { post: { title: string; excerpt: string } } = $props();
</script>

<svelte:head>
  <title>{post.title} · My Blog</title>
  <meta name="description" content={post.excerpt} />
  <meta property="og:title" content={post.title} />
</svelte:head>

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.

svelte:boundary: catch render errors

Component
<svelte:boundary onerror={(e) => report(e)}>
  <RiskyWidget />

  {#snippet failed(error, reset)}
    <div class="err">
      <p>Something broke: {error.message}</p>
      <button onclick={reset}>Try again</button>
    </div>
  {/snippet}
</svelte:boundary>

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.

Events (10)

onclick — native DOM event

Events
<script lang="ts">
  let count = $state(0);
  function handle(e: MouseEvent) {
    console.log(e.clientX, e.clientY);
    count++;
  }
</script>

<button onclick={handle}>{count}</button>
<button onclick={() => count = 0}>reset</button>

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.

event modifier shim — preventDefault wrapper

Events
// utils/events.ts
export function preventDefault<E extends Event>(fn: (e: E) => void) {
  return (e: E) => { e.preventDefault(); fn(e); };
}

// usage
<script lang="ts">
  import { preventDefault } from './utils/events';
  function submit() { /* ... */ }
</script>
<form onsubmit={preventDefault(submit)}>...</form>

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.

custom event via callback prop

Events
<!-- Toggle.svelte -->
<script lang="ts">
  let { value = $bindable(false), onChange }:
    { value?: boolean; onChange?: (v: boolean) => void } = $props();
  function flip() {
    value = !value;
    onChange?.(value);
  }
</script>
<button onclick={flip}>{value ? 'on' : 'off'}</button>

<!-- Parent.svelte -->
<Toggle bind:value={enabled} onChange={(v) => log(v)} />

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.

event capturing — oncapture / onclickcapture

Events
<div onclickcapture={(e) => console.log('outer capture')}>
  <button onclick={(e) => console.log('inner bubble')}>
    click me
  </button>
</div>

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.

component events via callback array

Events
<!-- Stepper.svelte -->
<script lang="ts">
  let { value = $bindable(0), onIncrement, onDecrement }:
    {
      value?: number;
      onIncrement?: (v: number) => void;
      onDecrement?: (v: number) => void;
    } = $props();
</script>

<button onclick={() => { value--; onDecrement?.(value); }}>-</button>
<span>{value}</span>
<button onclick={() => { value++; onIncrement?.(value); }}>+</button>

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).

bind:this — element reference

Directive
<script lang="ts">
  let inputEl: HTMLInputElement | undefined = $state();

  $effect(() => {
    inputEl?.focus();
  });
</script>

<input bind:this={inputEl} />

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.

animate:flip — FLIP list reordering

Directive
<script lang="ts">
  import { flip } from 'svelte/animate';
  let items = $state([
    { id: 1, name: 'A' },
    { id: 2, name: 'B' },
    { id: 3, name: 'C' },
  ]);
  function shuffle() {
    items = [...items].sort(() => Math.random() - 0.5);
  }
</script>

<button onclick={shuffle}>shuffle</button>
<ul>
  {#each items as item (item.id)}
    <li animate:flip={{ duration: 250 }}>{item.name}</li>
  {/each}
</ul>

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.

{#each items as item (item.id)}

Control Flow
<script lang="ts">
  let items = $state([
    { id: 1, name: 'apple' },
    { id: 2, name: 'banana' },
  ]);
</script>

<ul>
  {#each items as item, i (item.id)}
    <li>{i + 1}. {item.name}</li>
  {:else}
    <li>no items</li>
  {/each}
</ul>

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.

{#await promise}

Control Flow
<script lang="ts">
  let promise = $state<Promise<unknown>>(fetch('/api/me').then(r => r.json()));
</script>

{#await promise}
  <p>Loading…</p>
{:then data}
  <pre>{JSON.stringify(data, null, 2)}</pre>
{:catch err}
  <p class="err">{err.message}</p>
{/await}

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).

{#key expr} — force remount

Control Flow
<script lang="ts">
  let userId = $state('alice');
</script>

<button onclick={() => userId = userId === 'alice' ? 'bob' : 'alice'}>
  switch
</button>

{#key userId}
  <UserProfile id={userId} />
{/key}

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.

{@const} in blocks

Control Flow
<script lang="ts">
  let rows = $state([{ price: 100, qty: 3 }, { price: 50, qty: 2 }]);
</script>

{#each rows as row}
  {@const subtotal = row.price * row.qty}
  <li>
    {row.qty} × {row.price} = <strong>{subtotal}</strong>
    {#if subtotal > 200}<span class="hot">big</span>{/if}
  </li>
{/each}

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.

Snippets (8)

{#snippet} — define a snippet

Snippets
<script lang="ts">
  let items = $state(['a', 'b', 'c']);
</script>

{#snippet row(item: string, i: number)}
  <li><strong>{i + 1}.</strong> {item}</li>
{/snippet}

<ul>
  {#each items as item, i}
    {@render row(item, i)}
  {/each}
</ul>

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.

{@render} — invoke a snippet

Snippets
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { header, children, footer }:
    { header?: Snippet; children: Snippet; footer?: Snippet } = $props();
</script>

<section>
  {#if header}<header>{@render header()}</header>{/if}
  <main>{@render children()}</main>
  {#if footer}<footer>{@render footer()}</footer>{/if}
</section>

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.

snippet as a prop — headless components

Snippets
<!-- List.svelte -->
<script lang="ts" generics="T">
  import type { Snippet } from 'svelte';
  let { items, row }:
    { items: T[]; row: Snippet<[T, number]> } = $props();
</script>
<ul>
  {#each items as item, i}<li>{@render row(item, i)}</li>{/each}
</ul>

<!-- Parent.svelte -->
<List items={users} row={userRow} />
{#snippet userRow(u: User, i: number)}
  <strong>{i + 1}.</strong> {u.name} — {u.email}
{/snippet}

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.

children — implicit default snippet

Snippets
<!-- Modal.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { open, children }:
    { open: boolean; children: Snippet } = $props();
</script>
{#if open}
  <div class="modal-backdrop">
    <div class="modal">{@render children()}</div>
  </div>
{/if}

<!-- usage -->
<Modal open={true}>
  <h2>Hi</h2>
  <button>OK</button>
</Modal>

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>`.

recursive snippet: render a tree

Snippets
<script lang="ts">
  type Node = { name: string; children?: Node[] };
  let root = $state<Node>({
    name: 'root',
    children: [{ name: 'a' }, { name: 'b', children: [{ name: 'c' }] }],
  });
</script>

{#snippet branch(node: Node)}
  <li>
    {node.name}
    {#if node.children}
      <ul>{#each node.children as child}{@render branch(child)}{/each}</ul>
    {/if}
  </li>
{/snippet}

<ul>{@render branch(root)}</ul>

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.

snippet with fallback via #if

Snippets
<!-- Avatar.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { name, badge }: { name: string; badge?: Snippet } = $props();
</script>

<span class="avatar">
  {name[0]}
  {#if badge}
    {@render badge()}
  {:else}
    <span class="dot" aria-hidden="true"></span>
  {/if}
</span>

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.

export a snippet from a module script

Snippets
<!-- icons.svelte -->
<script module lang="ts">
  export { check, cross };
</script>

{#snippet check()}<svg viewBox="0 0 16 16"><path d="M2 8l4 4 8-8"/></svg>{/snippet}
{#snippet cross()}<svg viewBox="0 0 16 16"><path d="M3 3l10 10M13 3L3 13"/></svg>{/snippet}

<!-- consumer.svelte -->
<script lang="ts">
  import { check, cross } from './icons.svelte';
</script>
{@render check()}

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">`.

Stores (11)

writable — basic store

Stores
// stores/counter.ts
import { writable } from 'svelte/store';
export const count = writable(0);

// Counter.svelte
<script lang="ts">
  import { count } from './stores/counter';
</script>
<button onclick={() => count.update(n => n + 1)}>
  {$count}
</button>
<button onclick={() => count.set(0)}>reset</button>

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.

derived — store that depends on stores

Stores
import { writable, derived } from 'svelte/store';

export const price = writable(100);
export const qty = writable(3);
export const total = derived(
  [price, qty],
  ([$p, $q]) => $p * $q
);

// usage: <p>{$total}</p>

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.

custom store — encapsulated API

Stores
import { writable } from 'svelte/store';

export function createCounter(initial = 0) {
  const { subscribe, set, update } = writable(initial);
  return {
    subscribe,
    inc: () => update(n => n + 1),
    dec: () => update(n => n - 1),
    reset: () => set(initial),
  };
}

export const counter = createCounter();
// usage: <button onclick={counter.inc}>{$counter}</button>

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.

persisted store: sync to localStorage

Stores
import { writable } from 'svelte/store';

export function persisted<T>(key: string, initial: T) {
  const stored =
    typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;
  const store = writable<T>(stored ? JSON.parse(stored) : initial);
  store.subscribe((v) => {
    if (typeof localStorage !== 'undefined')
      localStorage.setItem(key, JSON.stringify(v));
  });
  return store;
}

export const settings = persisted('settings', { dark: true });

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()`.

use:enhance: progressive form actions

SvelteKit
<script lang="ts">
  import { enhance } from '$app/forms';
  let saving = $state(false);
</script>

<form method="POST" action="?/save" use:enhance={() => {
  saving = true;
  return async ({ update }) => {
    await update();        // apply result, reset form if invalid
    saving = false;
  };
}}>
  <input name="title" required />
  <button disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
</form>

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.

named form actions

SvelteKit
// +page.server.ts
import type { Actions } from './$types';
export const actions: Actions = {
  create: async ({ request }) => { /* ... */ },
  delete: async ({ request }) => { /* ... */ },
};

// +page.svelte — pick the action with action="?/name"
<form method="POST" action="?/create"><button>Create</button></form>
<form method="POST" action="?/delete"><button>Delete</button></form>

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.

hooks.server.ts: handle middleware

SvelteKit
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  const token = event.cookies.get('session');
  event.locals.user = token ? await lookupUser(token) : null;

  const response = await resolve(event);
  response.headers.set('x-frame-options', 'DENY');
  return response;
};

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.

cookies in load and actions

SvelteKit
// +page.server.ts
import type { Actions } from './$types';

export const actions: Actions = {
  login: async ({ cookies, request }) => {
    const form = await request.formData();
    const token = await signIn(form.get('email'));
    cookies.set('session', token, {
      path: '/', httpOnly: true, sameSite: 'lax',
      secure: true, maxAge: 60 * 60 * 24 * 7,
    });
  },
  logout: async ({ cookies }) => { cookies.delete('session', { path: '/' }); },
};

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.

form actions — progressive forms

SvelteKit
// +page.server.ts
import type { Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';

export const actions: Actions = {
  default: async ({ request, locals }) => {
    const data = await request.formData();
    const email = data.get('email')?.toString();
    if (!email) return fail(400, { email, missing: true });
    await locals.auth.signup(email);
    throw redirect(303, '/welcome');
  },
};

// +page.svelte
<form method="POST">
  <input name="email" required />
  <button>Sign up</button>
</form>

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.

  1. 1 JSON Formatter & Validator Format, validate, and minify JSON instantly — right in your browser. Open
  2. 2 React Hooks Cheatsheet React hooks cheat sheet — all 17 built-in hooks (useState / useEffect / useMemo / useTransition / useFormStatus...) with real examples and pitfalls. Open
  3. 3 Vue 3 Cheatsheet Vue 3 cheat sheet — Composition API, reactivity, components, directives, Pinia, with side-by-side Options API comparison. Open

Real-world use cases

  • 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

Tool combos

Folks in your role tend to reach for these alongside this tool.

Made by Toolora · 100% client-side · Updated 2026-06-13