← Tech Guides
Color Field · Developer Reference

Svelte

The compiler that disappears. Write components as .svelte files — HTML, CSS, and JavaScript that compiles to surgical, framework-free DOM updates. No virtual DOM. No runtime overhead. Just the code your app actually needs.

01

Quick Reference

The essential patterns at a glance. Pin this section and reach for it daily.

New Project

Scaffold a SvelteKit app with Vite

npx sv create my-app
Dev Server

Start development with hot reload

npm run dev -- --open
Build

Compile for production

npm run build
Preview

Preview the production build locally

npm run preview
Add Library

Add integrations via the CLI

npx sv add tailwindcss
Check Types

Run svelte-check for diagnostics

npx svelte-check

Component Anatomy at a Glance

<!-- Counter.svelte -->
<script>
  let count = $state(0);           // reactive state (Svelte 5)
  let doubled = $derived(count * 2); // derived value

  function increment() {
    count++;                        // just mutate — it's reactive
  }
</script>

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

<style>
  button { background: #ff3e00; color: white; }
</style>

Svelte 5 Runes Quick Reference

Rune Purpose Example
$state Declare reactive state let count = $state(0)
$state.raw Non-deeply-reactive state let items = $state.raw([])
$derived Computed from other state let doubled = $derived(count * 2)
$derived.by Complex derived (function body) let total = $derived.by(() => { ... })
$effect Side effects when deps change $effect(() => { console.log(count) })
$effect.pre Run before DOM update $effect.pre(() => { ... })
$props Declare component props let { name, age } = $props()
$bindable Allow two-way binding on a prop let { value = $bindable() } = $props()
$inspect Debug reactive values $inspect(count)
$host Access custom element host $host().dispatchEvent(...)
02

Philosophy

Svelte is a compiler, not a runtime. It shifts the work from the browser to the build step, producing tight, dependency-free JavaScript that surgically updates the DOM.

Compiler, Not Runtime

React, Vue, and Angular ship a runtime library to the browser. That runtime interprets your components, diffs a virtual DOM, and patches changes. Svelte takes a fundamentally different approach: your .svelte files are compiled away at build time into plain JavaScript that directly manipulates the DOM.

Key Insight
The framework disappears at build time. There is no svelte.min.js in your production bundle — just the compiled output of your components.
Traditional Framework
// Ship framework + your code to browser
import { createApp } from 'framework';
// Runtime diffs virtual DOM on every
// state change, then patches real DOM
createApp(App).mount('#app');
Svelte Compiler Output
// No framework in bundle — just your code
// Compiler generates targeted DOM ops:
element.textContent = count;
// Only updates exactly what changed
// No diffing. No virtual DOM.

Core Design Principles

Write Less Code

Svelte components require dramatically less boilerplate than React or Vue. Reactivity is built into the language, not layered on via hooks or APIs.

No Virtual DOM

Virtual DOM diffing is overhead. Svelte compiles your declarative code into imperative DOM operations that run at native speed.

Truly Reactive

Reactivity is baked into the language via runes ($state, $derived). Assignments trigger updates — no setState calls needed.

Scoped by Default

CSS in <style> blocks is automatically scoped to the component. No CSS-in-JS libraries, no naming conventions, no leaking styles.

Progressive Complexity

Simple things are simple. A basic component is just HTML. Add reactivity, props, and logic only when you need them.

Small Bundles

Because there is no runtime overhead, Svelte apps tend to be significantly smaller than equivalent React/Vue apps, especially for smaller projects.

Svelte 4 vs Svelte 5

Svelte 5 introduced runes, a major evolution of the reactivity system. The old $: reactive declarations and export let props still work in legacy mode, but runes are the recommended approach for all new code.

Concept Svelte 4 (Legacy) Svelte 5 (Runes)
Reactive state let count = 0 (top-level) let count = $state(0)
Derived values $: doubled = count * 2 let doubled = $derived(count * 2)
Side effects $: { console.log(count) } $effect(() => { console.log(count) })
Props export let name let { name } = $props()
Events createEventDispatcher() Callback props: let { onclick } = $props()
03

Component Anatomy

A .svelte file contains three optional sections: <script> for logic, markup for structure, and <style> for scoped CSS. Order does not matter, but convention places script first.

The Three Sections

<!-- UserCard.svelte -->
<script>
  // 1. LOGIC — imports, state, functions
  let { name, role, avatar } = $props();
  let expanded = $state(false);
</script>

<!-- 2. MARKUP — template with reactive expressions -->
<div class="card" class:expanded>
  <img src={avatar} alt={name} />
  <h2>{name}</h2>
  <span>{role}</span>
  <button onclick={() => expanded = !expanded}>
    {expanded ? 'Less' : 'More'}
  </button>
</div>

<!-- 3. STYLE — scoped CSS (won't leak) -->
<style>
  .card { padding: 1rem; border: 1px solid #ddd; }
  .expanded { max-height: none; }
  h2 { color: #ff3e00; }   /* only affects THIS h2 */
</style>

Module Context

A <script module> block runs once per module, not per component instance. Use it for shared logic, module-level constants, or named exports.

<script module>
  // Runs once — shared across all instances
  export const THEMES = ['light', 'dark', 'auto'];

  let instanceCount = 0;
</script>

<script>
  // Runs per instance
  instanceCount++;
  console.log(`Instance #${instanceCount}`);
</script>

Template Expressions

Anything inside curly braces {...} in the markup is a JavaScript expression.

<!-- Text interpolation -->
<p>Hello, {name}!</p>

<!-- Attribute binding -->
<img src={url} alt={description} />

<!-- Shorthand when name === value -->
<img {src} {alt} />

<!-- Spread props -->
<Widget {...config} />

<!-- HTML string (use carefully) -->
{@html rawHtmlString}

<!-- Debug output -->
{@debug name, count}

<!-- Render raw content -->
{@const area = width * height}
<p>Area: {area}</p>

CSS Scoping & the :global() Escape Hatch

<style>
  /* Scoped to this component */
  p { color: #333; }

  /* Escape to global scope */
  :global(body) { margin: 0; }

  /* Global within scoped context */
  .wrapper :global(p) { font-size: 14px; }

  /* Conditional class using class: directive */
</style>

<!-- Class directives -->
<div class:active={isActive}>...</div>
<div class:active>...</div>              <!-- shorthand: uses `active` variable -->
<div class="{base} {size}">...</div>  <!-- string interpolation -->
04

Reactivity & Runes

Svelte 5 runes are compiler instructions that declare reactive state. They look like function calls, but the compiler transforms them into efficient reactive primitives at build time.

$state — Reactive State Svelte 5

Declare reactive state with $state. When the value changes, anything that reads it updates automatically. Objects and arrays are deeply reactive by default.

<script>
  // Primitive state
  let count = $state(0);
  let name = $state('world');

  // Object state — deeply reactive
  let user = $state({
    name: 'Alice',
    scores: [100, 95, 88]
  });

  // Mutating nested properties triggers updates
  user.name = 'Bob';           // reactive!
  user.scores.push(92);         // reactive!

  // Non-deeply-reactive (reference equality only)
  let data = $state.raw([1, 2, 3]);
  data = [...data, 4];          // must replace, not mutate
</script>

<p>{count}</p>       <!-- re-renders when count changes -->
<p>{user.name}</p> <!-- re-renders when name changes -->
$state vs $state.raw
Use $state.raw for large datasets, immutable data from APIs, or when you want to opt out of deep reactivity for performance. With $state.raw, only reassignment (not mutation) triggers updates.

$derived — Computed Values Svelte 5

Derived values recompute automatically when their dependencies change. Use $derived for simple expressions and $derived.by for complex logic.

<script>
  let items = $state([
    { name: 'Apple', price: 1.20, qty: 3 },
    { name: 'Bread', price: 2.50, qty: 1 },
  ]);

  // Simple expression
  let count = $derived(items.length);

  // Complex computation with $derived.by
  let total = $derived.by(() => {
    return items.reduce((sum, item) =>
      sum + item.price * item.qty, 0
    );
  });

  // Derived from derived — it chains
  let totalWithTax = $derived(total * 1.08);
</script>

<p>{count} items, total: ${total.toFixed(2)}</p>

$effect — Side Effects Svelte 5

Effects run after the DOM updates whenever their dependencies change. They automatically track which reactive values they read.

<script>
  let query = $state('');
  let results = $state([]);

  // Auto-tracks `query` — re-runs when it changes
  $effect(() => {
    if (query.length < 3) return;

    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, {
      signal: controller.signal
    })
      .then(r => r.json())
      .then(data => results = data);

    // Cleanup function — runs before next effect or on destroy
    return () => controller.abort();
  });

  // $effect.pre — runs BEFORE DOM update
  $effect.pre(() => {
    console.log('About to update DOM, count is:', count);
  });
</script>
Avoid Infinite Loops
Do not write to reactive state that the same $effect reads, or you will create an infinite loop. If you need to update state in an effect, make sure the write does not re-trigger the effect.

Legacy Reactivity ($: Labels) Svelte 4

In Svelte 4 and earlier, reactivity was triggered by top-level let assignments and $: reactive declarations. This still works in compatibility mode.

<script>
  // Svelte 4 — reactive by assignment
  let count = 0;

  // Reactive declaration — re-runs when `count` changes
  $: doubled = count * 2;

  // Reactive statement block
  $: {
    console.log('count is', count);
    console.log('doubled is', doubled);
  }

  // Reactive if
  $: if (count >= 10) {
    alert('High count!');
  }
</script>
05

Props & Events

Components communicate downward through props and upward through callback props (Svelte 5) or dispatched events (Svelte 4).

$props — Component Input Svelte 5

<!-- Greeting.svelte -->
<script>
  // Destructure props with defaults
  let {
    name,
    greeting = 'Hello',        // default value
    children,                    // snippet (replaces slots)
    ...rest                      // remaining props
  } = $props();
</script>

<div {...rest}>
  <h1>{greeting}, {name}!</h1>
  {@render children?.()}
</div>

<!-- Usage -->
<Greeting name="Alice" class="fancy">
  <p>Welcome back!</p>
</Greeting>

Callback Props (Events in Svelte 5)

In Svelte 5, events are just callback props. Pass functions down, call them from children.

<!-- SearchBox.svelte -->
<script>
  let { onsearch, onreset } = $props();
  let query = $state('');
</script>

<input bind:value={query} />
<button onclick={() => onsearch(query)}>Search</button>
<button onclick={onreset}>Reset</button>

<!-- Parent.svelte -->
<SearchBox
  onsearch={(q) => console.log('Searching:', q)}
  onreset={() => results = []}
/>

$bindable — Two-Way Prop Binding Svelte 5

<!-- Toggle.svelte -->
<script>
  let { checked = $bindable(false) } = $props();
</script>

<label>
  <input type="checkbox" bind:checked />
  {checked ? 'On' : 'Off'}
</label>

<!-- Parent can bind: -->
<Toggle bind:checked={darkMode} />

Snippets (Replacing Slots) Svelte 5

Svelte 5 replaces slots with snippets — composable, renderable blocks of markup.

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

<div class="card">
  {#if header}
    <header>{@render header()}</header>
  {/if}
  <main>{@render children()}</main>
  {#if footer}
    <footer>{@render footer()}</footer>
  {/if}
</div>

<!-- Usage with named snippets -->
<Card>
  {#snippet header()}
    <h2>My Title</h2>
  {/snippet}

  <p>Default content goes here (children).</p>

  {#snippet footer()}
    <small>Footer text</small>
  {/snippet}
</Card>

Legacy Slots (Svelte 4) Svelte 4

<!-- Card.svelte (Svelte 4) -->
<div class="card">
  <slot name="header">Default header</slot>
  <slot>Default content</slot>
  <slot name="footer" />
</div>

<!-- Usage -->
<Card>
  <h2 slot="header">Title</h2>
  <p>Body content</p>
</Card>
06

Control Flow

Svelte uses block-level template syntax for conditionals, loops, and async resolution — no JSX, no v-directives.

{#if} / {:else if} / {:else}

{#if loggedIn}
  <Dashboard user={currentUser} />
{:else if registering}
  <RegisterForm />
{:else}
  <LoginForm />
{/if}

{#each} — Lists

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

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

<!-- KEYED — critical for animations & state -->
{#each items as item (item.id)}
  <TodoItem {item} />
{/each}

<!-- Destructured -->
{#each items as { id, name, done } (id)}
  <li class:done>{name}</li>
{/each}

<!-- Empty state -->
{#each results as result}
  <Result {result} />
{:else}
  <p>No results found.</p>
{/each}
Always Key Dynamic Lists
Without a key expression (item.id), Svelte uses index-based diffing, which can cause bugs with animations, component state, and transitions. Always provide a unique key for dynamic lists.

{#await} — Promise Handling

<!-- Full three-state pattern -->
{#await fetchUser(id)}
  <Spinner />
{:then user}
  <h1>{user.name}</h1>
{:catch error}
  <p class="error">{error.message}</p>
{/await}

<!-- Skip loading state (show nothing until resolved) -->
{#await fetchUser(id) then user}
  <h1>{user.name}</h1>
{/await}

<!-- Skip loading, handle error -->
{#await fetchData() then data}
  <Chart {data} />
{:catch err}
  <Error message={err.message} />
{/await}

{#key} — Force Re-creation

<!-- Destroys and recreates when `userId` changes -->
{#key userId}
  <UserProfile id={userId} />
{/key}

<!-- Useful for re-triggering intro transitions -->
{#key selectedTab}
  <div transition:fade>
    {tabContent}
  </div>
{/key}
07

Stores

Stores are reactive containers for state that lives outside components. They are especially useful for cross-component state sharing. In Svelte 5, you can also use $state in plain .svelte.js modules as an alternative.

writable — Read/Write Store

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

export const count = writable(0);
export const user = writable(null);

// API:
count.set(10);                       // replace value
count.update(n => n + 1);             // transform value
const unsubscribe = count.subscribe( // listen
  value => console.log(value)
);
<!-- Component.svelte -->
<script>
  import { count } from './stores.js';

  // $ prefix auto-subscribes (and unsubscribes on destroy)
</script>

<p>Count: {$count}</p>
<button onclick={() => $count++}>+1</button>

readable — Read-Only Store

import { readable } from 'svelte/store';

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

  // Return cleanup function
  return () => clearInterval(interval);
});

export const mouse = readable({ x: 0, y: 0 }, (set) => {
  const handler = (e) => set({ x: e.clientX, y: e.clientY });
  window.addEventListener('mousemove', handler);
  return () => window.removeEventListener('mousemove', handler);
});

derived — Computed Store

import { derived } from 'svelte/store';
import { items, taxRate } from './stores.js';

// From a single store
export const itemCount = derived(
  items,
  $items => $items.length
);

// From multiple stores
export const total = derived(
  [items, taxRate],
  ([$items, $taxRate]) => {
    const subtotal = $items.reduce((s, i) => s + i.price, 0);
    return subtotal * (1 + $taxRate);
  }
);

Svelte 5 Alternative: Shared $state Svelte 5

In Svelte 5, you can use runes in .svelte.js (or .svelte.ts) files. This is often simpler than stores for shared state.

// counter.svelte.js
let count = $state(0);

export function getCount() {
  return count;
}

export function increment() {
  count++;
}

export function reset() {
  count = 0;
}
<!-- Any component can import and use it -->
<script>
  import { getCount, increment } from './counter.svelte.js';
</script>

<p>{getCount()}</p>
<button onclick={increment}>+1</button>
08

Bindings

Two-way data bindings connect DOM elements to component state. One directive, many applications.

Form Element Bindings

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

<!-- Numeric (auto-coerces to number) -->
<input type="number" bind:value={age} />
<input type="range" bind:value={volume} min="0" max="100" />

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

<!-- Checkbox group (bind to array) -->
<input type="checkbox" bind:group={flavors} value="vanilla" />
<input type="checkbox" bind:group={flavors} value="chocolate" />

<!-- Radio group -->
<input type="radio" bind:group={color} value="red" />
<input type="radio" bind:group={color} value="blue" />

<!-- Select -->
<select bind:value={selected}>
  {#each options as opt}
    <option value={opt.id}>{opt.label}</option>
  {/each}
</select>

<!-- Multi-select (bind to array) -->
<select multiple bind:value={selectedItems}>
  ...
</select>

<!-- Textarea -->
<textarea bind:value={content} />

<!-- Contenteditable -->
<div contenteditable="true" bind:innerHTML={html} />

Dimension & Element Bindings

<!-- Read-only dimension bindings -->
<div bind:clientWidth={w} bind:clientHeight={h}>
  <p>{w}px × {h}px</p>
</div>

<!-- Element reference -->
<canvas bind:this={canvasEl} />

<!-- Media bindings -->
<video
  bind:currentTime={time}
  bind:duration
  bind:paused
  bind:volume
  bind:muted
>
  <source src={videoUrl} type="video/mp4" />
</video>

<!-- Scroll position -->
<div bind:scrollY={y}>...</div>

Component Bindings

<!-- Bind to a prop marked with $bindable -->
<ColorPicker bind:color={selectedColor} />

<!-- Bind to the component instance itself -->
<MyComponent bind:this={componentRef} />
09

Transitions & Animations

Svelte has first-class support for transitions, animations, and motion. No third-party animation libraries needed for most use cases.

Built-in Transitions

<script>
  import { fade, fly, slide, scale, blur, draw }
    from 'svelte/transition';

  let visible = $state(true);
</script>

<!-- Both in and out -->
{#if visible}
  <div transition:fade>Fades in and out</div>
{/if}

<!-- With parameters -->
{#if visible}
  <div transition:fly={{ y: 200, duration: 400 }}>
    Flies in from below
  </div>
{/if}

<!-- Separate in/out transitions -->
{#if visible}
  <div
    in:fly={{ y: -50 }}
    out:fade={{ duration: 200 }}
  >
    Flies in, fades out
  </div>
{/if}
Transition Parameters Effect
fade delay, duration, easing Opacity 0 ↔ 1
fly x, y, delay, duration, easing, opacity Translate + fade
slide delay, duration, easing, axis Height/width collapse
scale start, delay, duration, easing, opacity Scale + fade
blur amount, delay, duration, easing, opacity Blur + fade
draw delay, duration, easing, speed SVG path drawing

Custom Transitions

function typewriter(node, { speed = 1 }) {
  const text = node.textContent;
  const duration = text.length / (speed * 0.015);

  return {
    duration,
    tick(t) {
      const i = Math.trunc(text.length * t);
      node.textContent = text.slice(0, i);
    }
  };
}

<!-- Usage -->
{#if visible}
  <p transition:typewriter>This types out letter by letter</p>
{/if}

animate:flip & Keyed Each

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

<!-- Items smoothly animate to new positions -->
{#each items as item (item.id)}
  <div
    animate:flip={{ duration: 300 }}
    in:fade
    out:fade
  >
    {item.name}
  </div>
{/each}

Motion (Tweened & Spring)

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

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

<progress value={$progress} />
<button onclick={() => progress.set(1)}>Go</button>
<script>
  import { spring } from 'svelte/motion';

  const coords = spring({ x: 50, y: 50 }, {
    stiffness: 0.1,
    damping: 0.25
  });
</script>

<svg onmousemove={(e) => coords.set({x: e.clientX, y: e.clientY})}>
  <circle cx={$coords.x} cy={$coords.y} r="10" />
</svg>
10

SvelteKit

SvelteKit is the full-stack framework for Svelte. File-based routing, server-side rendering, API routes, form actions, and adapters for every deployment target.

Project Structure

my-app/
├── src/
│   ├── lib/                   // $lib alias — shared code
│   │   ├── components/
│   │   ├── server/            // $lib/server — server-only
│   │   └── utils.js
│   ├── params/                // param matchers
│   ├── routes/                // file-based routing
│   │   ├── +layout.svelte     // root layout
│   │   ├── +layout.server.js  // layout load (server)
│   │   ├── +page.svelte       // home page
│   │   ├── +page.server.js    // page load + actions
│   │   ├── +error.svelte      // error page
│   │   ├── about/
│   │   │   └── +page.svelte   // /about
│   │   └── blog/
│   │       ├── +page.svelte   // /blog
│   │       └── [slug]/
│   │           ├── +page.svelte      // /blog/:slug
│   │           └── +page.server.js
│   ├── app.html               // HTML shell template
│   ├── app.css                // global CSS
│   └── hooks.server.js        // server hooks
├── static/                    // served as-is
├── svelte.config.js
├── vite.config.js
└── package.json

Routing Conventions

File Purpose Runs On
+page.svelte Page component (UI) Client + Server (SSR)
+page.js Universal load function Client + Server
+page.server.js Server-only load + form actions Server only
+layout.svelte Shared layout (wraps child pages) Client + Server
+layout.js Layout universal load Client + Server
+layout.server.js Layout server load Server only
+error.svelte Error boundary page Client + Server
+server.js API route (GET, POST, etc.) Server only

Route Parameters

// Dynamic routes
src/routes/blog/[slug]/+page.svelte     // /blog/hello-world
src/routes/[category]/[id]/+page.svelte // /tech/42

// Optional params
src/routes/[[lang]]/about/+page.svelte  // /about or /en/about

// Rest params (catch-all)
src/routes/docs/[...path]/+page.svelte  // /docs/a/b/c

// Param matchers
src/routes/items/[id=integer]/+page.svelte
// src/params/integer.js:
export function match(param) {
  return /^\d+$/.test(param);
}

// Route groups (share layout, no URL segment)
src/routes/(auth)/login/+page.svelte
src/routes/(auth)/register/+page.svelte
src/routes/(auth)/+layout.svelte        // shared auth layout

Load Functions

// +page.server.js — server-only load
import { db } from '$lib/server/database';
import { error } from '@sveltejs/kit';

export async function load({ params, url, cookies, locals }) {
  const post = await db.posts.findUnique({
    where: { slug: params.slug }
  });

  if (!post) {
    error(404, 'Post not found');
  }

  return {
    post,
    readCount: cookies.get('read_count') ?? 0
  };
}
<!-- +page.svelte — receives data from load -->
<script>
  let { data } = $props();
</script>

<h1>{data.post.title}</h1>
{@html data.post.content}
// +page.js — universal load (runs client + server)
export async function load({ fetch, params }) {
  // SvelteKit's `fetch` deduplicates on server
  const res = await fetch(`/api/posts/${params.slug}`);
  return { post: await res.json() };
}

Form Actions

// +page.server.js
import { fail, redirect } from '@sveltejs/kit';

export const actions = {
  // Default action (POST to this page)
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email');

    if (!email) {
      return fail(400, { email, missing: true });
    }

    await createUser(email);
    redirect(303, '/welcome');
  },

  // Named action (POST with ?/login)
  login: async ({ request }) => { ... },
  register: async ({ request }) => { ... },
};
<!-- +page.svelte -->
<script>
  import { enhance } from '$app/forms';
  let { form } = $props();
</script>

<!-- use:enhance progressively enhances the form -->
<form method="POST" use:enhance>
  <input name="email" value={form?.email ?? ''} />
  {#if form?.missing}
    <p class="error">Email is required</p>
  {/if}
  <button>Submit</button>
</form>

<!-- Named action -->
<form method="POST" action="?/login" use:enhance>
  ...
</form>

API Routes (+server.js)

// src/routes/api/posts/+server.js
import { json, error } from '@sveltejs/kit';

export async function GET({ url }) {
  const page = url.searchParams.get('page') ?? '1';
  const posts = await getPosts(Number(page));
  return json(posts);
}

export async function POST({ request, locals }) {
  if (!locals.user) {
    error(401, 'Unauthorized');
  }

  const body = await request.json();
  const post = await createPost(body);
  return json(post, { status: 201 });
}

export async function DELETE({ params }) {
  await deletePost(params.id);
  return new Response(null, { status: 204 });
}

SSR, SSG, and Adapters

// Per-page rendering mode (in +page.js or +layout.js)
export const ssr = true;          // default — server-side render
export const csr = true;          // default — hydrate on client
export const prerender = true;    // generate static HTML at build
export const trailingSlash = 'always';

// Fully static site (in root +layout.js)
export const prerender = true;    // all pages become static HTML
// svelte.config.js — choose adapter
import adapter from '@sveltejs/adapter-auto';
// Other adapters:
// @sveltejs/adapter-node       — Node.js server
// @sveltejs/adapter-static     — full SSG
// @sveltejs/adapter-vercel     — Vercel serverless
// @sveltejs/adapter-cloudflare — Cloudflare Pages/Workers
// @sveltejs/adapter-netlify    — Netlify Functions

export default {
  kit: {
    adapter: adapter()
  }
};
Adapter Auto
adapter-auto detects your deployment platform (Vercel, Cloudflare, Netlify) and uses the correct adapter automatically. For custom servers, switch to adapter-node.

Hooks

// src/hooks.server.js
export async function handle({ event, resolve }) {
  // Runs for every request
  const session = event.cookies.get('session');

  if (session) {
    event.locals.user = await getUserFromSession(session);
  }

  const response = await resolve(event, {
    // Transform HTML (e.g., inject lang attribute)
    transformPageChunk({ html }) {
      return html.replace('%lang%', event.locals.lang ?? 'en');
    }
  });

  return response;
}

export function handleError({ error, event, status, message }) {
  console.error(error);
  return { message: 'Something went wrong' };
}

Navigation & App Modules

<script>
  import { goto, invalidate, invalidateAll }
    from '$app/navigation';
  import { page, navigating }
    from '$app/state';

  // Programmatic navigation
  goto('/dashboard');
  goto('/login', { replaceState: true });

  // Re-run load functions
  invalidate('/api/posts');      // by URL
  invalidate('app:posts');       // by custom identifier
  invalidateAll();                // everything

  // Current page state
  console.log(page.url.pathname); // '/blog/hello'
  console.log(page.params);       // { slug: 'hello' }
</script>

<!-- Loading indicator -->
{#if navigating.to}
  <LoadingBar />
{/if}
11

Lifecycle & Context

Component lifecycle hooks, the context API for dependency injection, and special elements for accessing window, body, and head.

Lifecycle Functions

<script>
  import { onMount, onDestroy, beforeUpdate, afterUpdate,
    tick, untrack } from 'svelte';

  // Runs after first render (client only, not during SSR)
  onMount(() => {
    const interval = setInterval(fetchData, 5000);
    // Return cleanup (like useEffect return in React)
    return () => clearInterval(interval);
  });

  // Runs when component is destroyed
  onDestroy(() => {
    console.log('Goodbye!');
  });

  // tick() — wait for pending DOM updates to flush
  async function handleInput() {
    text = text.toUpperCase();
    await tick();
    // DOM is now updated — safe to measure/read
  }

  // untrack() — read reactive value without tracking
  $effect(() => {
    console.log(count);  // tracked
    untrack(() => name);  // NOT tracked
  });
</script>

Context API

Context allows ancestor components to share data with all descendants without prop drilling. It is set during component initialization and read by any child.

<!-- ThemeProvider.svelte -->
<script>
  import { setContext } from 'svelte';
  let { children } = $props();

  const theme = $state({
    mode: 'dark',
    primary: '#ff3e00',
    toggle() { this.mode = this.mode === 'dark' ? 'light' : 'dark'; }
  });

  setContext('theme', theme);
</script>

{@render children()}

<!-- Any descendant -->
<script>
  import { getContext } from 'svelte';

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

<p>Current mode: {theme.mode}</p>
<button onclick={theme.toggle}>Toggle</button>
Context vs Stores
Context is component-tree-scoped (parent to children). Stores are module-scoped (anywhere). Use context when different subtrees need different values; use stores for truly global state.

Special Elements

<!-- Window events and bindings -->
<svelte:window
  onkeydown={handleKeydown}
  onscroll={handleScroll}
  bind:innerWidth={w}
  bind:innerHeight={h}
  bind:scrollY={y}
/>

<!-- Body class -->
<svelte:body onmouseenter={...} onmouseleave={...} />

<!-- Document head -->
<svelte:head>
  <title>{pageTitle}</title>
  <meta name="description" content={description} />
</svelte:head>

<!-- Dynamic component -->
<svelte:component this={selectedComponent} {...props} />

<!-- Dynamic element tag -->
<svelte:element this={tag} {...attrs}>
  Content
</svelte:element>

<!-- Self-referencing (recursive components) -->
<svelte:self {...childProps} />

<!-- Boundary for error handling -->
<svelte:boundary onerror={(e) => console.error(e)}>
  <RiskyComponent />
  {#snippet failed(error, reset)}
    <p>Error: {error.message}</p>
    <button onclick={reset}>Retry</button>
  {/snippet}
</svelte:boundary>
12

Advanced Topics

TypeScript support, actions, custom use: directives, performance patterns, and the broader Svelte ecosystem.

TypeScript

Svelte has first-class TypeScript support. Use lang="ts" in your script tag.

<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
  }

  let { user, onselect }: {
    user: User;
    onselect: (user: User) => void;
  } = $props();

  let count: number = $state(0);
  let items: User[] = $state([]);
</script>
// Typing load functions (+page.server.ts)
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
  return {
    post: await getPost(params.slug)
  };
};

// Auto-generated types from SvelteKit
// .svelte-kit/types/ contains $types for each route
Auto-Generated Route Types
SvelteKit automatically generates $types for each route, giving you type-safe load functions, actions, and page data without manual typing. Import from ./$types.

Actions (use: Directives)

Actions are functions that run when an element is mounted. They are Svelte's escape hatch for imperative DOM manipulation.

// actions.js
export function clickOutside(node, callback) {
  function handleClick(event) {
    if (!node.contains(event.target)) {
      callback();
    }
  }

  document.addEventListener('click', handleClick, true);

  return {
    // Called when the parameter changes
    update(newCallback) {
      callback = newCallback;
    },
    // Called when element is removed
    destroy() {
      document.removeEventListener('click', handleClick, true);
    }
  };
}

export function tooltip(node, text) {
  const tip = document.createElement('div');
  tip.textContent = text;
  tip.className = 'tooltip';

  node.addEventListener('mouseenter', () => node.appendChild(tip));
  node.addEventListener('mouseleave', () => tip.remove());

  return {
    update(newText) { tip.textContent = newText; },
    destroy() { tip.remove(); }
  };
}
<!-- Usage -->
<script>
  import { clickOutside, tooltip } from './actions.js';
</script>

<div use:clickOutside={() => open = false}>
  Dropdown
</div>

<button use:tooltip={'Click to save'}>Save</button>

Environment Variables

// .env
PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgres://...

// Server-only (import from $env/static/private)
import { DATABASE_URL } from '$env/static/private';

// Client-safe (must start with PUBLIC_)
import { PUBLIC_API_URL } from '$env/static/public';

// Dynamic (read at runtime, not build time)
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';

Performance Patterns

$state.raw for Big Data

Avoid deep reactivity for large arrays/objects from APIs. Use $state.raw and replace the whole reference on update.

Key Your Lists

Always use (item.id) keys in {#each} blocks. Without keys, Svelte uses index-based diffing that can cause unnecessary re-renders.

Lazy Loading

Use dynamic import() with {#await} to code-split heavy components. SvelteKit handles route-level splitting automatically.

Avoid $effect Chains

Prefer $derived over $effect that writes to state. Derived values are synchronous and avoid cascading re-renders.

Custom Elements / Web Components

// svelte.config.js — enable custom element compilation
export default {
  compilerOptions: {
    customElement: true
  }
};

<!-- MyWidget.svelte -->
<svelte:options customElement="my-widget" />

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

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

<!-- Use anywhere as standard HTML -->
<my-widget name="Svelte"></my-widget>

Ecosystem at a Glance

Category Library Purpose
Framework @sveltejs/kit Full-stack framework (routing, SSR, API)
Styling TailwindCSS, UnoCSS Utility-first CSS (works natively)
UI Libraries Skeleton, shadcn-svelte, Melt UI Component libraries & headless primitives
Forms Superforms, Formsnap Form validation & actions integration
Auth Lucia, Auth.js Authentication & sessions
Database Drizzle, Prisma Type-safe ORM / query builder
Testing Vitest, Playwright, @testing-library/svelte Unit, integration, and E2E
State Built-in stores, TanStack Query Global state & async data management
Animation Built-in transitions, Motion One Declarative animations
Mobile Capacitor, Tauri Native mobile/desktop apps

$inspect — Development Debugging Svelte 5

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

  // Logs to console whenever values change
  $inspect(count);            // logs count on every change
  $inspect(count, user);      // logs both

  // Custom handler (e.g., breakpoint)
  $inspect(count).with((type, value) => {
    if (type === 'update') debugger;
  });

  // Stripped from production builds automatically
</script>