Jesse A. Green / notebook

Svelte User Guide

Generated May 2026 from sveltejs/svelte at fcaa8ce

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Svelte v5.55.4 — "Cybernetically enhanced web apps"

Source-based reference for building reactive web applications with Svelte's compiler-based, runes-driven paradigm.


1. Quick Start

Get a Svelte application running in under five minutes using SvelteKit (the recommended application framework) or a standalone Vite project.

Tip

SvelteKit is the full-stack framework built on top of Svelte. Use it for any serious project — it handles routing, SSR, code splitting, and deployment adapters out of the box. Use standalone Svelte (via Vite) only when you need to embed components into an existing application.

npx sv create my-app
cd my-app
npm install
npm run dev

The sv create scaffold prompts you for TypeScript, ESLint, Prettier, Playwright, and Vitest. Accept the defaults for a batteries-included starting point.

Navigate to http://localhost:5173 to see the running application.

1.2 Create a Standalone Vite + Svelte Project

For a component library or an embedded widget that doesn't need SvelteKit's router:

npm create vite@latest my-app -- --template svelte
# or for TypeScript:
npm create vite@latest my-app -- --template svelte-ts
cd my-app
npm install
npm run dev

1.3 Your First Component

Create src/Counter.svelte:

<script>
  let count = $state(0);

  function increment() {
    count++;
  }
</script>

<button onclick={increment}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

<style>
  button {
    font-size: 1.5rem;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
  }
</style>

Use it in src/App.svelte:

<script>
  import Counter from './Counter.svelte';
</script>

<h1>Hello Svelte!</h1>
<Counter />

Tip

Svelte 5 uses runes — special $-prefixed functions that the compiler transforms into efficient reactive code. You do not import runes; the compiler recognizes them automatically anywhere inside .svelte files and .svelte.js/.svelte.ts modules.

1.4 Mount Manually (no SvelteKit)

// src/main.js
import { mount } from 'svelte';
import App from './App.svelte';

const app = mount(App, {
  target: document.getElementById('app'),
  props: { name: 'world' }
});

2. Installation

2.1 Requirements

Requirement Minimum Version
Node.js 18.x or higher
npm / pnpm / yarn any modern version
Browser Modern evergreen browsers (Chrome 64+, Firefox 69+, Safari 13+, Edge 79+)

Important

Svelte 5 requires Node.js >= 18. Older Node versions will produce build errors. Verify with node --version before starting.

2.2 Install as a Dependency

In an existing project, add Svelte and its Vite plugin:

npm install --save-dev svelte @sveltejs/vite-plugin-svelte vite

Configure Vite:

// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()]
});

2.3 Install SvelteKit

npm install --save-dev @sveltejs/kit @sveltejs/adapter-auto
npm install svelte

2.4 TypeScript Support

Svelte has first-class TypeScript support. When using sv create, select TypeScript at the prompt. For manual setup:

npm install --save-dev typescript svelte-check

Add svelte-check to your package.json scripts:

{
  "scripts": {
    "check": "svelte-check --tsconfig ./tsconfig.json"
  }
}

Run npm run check to type-check your .svelte files.

Note

Inside .svelte files, use <script lang="ts"> to enable TypeScript in that component's script block.

2.5 Editor Setup

VS Code (recommended): Install the Svelte for VS Code extension. It provides syntax highlighting, IntelliSense, hover docs, go-to-definition, and auto-completion for runes.

JetBrains IDEs: Install the Svelte plugin from the JetBrains marketplace.

Neovim / Vim: Use svelte-language-server via LSP.

2.6 Package Structure

The svelte npm package exports multiple submodules:

Submodule Contents
svelte Runes, lifecycle, context, rendering, utilities
svelte/store Writable, readable, derived stores
svelte/motion tweened, spring
svelte/transition fade, blur, fly, slide, scale, draw, crossfade
svelte/animate flip
svelte/easing All easing functions
svelte/events Event utilities
svelte/attachments Attachment utilities
svelte/reactivity SvelteDate, SvelteMap, SvelteSet, SvelteURL, etc.
svelte/reactivity/window Window-reactive values
svelte/server render() for SSR
svelte/compiler compile(), parse(), preprocess(), migrate(), compileModule(), VERSION
svelte/legacy Legacy compatibility helpers

Warning

Do not import runes ($state, $derived, etc.) from any package. They are compiler-recognized keywords, not runtime imports. Writing import { $state } from 'svelte' will produce an error.


3. Configuration

3.1 svelte.config.js

The svelte.config.js file at your project root controls the Svelte compiler, preprocessors, and (for SvelteKit) the deployment adapter.

// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Preprocessors run before the Svelte compiler
  preprocess: vitePreprocess(),

  kit: {
    adapter: adapter()
  },

  // Svelte compiler options
  compilerOptions: {
    // see section 3.2
  }
};

export default config;

3.2 Compiler Options (compilerOptions)

These options are passed to the Svelte compiler. Set them in svelte.config.js under compilerOptions, or per-component via <svelte:options>.

Output & Mode

Option Type Default Description
generate 'client' | 'server' | false 'client' Target output: DOM, SSR, or no output (analysis only)
dev boolean false Enable development checks and warnings
sourcemap boolean | object false Include source maps in output
outputFilename string undefined JS output filename (for source maps)
cssOutputFilename string undefined CSS output filename (for source maps)

Component Behavior

Option Type Default Description
runes boolean | undefined undefined Force runes mode (true), force legacy mode (false), or auto-detect (undefined)
hydratable boolean true (SSR) Emit hydration markers in SSR output
css 'injected' | 'external' | 'none' 'injected' How component CSS is emitted
namespace 'html' | 'mathml' | 'svg' 'html' Element namespace for top-level elements
customElement boolean false Compile as a custom element
accessors boolean false Generate getters/setters for props (legacy)
immutable boolean false Assume objects never mutate (legacy optimization)

Warnings

Option Type Default Description
warningFilter (warning) => boolean undefined Filter which warnings are shown

Example: disable a specific warning:

// svelte.config.js
const config = {
  compilerOptions: {
    warningFilter: (warning) => warning.code !== 'a11y_mouse_events_have_key_events'
  }
};

3.3 <svelte:options> (Per-Component Overrides)

Override compiler options for a single component at the top of the file:

<svelte:options
  runes={true}
  customElement={{ tag: 'my-widget', shadow: 'none' }}
  namespace="svg"
  css="injected"
  accessors={false}
/>

3.4 Vite Plugin Options

// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [
    svelte({
      // Path to svelte.config.js (auto-discovered by default)
      configFile: 'svelte.config.js',

      // Additional extensions to treat as Svelte files
      extensions: ['.svelte'],

      // Preprocessors (if not using svelte.config.js)
      preprocess: [],

      // Compiler options (merged with svelte.config.js)
      compilerOptions: {
        dev: process.env.NODE_ENV !== 'production'
      },

      // Enable hot module replacement
      hot: true
    })
  ]
});

3.5 TypeScript Configuration

// tsconfig.json (SvelteKit project)
{
  "extends": "./.svelte-kit/tsconfig.json",
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true
  }
}

3.6 SvelteKit Adapters

Adapters tell SvelteKit how to deploy your app:

Adapter Target
@sveltejs/adapter-auto Detects Vercel, Netlify, Cloudflare, etc.
@sveltejs/adapter-node Node.js server
@sveltejs/adapter-static Static site (no SSR)
@sveltejs/adapter-vercel Vercel (explicit)
@sveltejs/adapter-netlify Netlify (explicit)
@sveltejs/adapter-cloudflare Cloudflare Pages/Workers
// svelte.config.js — static site example
import adapter from '@sveltejs/adapter-static';

const config = {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: 'index.html', // for SPA mode
      precompress: false,
      strict: true
    })
  }
};
export default config;

4. Core Usage

4.1 Components

A Svelte component is a .svelte file with three optional sections:

<script>
  // Logic: runes, imports, variables, functions
</script>

<!-- Template: HTML + template syntax -->
<div>Content</div>

<style>
  /* Scoped CSS — only applies to this component */
  div { color: red; }
</style>

All three sections are optional. The order is conventional but not enforced.

Module-level script

For code that runs once per module (not once per instance), use <script module>:

<script module>
  // Runs once when module is loaded — shared across instances
  let instanceCount = 0;

  export function getInstanceCount() {
    return instanceCount;
  }
</script>

<script>
  import { onMount } from 'svelte';
  // Runs once per component instance
  instanceCount++;
</script>

4.2 Runes — The Reactivity System

Svelte 5's reactivity is driven by runes: compiler-recognized $-prefixed constructs that declare reactive intent. Runes work in .svelte files and in .svelte.js / .svelte.ts modules.

$state — Reactive State

<script>
  let count = $state(0);
  let user = $state({ name: 'Alice', age: 30 });
  let items = $state([1, 2, 3]);
</script>

<p>{count}</p>
<button onclick={() => count++}>Increment</button>

<!-- Deep reactivity: mutations to nested properties work -->
<button onclick={() => user.name = 'Bob'}>Rename</button>
<button onclick={() => items.push(4)}>Add item</button>

The $state rune creates a deeply reactive proxy for objects and arrays. Mutations at any depth trigger updates.

Note

$state uses JavaScript Proxy under the hood. Logging a state object shows a Proxy — use $state.snapshot() to get a plain object for debugging or serialization.

$state.raw — Non-deep Reactive State

For large objects or arrays where you only need to track top-level reassignment (not mutations):

<script>
  let data = $state.raw({ items: [1, 2, 3] });

  function reset() {
    // Must reassign — mutation won't trigger update
    data = { items: [] };
  }
</script>
$state.snapshot — Plain Copy of State
<script>
  let list = $state([1, 2, 3]);

  function logList() {
    // Returns a plain array, not a Proxy
    const plain = $state.snapshot(list);
    console.log(plain); // [1, 2, 3]
    localStorage.setItem('list', JSON.stringify(plain));
  }
</script>
$state.eager — Latest Value During Suspension

For use inside {#await} blocks when you need the newest value even before the promise settles:

<script>
  let value = $state.eager(0);
</script>

Warning

$state.eager combined with $effect.pending() has a known bug in 5.55.4 (issue #18095). Until fixed, avoid this combination in production.

$derived — Computed Values

<script>
  let width = $state(10);
  let height = $state(20);
  let area = $derived(width * height);
  let perimeter = $derived(2 * (width + height));
</script>

<p>Area: {area}</p>
<p>Perimeter: {perimeter}</p>

<input type="number" bind:value={width} />
<input type="number" bind:value={height} />

$derived is lazy and cached — it only recomputes when its dependencies change.

$derived.by — Complex Derivations

Use $derived.by(fn) when the expression is too complex for a single inline expression:

<script>
  let numbers = $state([3, 1, 4, 1, 5, 9, 2, 6]);

  let stats = $derived.by(() => {
    const sorted = [...numbers].sort((a, b) => a - b);
    const sum = numbers.reduce((a, b) => a + b, 0);
    const mean = sum / numbers.length;
    const median = sorted[Math.floor(sorted.length / 2)];
    return { sum, mean, median, min: sorted[0], max: sorted.at(-1) };
  });
</script>

<p>Sum: {stats.sum}, Mean: {stats.mean.toFixed(2)}</p>

Important

Never use side effects inside $derived. It runs in a tracking context and is expected to be pure. Use $effect for side effects.

$effect — Side Effects

<script>
  let count = $state(0);

  $effect(() => {
    // Runs after the DOM updates whenever count changes
    document.title = `Count: ${count}`;

    // Optional cleanup — returned function runs before next effect execution
    return () => {
      document.title = 'App';
    };
  });
</script>

Effects automatically track which reactive values they access. When any of those values change, the effect re-runs (after DOM update).

$effect.pre — Before DOM Update
<script>
  import { tick } from 'svelte';
  let messages = $state([]);
  let div;

  $effect.pre(() => {
    // Runs before DOM update
    // Useful for reading DOM layout before it changes
    const scrolledToBottom =
      div && div.offsetHeight + div.scrollTop > div.scrollHeight - 20;
    
    if (scrolledToBottom) {
      // Will be applied after DOM updates
      tick().then(() => {
        div.scrollTop = div.scrollHeight;
      });
    }
  });
</script>

<div bind:this={div} style="overflow: auto; height: 300px;">
  {#each messages as msg}
    <p>{msg}</p>
  {/each}
</div>
$effect.tracking — Check Reactive Context
<script>
  function maybeTrack() {
    if ($effect.tracking()) {
      // We're inside an effect or derived — safe to read reactive state
      console.log('Inside tracking context');
    }
  }
</script>
$effect.root — Manual Cleanup Scope

Use when you need to create effects outside of component lifecycle (e.g., in utilities):

// utils.svelte.js
import { $effect } from 'svelte'; // NOTE: still a compiler keyword, not an import

export function createLogger(getValue) {
  const cleanup = $effect.root(() => {
    $effect(() => {
      console.log('Value changed:', getValue());
    });
  });
  return cleanup; // Call to tear down all effects in this root
}
$effect.pending — Pending Async Boundaries

Returns the number of pending promises in the nearest <svelte:boundary>:

<script>
  let pending = $derived($effect.pending());
</script>

{#if pending > 0}
  <p>Loading {pending} items...</p>
{/if}

$props — Component Props

<!-- Greeting.svelte -->
<script>
  let { name, greeting = 'Hello' } = $props();
</script>

<p>{greeting}, {name}!</p>

Usage:

<Greeting name="Alice" />
<Greeting name="Bob" greeting="Howdy" />
Rest Props
<script>
  let { class: className, style, ...rest } = $props();
</script>

<div class={className} {style} {...rest}>
  <slot />
</div>
TypeScript Props
<script lang="ts">
  interface Props {
    name: string;
    count?: number;
    onUpdate?: (value: number) => void;
  }

  let { name, count = 0, onUpdate }: Props = $props();
</script>

$props.id — Unique Component ID

Available since Svelte 5.20.0. Generates a unique ID per component instance — useful for accessibility attributes like for/id pairings:

<script>
  const uid = $props.id();
</script>

<label for={uid}>Label</label>
<input id={uid} type="text" />

$bindable — Two-Way Bindable Props

Mark a prop as bindable so parent components can use bind:propName:

<!-- TextInput.svelte -->
<script>
  let { value = $bindable('') } = $props();
</script>

<input bind:value />

Parent usage:

<script>
  let text = $state('');
</script>

<TextInput bind:value={text} />
<p>You typed: {text}</p>

$inspect — Development Debugging

$inspect logs any reactive value changes in development mode. Has no effect in production builds.

<script>
  let count = $state(0);
  let user = $state({ name: 'Alice' });

  // Logs to console whenever count or user changes
  $inspect(count, user);

  // Custom handler
  $inspect(count).with((type, value) => {
    if (type === 'update') {
      myAnalytics.track('count_changed', { value });
    }
  });
</script>
$inspect.trace — Effect Dependency Tracing
<script>
  $effect(() => {
    $inspect.trace('my effect');
    // Now logs which state changes caused this effect to re-run
    doSomething(count, user);
  });
</script>

$host — Custom Element Access

For components compiled as custom elements, $host() returns the host element (this):

<svelte:options customElement="my-button" />

<script>
  function dispatch() {
    $host().dispatchEvent(new CustomEvent('activate', { bubbles: true }));
  }
</script>

<button onclick={dispatch}>Activate</button>

4.3 Lifecycle Functions

Import lifecycle functions from svelte:

<script>
  import { onMount, onDestroy } from 'svelte';

  let timer;

  onMount(() => {
    // Called after component is mounted to the DOM
    timer = setInterval(() => console.log('tick'), 1000);

    // Return a cleanup function (optional)
    return () => clearInterval(timer);
  });

  onDestroy(() => {
    // Called when component is about to be removed
    // Also runs during SSR (unlike onMount)
    clearInterval(timer);
  });
</script>

Note

onMount does not run during server-side rendering. onDestroy does run during SSR. For SSR cleanup, use onDestroy.

Deprecated Lifecycle Functions

Deprecated Replacement
beforeUpdate(fn) $effect.pre(() => ...)
afterUpdate(fn) $effect(() => ...)
createEventDispatcher() Callback props or $host()
<!-- OLD (still works, but deprecated) -->
<script>
  import { beforeUpdate, afterUpdate, createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();
  beforeUpdate(() => { /* before DOM update */ });
  afterUpdate(() => { /* after DOM update */ });
</script>
<button on:click={() => dispatch('activate')}>Click</button>

<!-- NEW (Svelte 5 style) -->
<script>
  let { onactivate } = $props();
  $effect.pre(() => { /* before DOM update */ });
  $effect(() => { /* after DOM update */ });
</script>
<button onclick={() => onactivate?.()}>Click</button>

4.4 Context API

Context allows passing data down the component tree without prop drilling.

<!-- Parent.svelte -->
<script>
  import { setContext } from 'svelte';
  import Child from './Child.svelte';

  const theme = $state({ primary: '#3498db', dark: false });

  // Key can be any value — use a Symbol for uniqueness
  const THEME_KEY = Symbol('theme');
  setContext(THEME_KEY, theme);
</script>

<Child />
<!-- Child.svelte (any depth below Parent) -->
<script>
  import { getContext } from 'svelte';
  const THEME_KEY = Symbol('theme'); // Must use same key — share via module export

  const theme = getContext(THEME_KEY);
</script>

<div style:background={theme.dark ? '#333' : '#fff'}>
  <p style:color={theme.primary}>Themed content</p>
</div>

Tip

Export the context key from a shared module so parent and child always use the same reference:

// context-keys.js
export const THEME_KEY = Symbol('theme');
export const USER_KEY = Symbol('user');

Context Utilities

import { setContext, getContext, hasContext, getAllContexts, createContext } from 'svelte';

// Check if context exists before reading
if (hasContext('key')) {
  const value = getContext('key');
}

// Get all contexts (returns a Map)
const all = getAllContexts();

// createContext — typed context helper
const { getContext: getTheme, setContext: setTheme } = createContext();

4.5 Template Syntax

Interpolation

<p>Hello, {name}!</p>
<p>The answer is {40 + 2}</p>
<p title={tooltip}>Hover me</p>

Conditionals — {#if}

{#if user.loggedIn}
  <p>Welcome back, {user.name}!</p>
{:else if user.pending}
  <p>Verifying your account...</p>
{:else}
  <a href="/login">Please log in</a>
{/if}

Loops — {#each}

<!-- Basic -->
{#each items as item}
  <li>{item.name}</li>
{/each}

<!-- With index -->
{#each items as item, i}
  <li>{i + 1}. {item.name}</li>
{/each}

<!-- With key (for efficient DOM updates) -->
{#each items as item (item.id)}
  <li>{item.name}</li>
{/each}

<!-- With else (empty state) -->
{#each items as item (item.id)}
  <li>{item.name}</li>
{:else}
  <li>No items found</li>
{/each}

<!-- Destructuring -->
{#each users as { id, name, avatar } (id)}
  <div>
    <img src={avatar} alt={name} />
    <span>{name}</span>
  </div>
{/each}

Important

Always use a key (item.id) when rendering lists of dynamic items. Without a key, Svelte patches the DOM in-place, which can produce incorrect results when items are reordered, added, or removed.

Note

There is a known performance regression (issue #18120) in Svelte 5.55.4 where keyed {#each} has slightly worse performance than non-keyed in some benchmarks. For most real-world lists, use keyed — correctness outweighs micro-benchmarks.

Async — {#await}

<script>
  let promise = fetchUser(userId);
</script>

{#await promise}
  <p>Loading...</p>
{:then user}
  <p>Hello, {user.name}!</p>
{:catch error}
  <p class="error">Error: {error.message}</p>
{/await}

<!-- Shorthand: skip loading state -->
{#await promise then user}
  <p>{user.name}</p>
{/await}

<!-- Shorthand: skip to error -->
{#await promise catch error}
  <p>{error.message}</p>
{/await}

Snippets — {#snippet} and {@render}

Snippets are reusable template fragments — the Svelte 5 replacement for slots:

<!-- Define a snippet -->
{#snippet greeting(name, formal = false)}
  {#if formal}
    <p>Good day, {name}.</p>
  {:else}
    <p>Hey {name}!</p>
  {/if}
{/snippet}

<!-- Render it -->
{@render greeting('Alice')}
{@render greeting('Dr. Smith', true)}
Passing Snippets as Props
<!-- Table.svelte -->
<script>
  let { rows, header, row } = $props();
</script>

<table>
  <thead>
    <tr>{@render header()}</tr>
  </thead>
  <tbody>
    {#each rows as r}
      <tr>{@render row(r)}</tr>
    {/each}
  </tbody>
</table>
<!-- Usage -->
<Table {rows}>
  {#snippet header()}
    <th>Name</th><th>Score</th>
  {/snippet}
  {#snippet row(r)}
    <td>{r.name}</td><td>{r.score}</td>
  {/snippet}
</Table>
Default Snippet (children)

Content between component tags becomes the children prop:

<!-- Card.svelte -->
<script>
  let { children, title } = $props();
</script>

<div class="card">
  <h2>{title}</h2>
  {@render children()}
</div>
<Card title="My Card">
  <p>This is the card body.</p>
</Card>

{@const} — Local Constants

Declare a local constant inside a template block:

{#each boxes as box}
  {@const area = box.width * box.height}
  {@const label = area > 100 ? 'large' : 'small'}
  <p>{box.id}: {area}px² ({label})</p>
{/each}

{@html} — Raw HTML

<script>
  // WARNING: Always sanitize before using @html
  import DOMPurify from 'dompurify';
  let rawHtml = $state('<p>Hello <strong>world</strong></p>');
  let safe = $derived(DOMPurify.sanitize(rawHtml));
</script>

<div>{@html safe}</div>

Caution

{@html} bypasses Svelte's XSS protection. Never use it with unsanitized user input. Always use a library like DOMPurify to sanitize HTML before rendering.

{@debug} — Template Debugging

<script>
  let user = $state({ name: 'Alice', role: 'admin' });
</script>

<!-- Pauses execution in devtools when user changes -->
{@debug user}
<p>{user.name}</p>

4.6 Directives

Event Handling

Svelte 5 uses standard HTML event attributes:

<script>
  function handleClick(event) {
    console.log('clicked', event.target);
  }

  function handleInput(event) {
    console.log(event.target.value);
  }
</script>

<!-- Inline handler -->
<button onclick={() => count++}>+1</button>

<!-- Named handler -->
<button onclick={handleClick}>Click me</button>

<!-- With event object -->
<input oninput={handleInput} />

<!-- Multiple events -->
<div
  onmouseenter={() => hovered = true}
  onmouseleave={() => hovered = false}
>Hover me</div>

Note

Svelte 5 uses direct event attributes (onclick, oninput) rather than the legacy on:click, on:input syntax. Both work during the migration period, but new code should use the native attribute form.

Delegated Events

Svelte automatically delegates these events for performance (one listener on the document root instead of per-element):

beforeinput, click, change, dblclick, contextmenu, focusin, focusout, input, keydown, keyup, mousedown, mousemove, mouseout, mouseover, mouseup, pointerdown, pointermove, pointerout, pointerover, pointerup, touchend, touchmove, touchstart

touchstart and touchmove are passive by default.

Event Modifiers (Legacy)

In legacy mode, on:event|modifier syntax is supported:

<button on:click|preventDefault={submit}>Submit</button>
<button on:click|stopPropagation|once={action}>Once</button>

bind: — Two-Way Binding

<script>
  let name = $state('');
  let checked = $state(false);
  let selected = $state('');
  let files = $state();
  let group = $state([]);
  let div;
</script>

<!-- Text input -->
<input type="text" bind:value={name} />

<!-- Checkbox -->
<input type="checkbox" bind:checked />

<!-- Select -->
<select bind:value={selected}>
  <option value="a">Option A</option>
  <option value="b">Option B</option>
</select>

<!-- File input -->
<input type="file" bind:files />

<!-- Checkbox group -->
{#each ['apple', 'banana', 'cherry'] as fruit}
  <label>
    <input type="checkbox" bind:group={group} value={fruit} />
    {fruit}
  </label>
{/each}

<!-- DOM element reference -->
<div bind:this={div}></div>

<!-- Read-only DOM bindings -->
<div bind:clientWidth={width} bind:clientHeight={height}></div>
<div bind:offsetWidth bind:offsetHeight></div>
<div bind:contentRect bind:contentBoxSize></div>
<div bind:borderBoxSize bind:devicePixelContentBoxSize></div>

<!-- Window bindings -->
<svelte:window bind:scrollX bind:scrollY />
<svelte:window bind:innerWidth bind:innerHeight />
<svelte:window bind:outerWidth bind:outerHeight />

<!-- Media bindings -->
<video
  bind:currentTime
  bind:duration
  bind:paused
  bind:volume
  bind:muted
  bind:playbackRate
  bind:buffered
  bind:seekable
  bind:played
/>

<!-- Content editable -->
<div contenteditable="true" bind:innerHTML={html}></div>
<div contenteditable="true" bind:innerText={text}></div>

class: — Conditional Classes

<script>
  let active = $state(false);
  let disabled = $state(false);
</script>

<!-- Conditional class -->
<button class:active class:disabled>Button</button>

<!-- Named class different from variable -->
<div class:is-highlighted={active}>Content</div>

<!-- Class object syntax -->
<div class={{ active, disabled, 'is-large': size === 'large' }}>Content</div>

<!-- Mix static and dynamic -->
<div class="base-styles" class:active>Content</div>

style: — Dynamic Styles

<script>
  let color = $state('red');
  let size = $state(16);
</script>

<!-- Single property -->
<p style:color>Red text</p>
<p style:font-size="{size}px">Sized text</p>

<!-- With importance -->
<p style:color|important={color}>Important!</p>

<!-- Object syntax via style attribute -->
<p style="color: {color}; font-size: {size}px">Mixed</p>

use: — Actions

Actions are functions applied to DOM elements. They receive the element and optional parameters, and can return an object with update and destroy methods:

// actions.js
export function tooltip(element, text) {
  let tip;

  function show() {
    tip = document.createElement('div');
    tip.textContent = text;
    tip.className = 'tooltip';
    document.body.appendChild(tip);
    // position relative to element...
  }

  function hide() {
    tip?.remove();
  }

  element.addEventListener('mouseenter', show);
  element.addEventListener('mouseleave', hide);

  return {
    update(newText) {
      text = newText;
      if (tip) tip.textContent = newText;
    },
    destroy() {
      element.removeEventListener('mouseenter', show);
      element.removeEventListener('mouseleave', hide);
      tip?.remove();
    }
  };
}
<script>
  import { tooltip } from './actions.js';
  let message = $state('Hello!');
</script>

<button use:tooltip={message}>Hover me</button>

transition:, in:, out: — Transitions

<script>
  import { fade, fly, slide, scale, blur } from 'svelte/transition';
  let visible = $state(true);
</script>

<!-- Both in and out -->
<div transition:fade>Fades in and out</div>

<!-- Different in/out -->
<div in:fly={{ y: -20, duration: 300 }} out:fade={{ duration: 150 }}>
  Different transitions
</div>

<!-- With parameters -->
<div transition:slide={{ duration: 400, easing: cubicOut }}>
  Slides
</div>

<!-- Local transition (only runs for this component's mount/unmount) -->
<div transition:fade|local>Local only</div>

{#if visible}
  <p transition:scale>Toggle me</p>
{/if}

animate: — List Animations

<script>
  import { flip } from 'svelte/animate';
  let items = $state([1, 2, 3, 4, 5]);
</script>

{#each items as item (item)}
  <div animate:flip={{ duration: 300 }}>
    {item}
  </div>
{/each}

4.7 Scoped Styles

CSS inside <style> is automatically scoped to the component via a generated class attribute:

<style>
  /* Only applies to <p> elements in THIS component */
  p { color: blue; }

  /* :global() escapes scoping */
  :global(body) { margin: 0; }
  
  /* Target children of a component using :global */
  .wrapper :global(p) { font-size: 1.2rem; }

  /* CSS custom properties are inherited by children */
  .themed {
    --accent: #ff6b35;
  }
</style>

The style attribute on component tags

Pass CSS variables to child components:

<!-- Parent -->
<ChildComponent style="--color: red; --size: 1.5rem" />
<!-- ChildComponent.svelte -->
<p style="color: var(--color); font-size: var(--size)">
  Styled via CSS variable
</p>

4.8 Special Elements

<svelte:component> — Dynamic Components

<script>
  import Foo from './Foo.svelte';
  import Bar from './Bar.svelte';

  let selected = $state('foo');
  let component = $derived(selected === 'foo' ? Foo : Bar);
</script>

<svelte:component this={component} prop="value" />
<!-- or simply: -->
<component this={component} prop="value" />

<svelte:element> — Dynamic HTML Elements

<script>
  let tag = $state('h2');
</script>

<svelte:element this={tag} class="title">
  Dynamic heading
</svelte:element>

<svelte:window> — Window Bindings

<script>
  let scrollY = $state(0);
  let innerWidth = $state(0);
</script>

<svelte:window
  bind:scrollY
  bind:innerWidth
  onresize={() => console.log('resized')}
  onkeydown={(e) => console.log(e.key)}
/>

<svelte:document> — Document Bindings

<svelte:document
  bind:fullscreenElement
  bind:visibilityState
  onclick={handleClick}
/>

<svelte:body> — Body Element Bindings

<svelte:body onmouseenter={() => bodyHovered = true} />

<svelte:head> — Document Head

<svelte:head>
  <title>My Page Title</title>
  <meta name="description" content="Page description" />
  <link rel="stylesheet" href="/fonts.css" />
</svelte:head>

<svelte:self> — Recursive Components

<!-- TreeNode.svelte -->
<script>
  let { node } = $props();
</script>

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

<svelte:fragment> — Slot Targeting (Legacy)

In legacy slot mode, render content into a named slot without a wrapper element:

<Layout>
  <svelte:fragment slot="header">
    <h1>Title</h1>
    <p>Subtitle</p>
  </svelte:fragment>
</Layout>

<svelte:boundary> — Error and Loading Boundaries

Catch errors or show loading states for async content:

<svelte:boundary>
  <AsyncComponent />

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

Note

<svelte:boundary> with the failed snippet creates an error boundary similar to React's ErrorBoundary. When a child component throws during rendering or in an async operation, the failed snippet is rendered instead.

4.9 Stores (svelte/store)

Stores are observable objects that work across component boundaries and in non-component code. They remain useful in Svelte 5 alongside runes.

Writable Store

// stores.js
import { writable } from 'svelte/store';

export const count = writable(0);
export const user = writable(null);
<script>
  import { count } from './stores.js';

  // Auto-subscribe with $ prefix (component-only)
  // Subscription is cleaned up automatically on destroy
</script>

<!-- $-prefix auto-subscribes in templates -->
<p>{$count}</p>
<button onclick={() => count.update(n => n + 1)}>+</button>
<button onclick={() => count.set(0)}>Reset</button>
// In non-component code
import { get } from 'svelte/store';
import { count } from './stores.js';

// Manual subscribe
const unsubscribe = count.subscribe(value => {
  console.log(value);
});
unsubscribe(); // clean up

// Get current value without subscribing
const current = get(count);

Readable Store

import { readable } from 'svelte/store';

export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => set(new Date()), 1000);
  return () => clearInterval(interval); // cleanup
});

Derived Store

import { derived, readable } from 'svelte/store';

const time = readable(new Date(), /* ... */);

export const hours = derived(time, $time => $time.getHours());

// Multiple source stores
export const summary = derived(
  [firstName, lastName],
  ([$first, $last]) => `${$first} ${$last}`
);

// Async derived
export const data = derived(url, ($url, set) => {
  fetch($url)
    .then(r => r.json())
    .then(set);
});

Custom Stores

import { writable } from 'svelte/store';

function createCart() {
  const { subscribe, set, update } = writable([]);

  return {
    subscribe,
    add: (item) => update(items => [...items, item]),
    remove: (id) => update(items => items.filter(i => i.id !== id)),
    clear: () => set([]),
    get total() {
      // Note: this doesn't work reactively as a getter on the store object
      // Use a derived store for reactive totals
    }
  };
}

export const cart = createCart();

Stores in Runes Mode

Stores and runes compose naturally:

<script>
  import { writable } from 'svelte/store';
  import { count } from './stores.js';

  // Convert store to rune-friendly reactive value
  let countValue = $derived($count); // uses auto-subscribe

  // Or use a local store reactively
  const local = writable(0);
</script>

<p>{$local}</p>
<button onclick={() => local.update(n => n + 1)}>+</button>

4.10 Motion (svelte/motion)

tweened — Interpolated Values

<script>
  import { tweened } from 'svelte/motion';
  import { cubicOut } from 'svelte/easing';

  const progress = tweened(0, {
    duration: 400,
    easing: cubicOut
  });

  function fill() {
    progress.set(1);
  }
</script>

<button onclick={fill}>Fill</button>
<progress value={$progress}></progress>

Tweened supports numbers, arrays, and objects with numeric values:

const coords = tweened({ x: 0, y: 0 }, { duration: 300 });
await coords.set({ x: 100, y: 200 });

Options:

  • duration — ms (or function (from, to) => ms)
  • easing — easing function from svelte/easing
  • interpolate — custom (from, to) => (t) => value function
  • delay — ms before starting

spring — Physics-Based Animation

<script>
  import { spring } from 'svelte/motion';

  const coords = spring({ x: 0, y: 0 }, {
    stiffness: 0.1,  // 0–1, lower = slower
    damping: 0.25,   // 0–1, lower = more oscillation
    precision: 0.01  // threshold to consider "settled"
  });

  function handleMouseMove(event) {
    coords.set({ x: event.clientX, y: event.clientY });
  }
</script>

<svelte:window onmousemove={handleMouseMove} />
<div
  style:left="{$coords.x}px"
  style:top="{$coords.y}px"
  style="position: fixed"
>
  🐼
</div>

You can update spring properties reactively:

coords.stiffness = 0.3;
coords.damping = 0.5;

4.11 Transitions (svelte/transition)

fade

<script>
  import { fade } from 'svelte/transition';
</script>

<div transition:fade={{ duration: 300 }}>Fades</div>

Parameters: delay, duration, easing

blur

<div transition:blur={{ amount: 10, duration: 400 }}>Blurs</div>

Parameters: delay, duration, easing, opacity, amount

fly

<div transition:fly={{ x: 0, y: -50, duration: 400 }}>Flies in from above</div>

Parameters: delay, duration, easing, x, y, opacity

slide

<div transition:slide={{ duration: 300, axis: 'y' }}>
  Slides open/close
</div>

Parameters: delay, duration, easing, axis ('x' or 'y')

scale

<div transition:scale={{ start: 0.5, opacity: 0, duration: 300 }}>
  Scales in
</div>

Parameters: delay, duration, easing, start, opacity

draw (SVG only)

<svg>
  <path
    transition:draw={{ duration: 1000, speed: 0.5 }}
    d="M0,0 L100,100"
  />
</svg>

Parameters: delay, speed, duration, easing

crossfade — Shared Element Transitions

<script>
  import { crossfade } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  const [send, receive] = crossfade({
    duration: (d) => Math.sqrt(d * 200),
    fallback: fly
  });
</script>

<!-- List A -->
{#each todoItems as item (item.id)}
  <li in:receive={{ key: item.id }} out:send={{ key: item.id }}>
    {item.text}
  </li>
{/each}

<!-- List B (done items) -->
{#each doneItems as item (item.id)}
  <li in:receive={{ key: item.id }} out:send={{ key: item.id }}>
    ✓ {item.text}
  </li>
{/each}

Custom Transitions

// Custom transition function
function typewriter(node, { speed = 1 }) {
  const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;
  if (!valid) {
    throw new Error('This transition only works on elements with a single text node');
  }

  const text = node.textContent;
  const duration = text.length / (speed * 0.01);

  return {
    duration,
    tick: (t) => {
      const i = Math.trunc(text.length * t);
      node.textContent = text.slice(0, i);
    }
  };
}
<p transition:typewriter={{ speed: 2 }}>
  Hello, world!
</p>

Transition Events

<div
  transition:fade
  onintrostart={() => console.log('intro started')}
  onintroend={() => console.log('intro ended')}
  onoutrostart={() => console.log('outro started')}
  onoutroend={() => console.log('outro ended')}
>
  Content
</div>

4.12 Animation (svelte/animate)

flip — First, Last, Invert, Play

FLIP animations automatically animate list items when they reorder:

<script>
  import { flip } from 'svelte/animate';
  import { quintOut } from 'svelte/easing';

  let items = $state(['Alice', 'Bob', 'Charlie', 'Dave']);

  function shuffle() {
    items = [...items].sort(() => Math.random() - 0.5);
  }
</script>

<button onclick={shuffle}>Shuffle</button>

<ul>
  {#each items as item (item)}
    <li animate:flip={{ duration: 400, easing: quintOut }}>
      {item}
    </li>
  {/each}
</ul>

Parameters: delay, duration, easing

4.13 Easing Functions (svelte/easing)

All standard easing functions are available:

Function Description
linear Constant speed
quadIn, quadOut, quadInOut Quadratic
cubicIn, cubicOut, cubicInOut Cubic
quartIn, quartOut, quartInOut Quartic
quintIn, quintOut, quintInOut Quintic
sineIn, sineOut, sineInOut Sinusoidal
expoIn, expoOut, expoInOut Exponential
circIn, circOut, circInOut Circular
backIn, backOut, backInOut Overshoots slightly
bounceIn, bounceOut, bounceInOut Bounces
elasticIn, elasticOut, elasticInOut Spring-like wobble
<script>
  import { fade } from 'svelte/transition';
  import { elasticOut, bounceOut } from 'svelte/easing';
</script>

<div transition:fade={{ easing: elasticOut, duration: 600 }}>
  Elastic fade
</div>

4.14 Reactive Utilities (svelte/reactivity)

Reactive wrappers for built-in JavaScript objects:

<script>
  import {
    SvelteDate,
    SvelteMap,
    SvelteSet,
    SvelteURL,
    SvelteURLSearchParams
  } from 'svelte/reactivity';

  // Reactive Date — updates when methods mutate the date
  const now = new SvelteDate();
  setInterval(() => now.setMilliseconds(Date.now()), 1000);

  // Reactive Map — updates on set/delete/clear
  const map = new SvelteMap([['key', 'value']]);
  map.set('another', 'item');

  // Reactive Set — updates on add/delete/clear
  const set = new SvelteSet([1, 2, 3]);
  set.add(4);

  // Reactive URL
  const url = new SvelteURL('https://example.com/path?q=hello');
  url.searchParams.set('page', '2'); // triggers update
</script>

<p>Now: {now.toLocaleTimeString()}</p>
<p>Map size: {map.size}</p>
<p>URL: {url.href}</p>

Window Reactive Values (svelte/reactivity/window)

<script>
  import {
    innerWidth,
    innerHeight,
    outerWidth,
    outerHeight,
    scrollX,
    scrollY,
    devicePixelRatio
  } from 'svelte/reactivity/window';
</script>

<p>
  Window: {$innerWidth} × {$innerHeight}
  (DPR: {$devicePixelRatio})
</p>

4.15 Rendering API

mount — Client-Side Rendering

import { mount } from 'svelte';
import App from './App.svelte';

const app = mount(App, {
  target: document.getElementById('app'), // required
  props: { name: 'world' },              // optional
  anchor: null,                           // insert before this element
  context: new Map([['key', 'value']]),  // initial context
  intro: false                            // animate on initial render
});

unmount — Remove a Component

import { unmount } from 'svelte';

// Later, when done:
unmount(app);

hydrate — SSR Hydration

import { hydrate } from 'svelte';
import App from './App.svelte';

// Attaches event listeners to server-rendered HTML
const app = hydrate(App, {
  target: document.getElementById('app'),
  props: { /* must match SSR props */ }
});

4.16 Utilities

tick() — Await DOM Updates

<script>
  import { tick } from 'svelte';

  let messages = $state([]);
  let container;

  async function addMessage(text) {
    messages.push(text);
    await tick(); // Wait for DOM to update
    container.scrollTop = container.scrollHeight;
  }
</script>

untrack() — Opt Out of Tracking

<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let threshold = $state(10);

  $effect(() => {
    // This effect only tracks `count`, not `threshold`
    if (count > untrack(() => threshold)) {
      console.log('Count exceeded threshold!');
    }
  });
</script>

flushSync() — Synchronous State Flush

Force all pending state updates to apply synchronously (useful for testing):

import { flushSync } from 'svelte';

count++;
flushSync(); // DOM is now updated
expect(document.querySelector('p').textContent).toBe('1');

fork() — Off-Screen Preloading (since 5.42)

Create a detached copy of the current reactive context for preloading:

import { fork, mount, unmount } from 'svelte';

const branch = fork();
// Run computations without affecting the main tree
const result = await branch.run(() => expensiveComputation());

settled() — Await All Async Work

import { settled } from 'svelte';

// After triggering async updates
await settled();
// All async operations and state updates are now complete

getAbortSignal() — Reactive AbortSignal

Returns an AbortSignal that aborts when the current derived value or effect re-runs — perfect for cancelling fetch requests:

<script>
  import { getAbortSignal } from 'svelte';

  let userId = $state(1);

  let userData = $derived.by(async () => {
    const signal = getAbortSignal();
    const response = await fetch(`/api/users/${userId}`, { signal });
    return response.json();
  });
</script>

5. Common Tasks

5.1 Building a Form

<script>
  let form = $state({
    name: '',
    email: '',
    message: '',
    agree: false
  });

  let errors = $state({});
  let submitting = $state(false);
  let submitted = $state(false);

  function validate() {
    const e = {};
    if (!form.name.trim()) e.name = 'Name is required';
    if (!form.email.includes('@')) e.email = 'Valid email required';
    if (form.message.length < 10) e.message = 'Message must be at least 10 characters';
    if (!form.agree) e.agree = 'You must agree to the terms';
    return e;
  }

  async function handleSubmit(event) {
    event.preventDefault();
    errors = validate();
    if (Object.keys(errors).length > 0) return;

    submitting = true;
    try {
      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify($state.snapshot(form))
      });
      submitted = true;
    } finally {
      submitting = false;
    }
  }
</script>

{#if submitted}
  <p class="success">Thank you! We'll be in touch.</p>
{:else}
  <form onsubmit={handleSubmit}>
    <div>
      <label for="name">Name</label>
      <input id="name" bind:value={form.name} />
      {#if errors.name}<span class="error">{errors.name}</span>{/if}
    </div>

    <div>
      <label for="email">Email</label>
      <input id="email" type="email" bind:value={form.email} />
      {#if errors.email}<span class="error">{errors.email}</span>{/if}
    </div>

    <div>
      <label for="message">Message</label>
      <textarea id="message" bind:value={form.message}></textarea>
      {#if errors.message}<span class="error">{errors.message}</span>{/if}
    </div>

    <div>
      <label>
        <input type="checkbox" bind:checked={form.agree} />
        I agree to the terms
      </label>
      {#if errors.agree}<span class="error">{errors.agree}</span>{/if}
    </div>

    <button type="submit" disabled={submitting}>
      {submitting ? 'Sending...' : 'Send'}
    </button>
  </form>
{/if}

5.2 Fetching Data

<script>
  let userId = $state(1);
  let userPromise = $derived(fetchUser(userId));

  async function fetchUser(id) {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }
</script>

<label>
  User ID:
  <input type="number" bind:value={userId} min="1" max="10" />
</label>

{#await userPromise}
  <p>Loading user {userId}...</p>
{:then user}
  <h2>{user.name}</h2>
  <p>{user.email}</p>
{:catch error}
  <p class="error">Failed: {error.message}</p>
{/await}

Fetch with Cancellation

<script>
  import { getAbortSignal } from 'svelte';

  let query = $state('');

  let results = $derived.by(async () => {
    if (!query) return [];
    const signal = getAbortSignal();
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
    return res.json();
  });
</script>

<input bind:value={query} placeholder="Search..." />

{#await results}
  <p>Searching...</p>
{:then items}
  {#each items as item}
    <div>{item.title}</div>
  {/each}
{/await}

5.3 Global State with Runes (.svelte.js modules)

Create shared reactive state that works across components:

// state/cart.svelte.js
let items = $state([]);
let discount = $state(0);

const total = $derived(
  items.reduce((sum, item) => sum + item.price * item.qty, 0) * (1 - discount)
);

export function useCart() {
  return {
    get items() { return items; },
    get total() { return total; },
    addItem(item) {
      const existing = items.find(i => i.id === item.id);
      if (existing) existing.qty++;
      else items.push({ ...item, qty: 1 });
    },
    removeItem(id) {
      items = items.filter(i => i.id !== id);
    },
    applyDiscount(pct) {
      discount = pct / 100;
    },
    clear() {
      items = [];
      discount = 0;
    }
  };
}
<!-- CartIcon.svelte -->
<script>
  import { useCart } from '../state/cart.svelte.js';
  const cart = useCart();
</script>

<span>{cart.items.length} items</span>
<span>${cart.total.toFixed(2)}</span>

5.4 Modal Dialog

<!-- Modal.svelte -->
<script>
  let { open = $bindable(false), title, children } = $props();
  import { fade, scale } from 'svelte/transition';

  function close() { open = false; }

  function handleKeydown(e) {
    if (e.key === 'Escape') close();
  }
</script>

<svelte:window onkeydown={handleKeydown} />

{#if open}
  <!-- Backdrop -->
  <div
    class="overlay"
    transition:fade={{ duration: 150 }}
    onclick={close}
    role="presentation"
  ></div>

  <!-- Dialog -->
  <div
    class="modal"
    transition:scale={{ start: 0.95, duration: 150 }}
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
  >
    <div class="modal-header">
      <h2 id="modal-title">{title}</h2>
      <button onclick={close} aria-label="Close">&times;</button>
    </div>
    <div class="modal-body">
      {@render children()}
    </div>
  </div>
{/if}

<style>
  .overlay {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.5);
    z-index: 100;
  }
  .modal {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: white;
    border-radius: 8px;
    padding: 1.5rem;
    z-index: 101;
    min-width: 400px;
    max-width: 90vw;
  }
</style>

Usage:

<script>
  import Modal from './Modal.svelte';
  let showModal = $state(false);
</script>

<button onclick={() => showModal = true}>Open Modal</button>

<Modal bind:open={showModal} title="Confirm Action">
  <p>Are you sure you want to proceed?</p>
  <button onclick={() => showModal = false}>Cancel</button>
  <button onclick={confirmAction}>Confirm</button>
</Modal>

5.5 Toast Notifications

// toast.svelte.js
let toasts = $state([]);
let nextId = 0;

export function useToasts() {
  return {
    get list() { return toasts; },
    add(message, type = 'info', duration = 3000) {
      const id = ++nextId;
      toasts.push({ id, message, type });
      if (duration > 0) setTimeout(() => this.remove(id), duration);
      return id;
    },
    remove(id) {
      toasts = toasts.filter(t => t.id !== id);
    }
  };
}
<!-- ToastContainer.svelte -->
<script>
  import { useToasts } from './toast.svelte.js';
  import { fly } from 'svelte/transition';
  import { flip } from 'svelte/animate';

  const toasts = useToasts();
</script>

<div class="toast-container">
  {#each toasts.list as toast (toast.id)}
    <div
      class="toast {toast.type}"
      animate:flip={{ duration: 200 }}
      in:fly={{ x: 300, duration: 300 }}
      out:fly={{ x: 300, duration: 200 }}
    >
      <span>{toast.message}</span>
      <button onclick={() => toasts.remove(toast.id)}>&times;</button>
    </div>
  {/each}
</div>

5.6 Infinite Scroll

<script>
  import { onMount } from 'svelte';

  let items = $state([]);
  let page = $state(0);
  let loading = $state(false);
  let hasMore = $state(true);
  let sentinel;

  async function loadMore() {
    if (loading || !hasMore) return;
    loading = true;
    try {
      const res = await fetch(`/api/items?page=${page}`);
      const data = await res.json();
      items.push(...data.items);
      hasMore = data.hasMore;
      page++;
    } finally {
      loading = false;
    }
  }

  onMount(() => {
    loadMore(); // Initial load

    const observer = new IntersectionObserver(
      (entries) => { if (entries[0].isIntersecting) loadMore(); },
      { threshold: 0.1 }
    );

    if (sentinel) observer.observe(sentinel);
    return () => observer.disconnect();
  });
</script>

{#each items as item (item.id)}
  <div class="item">{item.title}</div>
{/each}

<div bind:this={sentinel}>
  {#if loading}
    <p>Loading...</p>
  {:else if !hasMore}
    <p>All items loaded.</p>
  {/if}
</div>

5.7 Drag and Drop

<script>
  let items = $state(['Alpha', 'Beta', 'Gamma', 'Delta']);
  let dragging = $state(null);

  function dragstart(event, index) {
    dragging = index;
    event.dataTransfer.effectAllowed = 'move';
  }

  function dragover(event, index) {
    event.preventDefault();
    if (dragging === null || dragging === index) return;

    const newItems = [...items];
    const [removed] = newItems.splice(dragging, 1);
    newItems.splice(index, 0, removed);
    items = newItems;
    dragging = index;
  }

  function dragend() {
    dragging = null;
  }
</script>

<ul>
  {#each items as item, i (item)}
    <li
      draggable="true"
      class:dragging={dragging === i}
      ondragstart={(e) => dragstart(e, i)}
      ondragover={(e) => dragover(e, i)}
      ondragend={dragend}
    >
      {item}
    </li>
  {/each}
</ul>

5.8 Tabbed Interface

<script>
  let tabs = $state([
    { id: 'overview', label: 'Overview' },
    { id: 'details', label: 'Details' },
    { id: 'history', label: 'History' }
  ]);
  let activeTab = $state('overview');
</script>

<div role="tablist">
  {#each tabs as tab}
    <button
      role="tab"
      aria-selected={activeTab === tab.id}
      aria-controls="panel-{tab.id}"
      onclick={() => activeTab = tab.id}
    >
      {tab.label}
    </button>
  {/each}
</div>

{#each tabs as tab}
  <div
    role="tabpanel"
    id="panel-{tab.id}"
    hidden={activeTab !== tab.id}
  >
    {#if tab.id === 'overview'}
      <p>Overview content...</p>
    {:else if tab.id === 'details'}
      <p>Details content...</p>
    {:else}
      <p>History content...</p>
    {/if}
  </div>
{/each}

5.9 Debounced Input

<script>
  let query = $state('');
  let debouncedQuery = $state('');
  let timer;

  $effect(() => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      debouncedQuery = query;
    }, 300);

    return () => clearTimeout(timer);
  });

  let results = $derived(debouncedQuery ? search(debouncedQuery) : []);
</script>

<input bind:value={query} placeholder="Type to search..." />
<p>Searching for: {debouncedQuery}</p>

5.10 Accordion Component

<!-- Accordion.svelte -->
<script>
  let { items } = $props();
  let openIndex = $state(null);

  function toggle(i) {
    openIndex = openIndex === i ? null : i;
  }
</script>

{#each items as item, i}
  <div class="accordion-item">
    <button
      class="accordion-header"
      aria-expanded={openIndex === i}
      onclick={() => toggle(i)}
    >
      {item.title}
      <span class="icon">{openIndex === i ? '−' : '+'}</span>
    </button>

    {#if openIndex === i}
      <div
        class="accordion-body"
        transition:slide={{ duration: 200 }}
      >
        {item.body}
      </div>
    {/if}
  </div>
{/each}

5.11 Virtual List (Windowing)

For very large lists, render only visible items:

<script>
  let items = $state(Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` })));
  const ITEM_HEIGHT = 40;
  const VISIBLE_COUNT = 20;
  const BUFFER = 5;

  let scrollTop = $state(0);
  let containerHeight = $state(800);

  let visibleStart = $derived(Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER));
  let visibleEnd = $derived(Math.min(items.length, visibleStart + VISIBLE_COUNT + BUFFER * 2));
  let visibleItems = $derived(items.slice(visibleStart, visibleEnd));
  let totalHeight = $derived(items.length * ITEM_HEIGHT);
  let offsetY = $derived(visibleStart * ITEM_HEIGHT);
</script>

<div
  style:height="{containerHeight}px"
  style="overflow-y: auto"
  onscroll={(e) => scrollTop = e.target.scrollTop}
>
  <div style:height="{totalHeight}px" style="position: relative">
    <div style:transform="translateY({offsetY}px)">
      {#each visibleItems as item (item.id)}
        <div style:height="{ITEM_HEIGHT}px">{item.text}</div>
      {/each}
    </div>
  </div>
</div>

5.12 Custom Store Pattern — User Session

// session.svelte.js
import { writable, derived, get } from 'svelte/store';

const _user = writable(null);
const _token = writable(localStorage.getItem('token'));

export const user = { subscribe: _user.subscribe };
export const isLoggedIn = derived(_user, $u => $u !== null);

export async function login(email, password) {
  const res = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });
  if (!res.ok) throw new Error('Login failed');
  const { user: userData, token } = await res.json();
  _user.set(userData);
  _token.set(token);
  localStorage.setItem('token', token);
}

export function logout() {
  _user.set(null);
  _token.set(null);
  localStorage.removeItem('token');
}

export function getAuthHeader() {
  const token = get(_token);
  return token ? { Authorization: `Bearer ${token}` } : {};
}

5.13 Routing with SvelteKit

SvelteKit uses a file-based router. Files in src/routes/ define pages:

src/routes/
  +page.svelte          → /
  +layout.svelte        → shared layout wrapper
  about/
    +page.svelte        → /about
  blog/
    +page.svelte        → /blog
    +page.ts            → data loading for /blog
    [slug]/
      +page.svelte      → /blog/my-post
      +page.ts          → data loading with slug param
  api/
    users/
      +server.ts        → /api/users (REST endpoint)

Page with data loading:

// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params, fetch }) => {
  const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json());
  return { post };
};
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
  let { data } = $props();
</script>

<article>
  <h1>{data.post.title}</h1>
  <div>{@html data.post.content}</div>
</article>

5.14 Reactive Class ($state + class pattern)

// Counter.svelte.js
export class Counter {
  count = $state(0);
  step = $state(1);
  history = $state([]);

  doubled = $derived(this.count * 2);

  increment() {
    this.count += this.step;
    this.history.push({ action: 'increment', value: this.count });
  }

  reset() {
    this.count = 0;
    this.history = [];
  }
}
<script>
  import { Counter } from './Counter.svelte.js';
  const counter = new Counter();
</script>

<button onclick={() => counter.increment()}>+{counter.step}</button>
<p>Count: {counter.count} (Doubled: {counter.doubled})</p>

5.15 Server-Side Rendering (SSR)

For standalone Svelte (not SvelteKit), use svelte/server:

// server.js
import { render } from 'svelte/server';
import App from './App.svelte';

const { head, body } = render(App, {
  props: { name: 'World' }
});

const html = `<!DOCTYPE html>
<html>
  <head>${head}</head>
  <body>${body}</body>
</html>`;

Then hydrate on the client:

// client.js
import { hydrate } from 'svelte';
import App from './App.svelte';

hydrate(App, {
  target: document.body,
  props: { name: 'World' }
});

5.16 Custom Elements (Web Components)

<svelte:options customElement="my-greeting" />

<script>
  let { name = 'World' } = $props();
</script>

<h1>Hello, {name}!</h1>

<style>
  h1 { color: navy; }
</style>

Build it:

// vite.config.js
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default {
  plugins: [svelte({
    compilerOptions: { customElement: true }
  })],
  build: {
    lib: {
      entry: 'src/my-greeting.svelte',
      formats: ['es']
    }
  }
};

Usage in plain HTML:

<script type="module" src="./my-greeting.js"></script>
<my-greeting name="Alice"></my-greeting>

Custom element with advanced options:

<svelte:options customElement={{
  tag: 'my-widget',
  shadow: 'none',           // 'open' | 'none'
  props: {
    name: { type: String },
    count: { type: Number, attribute: 'data-count' }
  }
}} />

5.17 {#snippet} Advanced Patterns

Snippets as Layout Components

<!-- DataTable.svelte -->
<script>
  let { data, columns, emptyMessage = 'No data', actions } = $props();
</script>

<table>
  <thead>
    <tr>
      {#each columns as col}
        <th>{col.label}</th>
      {/each}
      {#if actions}<th>Actions</th>{/if}
    </tr>
  </thead>
  <tbody>
    {#each data as row (row.id)}
      <tr>
        {#each columns as col}
          <td>{row[col.key]}</td>
        {/each}
        {#if actions}
          <td>{@render actions(row)}</td>
        {/if}
      </tr>
    {:else}
      <tr><td colspan={columns.length + (actions ? 1 : 0)}>{emptyMessage}</td></tr>
    {/each}
  </tbody>
</table>
<DataTable {data} {columns}>
  {#snippet actions(row)}
    <button onclick={() => edit(row)}>Edit</button>
    <button onclick={() => deleteRow(row.id)}>Delete</button>
  {/snippet}
</DataTable>

5.18 Transition Recipe: Page Transitions

<!-- +layout.svelte (SvelteKit) -->
<script>
  import { fade } from 'svelte/transition';
  let { children } = $props();
</script>

{#key page.url.pathname}
  <div transition:fade={{ duration: 200 }}>
    {@render children()}
  </div>
{/key}

5.19 Binding to Media Elements

<script>
  let paused = $state(true);
  let currentTime = $state(0);
  let duration = $state(0);
  let volume = $state(1);
  let video;

  let progress = $derived(duration ? currentTime / duration : 0);
</script>

<video
  bind:this={video}
  bind:paused
  bind:currentTime
  bind:duration
  bind:volume
  src="/video.mp4"
></video>

<div class="controls">
  <button onclick={() => paused = !paused}>
    {paused ? '▶' : '⏸'}
  </button>
  <input
    type="range" min="0" max="1" step="0.01"
    bind:value={progress}
    oninput={(e) => currentTime = e.target.value * duration}
  />
  <span>{Math.floor(currentTime)}s / {Math.floor(duration)}s</span>
</div>

5.20 Keyboard Shortcuts

<script>
  function handleKeydown(event) {
    if (event.ctrlKey && event.key === 's') {
      event.preventDefault();
      save();
    }
    if (event.key === 'Escape') {
      closeModal();
    }
    if (event.key === '?' && !event.target.closest('input, textarea')) {
      showHelp = true;
    }
  }
</script>

<svelte:window onkeydown={handleKeydown} />

5.21 Clipboard Operations

<script>
  let copied = $state(false);

  async function copyToClipboard(text) {
    try {
      await navigator.clipboard.writeText(text);
      copied = true;
      setTimeout(() => copied = false, 2000);
    } catch {
      // Fallback for older browsers
      const el = document.createElement('textarea');
      el.value = text;
      document.body.appendChild(el);
      el.select();
      document.execCommand('copy');
      el.remove();
    }
  }
</script>

<button onclick={() => copyToClipboard('Hello!')}>
  {copied ? '✓ Copied!' : 'Copy'}
</button>

5.22 Dark Mode with Context

// theme.svelte.js
const THEME_KEY = Symbol('theme');

export function createTheme() {
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  let dark = $state(prefersDark);

  return {
    get dark() { return dark; },
    toggle() { dark = !dark; },
    setDark(value) { dark = value; }
  };
}
<!-- App.svelte -->
<script>
  import { setContext } from 'svelte';
  import { createTheme } from './theme.svelte.js';

  const THEME_KEY = Symbol('theme');
  const theme = createTheme();
  setContext(THEME_KEY, theme);
</script>

<div class:dark={theme.dark} class="app">
  <!-- children -->
</div>

5.23 Intersection Observer Action

// actions/observe.js
export function observe(element, { callback, options = {} } = {}) {
  const observer = new IntersectionObserver(([entry]) => {
    callback(entry.isIntersecting, entry);
  }, options);

  observer.observe(element);

  return {
    update({ callback: newCallback }) {
      callback = newCallback;
    },
    destroy() {
      observer.disconnect();
    }
  };
}
<script>
  import { observe } from './actions/observe.js';
  let visible = $state(false);
</script>

<div
  use:observe={{ callback: (isVisible) => visible = isVisible }}
  class:visible
>
  {visible ? 'I am visible!' : 'Scroll to see me'}
</div>

5.24 Persistent State (localStorage)

// persisted.svelte.js
export function persisted(key, initialValue) {
  let stored;
  try {
    const raw = localStorage.getItem(key);
    stored = raw ? JSON.parse(raw) : initialValue;
  } catch {
    stored = initialValue;
  }

  let value = $state(stored);

  $effect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // Storage full or unavailable
    }
  });

  return {
    get value() { return value; },
    set value(v) { value = v; }
  };
}
<script>
  import { persisted } from './persisted.svelte.js';
  const theme = persisted('theme', 'light');
</script>

<button onclick={() => theme.value = theme.value === 'light' ? 'dark' : 'light'}>
  Current: {theme.value}
</button>

5.25 Composable Reactive Logic (Custom Hooks)

// hooks/useWindowSize.svelte.js
export function useWindowSize() {
  let width = $state(window.innerWidth);
  let height = $state(window.innerHeight);

  function update() {
    width = window.innerWidth;
    height = window.innerHeight;
  }

  $effect(() => {
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  });

  return {
    get width() { return width; },
    get height() { return height; }
  };
}
<script>
  import { useWindowSize } from './hooks/useWindowSize.svelte.js';
  const size = useWindowSize();
</script>

<p>Window: {size.width} × {size.height}</p>

5.26 Animation Recipe: Staggered List Entry

<script>
  import { fly } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let items = $state([]);

  async function loadItems() {
    items = await fetchItems();
  }
</script>

{#each items as item, i (item.id)}
  <div
    in:fly={{
      y: 20,
      duration: 300,
      delay: i * 50, // stagger by 50ms per item
      easing: quintOut
    }}
  >
    {item.name}
  </div>
{/each}

5.27 Optimistic UI Updates

<script>
  let todos = $state([]);

  async function toggleTodo(id) {
    // Optimistically update
    const todo = todos.find(t => t.id === id);
    const previous = todo.done;
    todo.done = !todo.done;

    try {
      await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ done: todo.done })
      });
    } catch {
      // Revert on failure
      todo.done = previous;
    }
  }
</script>

{#each todos as todo (todo.id)}
  <label>
    <input type="checkbox" checked={todo.done} onchange={() => toggleTodo(todo.id)} />
    {todo.text}
  </label>
{/each}

5.28 Accessibility: Focus Management

<script>
  import { tick } from 'svelte';

  let dialogOpen = $state(false);
  let dialog;
  let trigger;

  async function openDialog() {
    dialogOpen = true;
    await tick();
    // Move focus to dialog
    dialog?.focus();
  }

  async function closeDialog() {
    dialogOpen = false;
    await tick();
    // Return focus to trigger
    trigger?.focus();
  }
</script>

<button bind:this={trigger} onclick={openDialog}>Open</button>

{#if dialogOpen}
  <div
    role="dialog"
    aria-modal="true"
    tabindex="-1"
    bind:this={dialog}
    onkeydown={(e) => e.key === 'Escape' && closeDialog()}
  >
    <button onclick={closeDialog}>Close</button>
  </div>
{/if}

6. Troubleshooting

Error: lifecycle_outside_component

Cause: Calling onMount, onDestroy, setContext, or getContext outside of a component's initialization phase. Common in SSR bundlers like Rollup/Rolldown (issue #17027).

Solution:

  1. Ensure lifecycle calls are at the top level of a <script> block, not inside callbacks or async functions.
  2. For SSR with Rollup/Rolldown, verify you are not accidentally calling component code in a non-component context.
  3. Use $effect.root() for effects that need to run outside component lifecycle.

Prevention: Only call lifecycle functions synchronously during component initialization, never inside setTimeout, Promise.then, or event handlers.


Error: Cannot read properties of undefined (reading 'subscribe')

Cause: Using the $store auto-subscribe syntax on a value that is not a store (not an object with a subscribe method), or importing a store before it is initialized.

Solution:

  1. Verify the imported value is actually a Svelte store (has .subscribe).
  2. Check for circular imports that may result in undefined at import time.
  3. Use optional chaining: $store?.property during initialization.

Prevention: Export stores from a dedicated module without circular dependencies.


Error: derived_inert warning

Cause: A $derived value exists but is never read, making it inert (issue #18097). The compiler warns because derived computations that are never consumed may indicate a logic bug.

Solution:

  1. Read the derived value in the template or in an $effect.
  2. If you only need the side effect, use $effect directly instead of $derived.
  3. To suppress: use void derived_value in an $effect (uncommon pattern).

Prevention: Avoid creating derived values that are only needed for side effects.


Error: Cannot use 'bind:value' on a non-bindable prop

Cause: Attempting to use bind:value on a child component's prop that was not declared with $bindable().

Solution:

  1. In the child component, change let { value } = $props() to let { value = $bindable() } = $props().
  2. Alternatively, pass a callback prop: onchange={(v) => parentValue = v}.

Prevention: Always mark props that should support two-way binding with $bindable().


Warning: ownership_invalid_mutation

Cause: A child component is directly mutating a prop value passed from a parent without $bindable. Svelte detects this because it violates unidirectional data flow.

Solution:

  1. Use $bindable() if two-way binding is intended.
  2. Use callback props to notify the parent, which updates its own state.
  3. Clone the prop locally: let local = $state($state.snapshot(propValue)).

Prevention: Treat props as read-only unless explicitly marked $bindable.


Warning: ownership_invalid_binding

Cause: Binding to a prop of a deeply nested child that does not own that state.

Solution:

  1. Lift state up to the nearest common ancestor.
  2. Use context API to share mutable state across the tree.
  3. Use a shared .svelte.js module for cross-tree state.

Error: state_snapshot_uncloneable

Cause: Calling $state.snapshot() on a value that contains non-cloneable items (functions, DOM elements, circular references).

Solution:

  1. Only snapshot plain data — no functions, no DOM nodes, no class instances with methods.
  2. Use JSON.parse(JSON.stringify(value)) as a manual fallback with awareness of its limitations.

Prevention: Keep state values serializable if you need snapshots.


$effect runs too often / infinite loop

Cause: An $effect reads and then writes to the same reactive state, creating a cycle. For example: reading items and then calling items.push(...) inside the same effect.

Solution:

  1. Use untrack(() => items) to read without registering a dependency.
  2. Separate the read and write into different effects or into event handlers.
  3. Use $derived for derived values instead of writing back to state.

Prevention: Effects should perform side effects (DOM updates, fetch, logging), not compute new state values.


Memory leak: $effect subscribing to parent prop (issue #18103)

Cause: In Svelte 5.55.4, an $effect in a child component that subscribes to a prop passed from a parent can leak the effect's cleanup if the component is destroyed and re-created frequently.

Solution:

  1. Ensure the cleanup function returned from the $effect releases all references.
  2. Use onDestroy in addition to the cleanup return for critical cleanup.
  3. Monitor with browser devtools heap snapshots.

Prevention: Always return a cleanup function from $effect when registering external listeners.


Memory leak: Detached DOM nodes on unresolved promises (issue #18094)

Cause: {#await} blocks with pending promises hold references to detached DOM nodes if the component is destroyed before the promise settles.

Solution:

  1. Cancel the underlying async operation using getAbortSignal() to cause the promise to reject, allowing Svelte to clean up.
  2. Avoid very long-running promises in {#await} blocks — add timeouts.

Prevention: Use getAbortSignal() in all async derived values and effects to enable cancellation.


<video> muted attribute not working (issue #17327)

Cause: The muted attribute on <video> does not render correctly as an HTML attribute in Svelte 5.55.4 due to how boolean attributes are handled.

Solution:

<!-- Workaround: set muted programmatically -->
<video
  bind:this={videoEl}
  onmount={() => videoEl.muted = true}
  autoplay
  src="/video.mp4"
></video>

Or use bind:muted:

<script>
  let muted = $state(true);
</script>
<video bind:muted src="/video.mp4" autoplay></video>

Prevention: Always use bind:muted rather than the static muted attribute on video elements.


Error: {@const} with let: slot prop causes initialization error (issue #18119)

Cause: Using {@const} to reference a let: slot prop in legacy slot mode causes an initialization ordering error in 5.55.4.

Solution:

  1. Migrate to the snippets API ({#snippet} / {@render}) which does not have this issue.
  2. If you must use legacy slots, compute the value inline without {@const}.

Prevention: Prefer the snippets API for all new components.


Style leak with hashed filenames (issue #18092)

Cause: When Svelte hashes component styles using filenames and two files have the same name in different directories, their style hashes can collide, causing style leaks between components.

Solution:

  1. Ensure component filenames are unique across the project.
  2. Configure your build tool to use full path-based hashing if supported.

Prevention: Avoid naming multiple components identically even in different directories.


a11y-mouse-events-have-key-events false positive with focusin/focusout (issue #8089)

Cause: Svelte's accessibility linter incorrectly triggers this warning when using focusin/focusout handlers, even though these are already keyboard-accessible equivalents of mouseenter/mouseleave.

Solution: Suppress the warning per-component:

// svelte.config.js
const config = {
  compilerOptions: {
    warningFilter: (w) => w.code !== 'a11y_mouse_events_have_key_events'
  }
};

Prevention: This is a known false positive — safe to suppress globally.


Error: Cannot destructure property 'x' of undefined

Cause: Trying to destructure reactive state before it is initialized, often in a $derived that runs before async data loads.

Solution:

  1. Provide a default value: let data = $state({ items: [] }).
  2. Use optional chaining: data?.items ?? [].
  3. Guard with {#if data} in the template.

Transition not playing on initial render

Cause: By default, transition:fn does not play when an element enters on first render (only on subsequent {#if} changes).

Solution: Use in:fn instead of transition:fn, and set intro: true in the mount() options:

mount(App, { target, intro: true });

Prevention: Use in: for entrance-only transitions; set intro: true when mount-time animations are needed.


Components not updating after store mutation

Cause: Directly mutating an object or array inside a writable store without calling .set() or .update() — stores require explicit notification.

Solution:

// WRONG — no notification
items.update(v => { v.push(newItem); return v; }); // same reference

// CORRECT — return new reference or use .set
items.update(v => [...v, newItem]); // new array
// or
const current = get(items);
current.push(newItem);
items.set(current); // explicit notification

Prevention: For deeply reactive objects, prefer $state in .svelte.js modules over writable stores.


SSR: window is not defined

Cause: Code that references browser globals (window, document, navigator) running during server-side rendering.

Solution:

  1. Move browser-only code into onMount (which never runs on the server).
  2. Guard with if (typeof window !== 'undefined').
  3. Use SvelteKit's browser check:
import { browser } from '$app/environment';
if (browser) { /* browser-only code */ }

Prevention: Always guard browser-specific APIs behind onMount or browser checks.


TypeScript: Type 'X' is not assignable to type 'Y' on props

Cause: Svelte's type inference for $props() requires explicit type annotations or the TypeScript inference cannot determine prop types.

Solution:

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

Prevention: Always type-annotate $props() in TypeScript components.


bind:this returns undefined at first render

Cause: The element reference is only available after the component mounts. Accessing it synchronously in <script> during initialization returns undefined.

Solution:

<script>
  let el;
  onMount(() => {
    // el is available here
    el.focus();
  });
</script>
<input bind:this={el} />

Prevention: Always access bind:this references inside onMount or lifecycle functions.


$effect does not track all dependencies

Cause: Dependencies accessed via untrack() or accessed inside a non-tracking callback (like a setTimeout callback) are not tracked.

Solution:

  1. Read all needed reactive values synchronously at the top of the $effect body.
  2. Avoid accessing reactive state inside setTimeout, Promise.then, etc.

Hydration mismatch: hydration_attribute_changed / hydration_html_changed

Cause: The server-rendered HTML does not match what the client-side component would render. Common causes: date/time values, random IDs, or different data on client vs. server.

Solution:

  1. Ensure SSR and client render with identical data.
  2. For truly client-only content, wrap in {#if browser} or use onMount.
  3. For unavoidable mismatches, these warnings are ignorable — listed in the suppression table.

Prevention: Never generate random values during render. Generate them in onMount instead.


await_waterfall warning

Cause: An {#await} block is nested inside another {#await} block, creating a sequential waterfall instead of parallel fetches.

Solution: Fetch in parallel using Promise.all:

<script>
  // Parallel fetch
  let data = $derived(
    Promise.all([fetchUsers(), fetchProducts()])
      .then(([users, products]) => ({ users, products }))
  );
</script>

Prevention: Use Promise.all() for independent async operations.


Components fail to tree-shake

Cause: Using export { MyComponent } from a barrel file combined with module-level <script module> side effects prevents tree-shaking.

Solution:

  1. Import directly from component files: import MyComponent from './MyComponent.svelte'.
  2. Avoid side effects in <script module> blocks.

await_reactivity_loss warning

Cause: Reactive state is accessed before an await in a derived or effect. After the await, Svelte can no longer track the values accessed before it.

Solution: Access all reactive values before the first await:

<script>
  let data = $derived.by(async () => {
    // Access reactive values BEFORE await
    const id = userId; // capture value
    const result = await fetch(`/api/users/${id}`);
    return result.json();
  });
</script>

Prevention: Capture all reactive reads at the beginning of async derived functions.


CSS not applying to child components

Cause: Svelte's scoped CSS only applies to elements within the same component file. Child component elements are not styled from the parent's <style>.

Solution:

  1. Use :global() to target specific elements: .parent :global(.child-class) { }.
  2. Pass CSS custom properties: <ChildComponent style="--color: red" />.
  3. Use a global stylesheet for shared styles.
  4. Add the class in the child component.

Performance: Too many re-renders

Cause: Creating new object/array references on every render pass, causing derived values and effects to see "new" values even when the content is identical.

Solution:

// WRONG — new array created every render
let filtered = $derived(items.map(i => ({ ...i, label: i.name.toUpperCase() })));

// BETTER — use $derived.by with stable output
let filtered = $derived.by(() => {
  return items.map(i => ({ ...i, label: i.name.toUpperCase() }));
  // Svelte tracks specific fields, not reference equality
});

$effect.pending() with $state.eager broken (issue #18095)

Cause: Known bug in Svelte 5.55.4 — $effect.pending() does not correctly reflect pending state when $state.eager values are involved.

Solution: Avoid combining $effect.pending() and $state.eager until this is fixed in a future release. Use a manual loading flag instead:

<script>
  let loading = $state(false);
  async function load() {
    loading = true;
    try { /* ... */ } finally { loading = false; }
  }
</script>

7. FAQ

Q: What is the difference between Svelte and SvelteKit?

Svelte is the UI component framework and compiler. SvelteKit is the full-stack application framework built on Svelte — it adds routing, server-side rendering, code splitting, API routes, and deployment adapters. For most new projects, start with SvelteKit. Use standalone Svelte only for component libraries or when embedding into another framework.


Q: Do I need to import runes like $state and $derived?

No. Runes are recognized by the Svelte compiler automatically. Never write import { $state } from 'svelte' — it will cause an error. Runes work in .svelte files and .svelte.js/.svelte.ts module files without any import.


Q: What is the difference between $state and writable?

$state is a rune that creates deeply reactive state within components and .svelte.js modules. It works without explicit subscription — the compiler inserts tracking automatically. writable from svelte/store creates a store object that requires explicit subscription (.subscribe() or the $ prefix in templates). Both have their place: $state is preferred for component-local and module-level state; stores remain useful when state must be consumed outside of components (e.g., in vanilla JS).


Q: Can I use Svelte 5 with existing Svelte 4 components?

Yes. Svelte 5 is backward-compatible with Svelte 4 components. Legacy syntax (on:click, slots, createEventDispatcher) continues to work. You can gradually migrate components to runes-based syntax. Components in legacy and runes mode can be mixed in the same project.


Q: How do I migrate from Svelte 4 to Svelte 5?

Use the built-in migration tool:

npx sv migrate svelte-5

Or call the compiler's migrate() function programmatically via svelte/compiler. The tool converts on:event to onevent, <slot> to {#snippet}/{@render}, and export let to $props().


Q: When should I use $effect vs. $derived?

Use $derived when you need a computed value — something that depends on other state and produces a result you read. Use $effect when you need a side effect — something that happens as a consequence of state changes (DOM manipulation, fetch calls, localStorage writes, analytics events). Never write back to reactive state inside $derived; do it in $effect if necessary (but prefer event handlers for user-triggered state changes).


Q: How do I share state between components without prop drilling?

Three patterns, in order of preference:

  1. .svelte.js module — export reactive state: let count = $state(0); export { count }. All importers share the same instance.
  2. Context API — setContext(key, value) in a parent, getContext(key) in any descendant. Good for component-tree-scoped state (like themes, modals).
  3. Svelte stores — writable / readable from svelte/store. The original pattern; still works, especially for state consumed outside of components.

Q: What is the $ prefix in templates ($store, $count)?

The $ prefix inside .svelte templates auto-subscribes to a Svelte store and automatically unsubscribes when the component is destroyed. It only works in the template and script sections of .svelte files — not in .js files. Use get(store) for a one-time read in .js files.


Q: How do I pass HTML content to a component?

Use the snippets API (Svelte 5):

<Card>
  <p>This is <strong>rich</strong> HTML content.</p>
</Card>

The child component receives this as the children snippet and renders it with {@render children()}. For raw HTML strings, use {@html sanitizedString} inside the component.


Q: How do scoped styles work? Can a parent style a child component?

Svelte compiles each component's CSS with a unique scope attribute (e.g., svelte-xyz). Parent styles cannot reach into child components by default — this is intentional. To style child elements from a parent:

  • Use :global(.class) in the parent's style block
  • Pass CSS custom properties from parent to child
  • Let the child expose a class prop and apply styles externally

Q: What is tick() and when should I use it?

tick() returns a promise that resolves after Svelte has finished processing all pending state changes and updated the DOM. Use it when you need to read the updated DOM after changing state — for example, measuring element dimensions or scrolling after new content is added.


Q: How do I handle errors in async components?

Use <svelte:boundary> with a failed snippet:

<svelte:boundary>
  <AsyncComponent />
  {#snippet failed(error, reset)}
    <p>Error: {error.message}</p>
    <button onclick={reset}>Retry</button>
  {/snippet}
</svelte:boundary>

For {#await}, use the {:catch error} block.


Q: How do I do code splitting / lazy loading in SvelteKit?

SvelteKit automatically code-splits at route boundaries. Each route's JavaScript is only loaded when that route is visited. For component-level lazy loading, use dynamic imports:

const HeavyChart = await import('./HeavyChart.svelte');

Q: Can Svelte generate static sites?

Yes. Use @sveltejs/adapter-static in your svelte.config.js. SvelteKit will pre-render all pages to static HTML at build time. For SPA mode (client-side routing only), set fallback: 'index.html' in the adapter options.


Q: How do I test Svelte components?

The recommended approach is Vitest + @testing-library/svelte:

npm install --save-dev vitest @testing-library/svelte @testing-library/jest-dom jsdom
import { render, screen, fireEvent } from '@testing-library/svelte';
import Counter from './Counter.svelte';

test('increments count', async () => {
  render(Counter);
  const button = screen.getByRole('button');
  await fireEvent.click(button);
  expect(screen.getByText('1')).toBeTruthy();
});

For end-to-end tests, use Playwright (included in the sv create scaffold).


Q: What is the difference between transition:, in:, and out:?

  • transition:fn — applies the transition both on entry (when the element is added to the DOM) and exit (when it is removed).
  • in:fn — applies only on entry.
  • out:fn — applies only on exit. Use in: and out: when you want different animations for enter and exit.

Q: How do I animate list reordering?

Use animate:flip on {#each} items (elements must be keyed):

{#each items as item (item.id)}
  <div animate:flip={{ duration: 300 }}>{item.name}</div>
{/each}

FLIP (First, Last, Invert, Play) calculates the start and end positions and animates between them automatically.


Q: How do I use Svelte with TypeScript?

Add lang="ts" to your script block:

<script lang="ts">
  interface User { id: number; name: string; }
  let user: User = $state({ id: 1, name: 'Alice' });
</script>

Run npm run check (svelte-check) for type checking. Generate types for SvelteKit routes by running npm run dev which creates .svelte-kit/tsconfig.json and route type definitions automatically.


Q: What are .svelte.js files?

Files ending in .svelte.js (or .svelte.ts) are Svelte reactive modules — regular JavaScript/TypeScript files where you can use runes ($state, $derived, $effect) outside of components. Use them to create shared reactive state, composable hooks, and reactive class instances that can be imported into multiple components.


Q: How do I make a component that works as both a Svelte component and a custom element?

Add <svelte:options customElement="tag-name" /> to the component. During build, compile with customElement: true. The resulting component can be used as both an imported Svelte component and a registered HTML custom element. Note: custom elements have limitations compared to standard components (shadow DOM, attribute reflection, etc.).


Q: When should I use $state.raw instead of $state?

Use $state.raw for:

  • Very large arrays or objects where deep proxying has a performance cost
  • Data you only replace entirely (never mutate in-place)
  • Third-party objects that break when wrapped in a Proxy
  • Data that should not be reactive at the property level — only at the variable level

Q: How do I share context between a SvelteKit layout and its pages?

Use the SvelteKit data loading system: return data from +layout.ts and access it in +page.svelte via $props().data. For UI state (not data), use setContext in +layout.svelte and getContext in any descendant page.


Q: Why does $derived not update when I expect it to?

Common causes:

  1. The value was accessed inside untrack() — removing it from the dependency graph.
  2. The reactive value was accessed inside an async callback (after an await) — Svelte cannot track async reads.
  3. You are comparing objects by reference and the same object reference is being mutated rather than replaced — use $state.snapshot() to debug.

Q: How do I debounce a reactive value?

// debounced.svelte.js
export function debounced(getValue, delay = 300) {
  let debouncedValue = $state(getValue());
  let timer;

  $effect(() => {
    const current = getValue();
    clearTimeout(timer);
    timer = setTimeout(() => { debouncedValue = current; }, delay);
    return () => clearTimeout(timer);
  });

  return { get value() { return debouncedValue; } };
}

Q: How do I upgrade from Svelte 4 stores to Svelte 5 runes?

Stores still work in Svelte 5 — no forced migration. When you want to upgrade:

  1. Move store logic to a .svelte.js file.
  2. Replace writable(initial) with let value = $state(initial).
  3. Replace derived(store, fn) with let computed = $derived(fn(value)).
  4. Replace store.subscribe + store.set with direct reads and assignments.
  5. Export getter/setter objects instead of store objects.

Q: What Svelte warnings can be safely suppressed?

The following runtime warnings are documented as safe to ignore in appropriate circumstances:

Warning Code When Safe to Ignore
await_waterfall When sequential fetching is intentional
await_reactivity_loss When async read pattern is correct
state_snapshot_uncloneable When you know the uncloneable item
binding_property_non_reactive When the bound property is intentionally non-reactive
hydration_attribute_changed When attribute differences are expected (e.g., dates)
hydration_html_changed When HTML differences are expected
ownership_invalid_binding During migration from legacy patterns
ownership_invalid_mutation During migration from legacy patterns

Q: How does Svelte handle accessibility?

Svelte compiles accessibility warnings directly from the template analyzer. It warns about:

  • Missing alt attributes on images
  • Form inputs without labels
  • Interactive elements without keyboard handlers
  • Invalid ARIA usage
  • Color contrast (via eslint-plugin-svelte)

Configure warnings in svelte.config.js via compilerOptions.warningFilter. Note the false positive with a11y_mouse_events_have_key_events and focusin/focusout (issue #8089) — safe to suppress.


Q: Can I use Svelte without a bundler?

For production use, a bundler (Vite, Rollup, webpack) is required because Svelte's .svelte files need compilation. For quick experiments, svelte.dev/playground runs the compiler in the browser. The Svelte REPL at svelte.dev is the fastest way to prototype without any setup.


Q: How do I profile and optimize a slow Svelte component?

  1. Use $inspect.trace('label') inside effects to identify what triggers re-renders.
  2. Use Chrome DevTools Performance tab — look for long "scripting" frames.
  3. Check for effects that run too frequently using $effect.tracking().
  4. For large lists, implement virtual scrolling (section 5.11).
  5. Use $state.raw for large datasets that only need top-level reactivity.
  6. Avoid creating new object/array references inside $derived unnecessarily.

Q: Does Svelte support progressive enhancement (forms that work without JavaScript)?

Yes, via SvelteKit. Use <form action="/api/endpoint" method="POST"> with SvelteKit form actions. The form works without JavaScript. With JavaScript, enhance it using the enhance directive from $app/forms for a spa-like experience without a full page reload.


Q: What is the difference between onMount cleanup and onDestroy?

onMount accepts a function that can optionally return a cleanup function. That cleanup runs when the component is destroyed (same as onDestroy). The difference: onMount never runs during SSR, so its cleanup is also SSR-safe. onDestroy always runs — including during SSR — making it the right place for cleanup that needs to run in both environments.


Q: How do I share Svelte components as an npm library?

  1. Set package.json exports to point to the .svelte source files (not compiled output).
  2. Include a svelte field in package.json pointing to the main entry.
  3. Add svelte to peerDependencies.
  4. Consider using @sveltejs/package for automatic package.json generation.
{
  "svelte": "./src/index.js",
  "exports": {
    ".": "./src/index.js",
    "./ComponentName": "./src/lib/ComponentName.svelte"
  },
  "peerDependencies": {
    "svelte": "^5.0.0"
  }
}