DevToolBoxGRATIS
Blog

Svelte Guide: Reactivity, Stores, SvelteKit, and Svelte 5 Runes

13 min readby DevToolBox

Svelte is a radical approach to building user interfaces. Unlike React or Vue, Svelte shifts work from the browser to the compile step, producing highly optimized vanilla JavaScript with no virtual DOM overhead. With Svelte 5 introducing runes and SvelteKit providing a full-stack framework, Svelte has become a compelling choice for modern web development in 2026.

TL;DR

Svelte compiles components into efficient imperative code at build time, eliminating virtual DOM diffing. Its reactivity system is built into the language with reactive declarations ($:) and Svelte 5 runes ($state, $derived, $effect). SvelteKit provides file-based routing, SSR, form actions, and progressive enhancement. Svelte consistently outperforms React and Vue in bundle size and runtime benchmarks.

Key Takeaways
  • Svelte compiles to vanilla JS with no runtime framework overhead
  • Reactivity is built into the language, not a library API
  • Svelte 5 runes provide fine-grained universal reactivity
  • SvelteKit offers SSR, file-based routing, and form actions
  • Transitions and animations are first-class with built-in directives
  • Stores provide simple, framework-integrated state management

Svelte Reactivity System

Svelte tracks reactive dependencies at compile time. When you assign to a variable declared with let, Svelte automatically updates the DOM. Reactive declarations ($:) let you define computed values and run side effects that re-execute whenever their dependencies change.

Reactive Declarations & Statements

<script>
  let count = 0;

  // Reactive declaration: recomputes when count changes
  $: doubled = count * 2;
  $: quadrupled = doubled * 2;

  // Reactive statement: runs side effect
  $: if (count >= 10) {
    console.log("Count reached 10!");
    count = 0;
  }

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

  function increment() {
    count += 1; // assignment triggers reactivity
  }
</script>

<button on:click={increment}>
  Clicked {count} times
</button>
<p>{count} x 2 = {doubled}</p>
<p>{count} x 4 = {quadrupled}</p>

Reactive Assignments

In Svelte, assignments trigger reactivity. The compiler instruments each assignment to schedule a DOM update. Array and object mutations require reassignment to trigger updates.

<script>
  let items = ["apple", "banana"];

  function addItem() {
    // Reassignment triggers reactivity
    items = [...items, "cherry"];

    // This will NOT trigger reactivity:
    // items.push("cherry");

    // Idiomatic: push then reassign
    items.push("date");
    items = items; // trigger update
  }

  let user = { name: "Alice", age: 30 };

  function birthday() {
    user.age += 1; // property assignment works
  }
</script>

Components: Props, Events, Slots & Lifecycle

Svelte components are written in .svelte files combining HTML, CSS, and JavaScript in a single file. Props flow down, events bubble up, and slots allow content composition.

Component Props

<!-- UserCard.svelte -->
<script>
  export let name;         // required prop
  export let age = 25;     // optional with default
  export let role = "user";
</script>

<div class="card">
  <h3>{name}</h3>
  <p>Age: {age} | Role: {role}</p>
</div>

<style>
  .card { padding: 1rem; border: 1px solid #ddd; }
</style>

<!-- Parent.svelte -->
<script>
  import UserCard from "./UserCard.svelte";
</script>

<UserCard name="Alice" age={30} role="admin" />
<UserCard name="Bob" />

Custom Events

Components dispatch custom events using createEventDispatcher. Parent components listen with the on: directive.

<!-- SearchInput.svelte -->
<script>
  import { createEventDispatcher } from "svelte";
  const dispatch = createEventDispatcher();
  let query = "";

  function handleInput() {
    dispatch("search", { query });
  }
</script>

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

<!-- App.svelte -->
<script>
  import SearchInput from "./SearchInput.svelte";
  function onSearch(event) {
    console.log("Searching:", event.detail.query);
  }
</script>
<SearchInput on:search={onSearch} />

Slots & Named Slots

Slots allow parent components to inject content into child components. Named slots provide multiple insertion points.

<!-- Modal.svelte -->
<script>
  export let title = "Dialog";
</script>

<div class="modal-backdrop">
  <div class="modal">
    <header>
      <slot name="header"><h2>{title}</h2></slot>
    </header>
    <main><slot /></main>
    <footer>
      <slot name="footer">
        <button>Close</button>
      </slot>
    </footer>
  </div>
</div>

<!-- Usage -->
<Modal>
  <h2 slot="header">Confirm Delete</h2>
  <p>Are you sure?</p>
  <div slot="footer">
    <button>Cancel</button>
    <button>Delete</button>
  </div>
</Modal>

Lifecycle Functions

Svelte provides onMount, onDestroy, beforeUpdate, afterUpdate, and tick for managing component lifecycle.

<script>
  import { onMount, onDestroy, beforeUpdate,
    afterUpdate, tick } from "svelte";
  let data = null;

  onMount(async () => {
    const res = await fetch("/api/data");
    data = await res.json();
    return () => { /* cleanup */ };
  });

  onDestroy(() => { /* remove listeners */ });
  beforeUpdate(() => { /* before DOM update */ });
  afterUpdate(() => { /* after DOM update */ });

  async function handleClick() {
    data = newValue;
    await tick(); // wait for DOM to update
  }
</script>

Svelte Stores

Stores are Svelte's built-in state management solution. They are reactive objects that can be subscribed to and updated from any component. The auto-subscription syntax ($store) makes them seamless to use.

Writable Stores

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

export const count = writable(0);
export const user = writable({ name: "", loggedIn: false });

// Component.svelte
<script>
  import { count, user } from "./stores.js";

  // $ prefix auto-subscribes and unsubscribes
  function increment() {
    $count += 1;
    // equivalent to: count.update(n => n + 1);
  }
</script>

<p>Count: {$count}</p>
<p>User: {$user.name}</p>
<button on:click={increment}>+1</button>

Readable & Derived Stores

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

// Readable store: external data source
export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);
  return () => clearInterval(interval);
});

// Derived store: computed from other stores
export const elapsed = derived(time, ($time) =>
  Math.round(($time.getTime() - start) / 1000)
);

// Derived from multiple stores
export const summary = derived(
  [count, user],
  ([$count, $user]) =>
    `\${$user.name} clicked \${$count} times`
);

Custom Stores

Custom stores encapsulate logic by wrapping a writable store and exposing a controlled API.

import { writable } from "svelte/store";

function createTodoStore() {
  const { subscribe, update, set } = writable([]);
  return {
    subscribe,
    add: (text) => update(todos =>
      [...todos, { id: Date.now(), text, done: false }]
    ),
    toggle: (id) => update(todos =>
      todos.map(t =>
        t.id === id ? { ...t, done: !t.done } : t
      )
    ),
    remove: (id) => update(todos =>
      todos.filter(t => t.id !== id)
    ),
    reset: () => set([])
  };
}

export const todos = createTodoStore();

SvelteKit: Routing, Layouts & Load Functions

SvelteKit is the official full-stack framework for Svelte. It provides file-based routing, server-side rendering, API routes, and a powerful load function system for data fetching.

File-Based Routing

SvelteKit uses the filesystem for routing. Each +page.svelte file in src/routes becomes a page. Dynamic parameters use [brackets] in folder names.

src/routes/
  +page.svelte          β†’ /
  +layout.svelte        β†’ shared layout
  about/+page.svelte    β†’ /about
  blog/
    +page.svelte        β†’ /blog
    [slug]/
      +page.svelte      β†’ /blog/:slug
      +page.server.ts   β†’ server load
  api/users/
    +server.ts          β†’ /api/users
  (auth)/
    login/+page.svelte  β†’ /login
    register/+page.svelte β†’ /register

Layouts

Layouts wrap pages and persist across navigation. Nested layouts allow shared UI at different route levels.

<!-- src/routes/+layout.svelte -->
<script>
  import Header from "$lib/Header.svelte";
  import Footer from "$lib/Footer.svelte";
</script>

<Header />
<main><slot /></main>
<Footer />

<!-- src/routes/dashboard/+layout.svelte -->
<script>
  import Sidebar from "$lib/Sidebar.svelte";
</script>
<div class="dashboard">
  <Sidebar />
  <div class="content"><slot /></div>
</div>

Load Functions

Load functions fetch data before rendering. They run on the server during SSR and in the browser during client-side navigation.

// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from "./$types";
import { error } from "@sveltejs/kit";

export const load: PageServerLoad = async ({
  params, fetch
}) => {
  const res = await fetch(
    `/api/posts/\${params.slug}`
  );
  if (!res.ok) throw error(404, "Not found");
  return { post: await res.json() };
};

// src/routes/blog/[slug]/+page.svelte
<script>
  export let data;
</script>
<article>
  <h1>{data.post.title}</h1>
  <div>{@html data.post.content}</div>
</article>

Form Actions & Progressive Enhancement

SvelteKit form actions handle form submissions on the server. They work without JavaScript and can be progressively enhanced with use:enhance.

// src/routes/login/+page.server.ts
import { fail, redirect } from "@sveltejs/kit";

export const actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get("email");
    const password = data.get("password");

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

    const user = await authenticate(email, password);
    if (!user)
      return fail(401, { email, incorrect: true });

    cookies.set("session", user.token, { path: "/" });
    throw redirect(303, "/dashboard");
  }
};

// src/routes/login/+page.svelte
<script>
  import { enhance } from "$app/forms";
  export let form;
</script>

<form method="POST" use:enhance>
  <input name="email" value={form?.email ?? ""} />
  {#if form?.missing}
    <p class="error">Email is required</p>
  {/if}
  <input name="password" type="password" />
  <button>Log In</button>
</form>

Transitions & Animations

Svelte has built-in support for transitions and animations via directives. Elements can smoothly enter and leave the DOM with minimal code.

Built-in Transitions

<script>
  import { fade, fly, slide, scale }
    from "svelte/transition";
  import { quintOut } from "svelte/easing";
  let visible = true;
</script>

<button on:click={() => visible = !visible}>
  Toggle
</button>

{#if visible}
  <div transition:fade={{ duration: 300 }}>
    Fades in and out
  </div>

  <div transition:fly={{ y: 200, duration: 500,
    easing: quintOut }}>
    Flies in from below
  </div>

  <div in:fly={{ x: -200 }} out:fade>
    Flies in, fades out
  </div>

  <div transition:slide={{ duration: 300 }}>
    Slides open and closed
  </div>
{/if}

Custom Transitions

Create custom transitions by returning an object with css or tick functions.

<script>
  function typewriter(node, { speed = 1 }) {
    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);
      }
    };
  }
</script>

{#if visible}
  <p transition:typewriter={{ speed: 2 }}>
    This text types itself out!
  </p>
{/if}

The animate Directive

The animate directive smoothly moves elements when their position changes in a keyed each block.

<script>
  import { flip } from "svelte/animate";
  import { fade } from "svelte/transition";
  let list = [1, 2, 3, 4, 5];

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

<button on:click={shuffle}>Shuffle</button>
{#each list as item (item)}
  <div animate:flip={{ duration: 300 }}
    transition:fade>{item}</div>
{/each}

Actions (use: Directive)

Actions are functions that run when an element is mounted to the DOM. They are useful for integrating third-party libraries, adding event listeners, or implementing reusable DOM behaviors.

<script>
  // Tooltip action
  function tooltip(node, text) {
    let tip;
    function show() {
      tip = document.createElement("div");
      tip.textContent = text;
      tip.style.cssText = `position:absolute;
        background:#333;color:#fff;padding:4px 8px;
        border-radius:4px;font-size:12px`;
      node.appendChild(tip);
    }
    function hide() { tip?.remove(); }

    node.addEventListener("mouseenter", show);
    node.addEventListener("mouseleave", hide);

    return {
      update(newText) { text = newText; },
      destroy() {
        tip?.remove();
        node.removeEventListener("mouseenter", show);
        node.removeEventListener("mouseleave", hide);
      }
    };
  }

  // Click-outside action
  function clickOutside(node, callback) {
    function onClick(e) {
      if (!node.contains(e.target)) callback();
    }
    document.addEventListener("click", onClick, true);
    return {
      destroy() {
        document.removeEventListener("click",
          onClick, true);
      }
    };
  }
</script>

<button use:tooltip={"Click me!"}>Hover</button>
<div use:clickOutside={() => open = false}>
  Dropdown content
</div>

Context API

The Context API passes data through the component tree without prop drilling. Unlike stores, context is scoped to a component subtree.

<!-- ThemeProvider.svelte -->
<script>
  import { setContext } from "svelte";
  import { writable } from "svelte/store";

  const theme = writable("light");
  setContext("theme", {
    theme,
    toggle: () => theme.update(t =>
      t === "light" ? "dark" : "light"
    )
  });
</script>
<slot />

<!-- DeepChild.svelte -->
<script>
  import { getContext } from "svelte";
  const { theme, toggle } = getContext("theme");
</script>
<p>Current theme: {$theme}</p>
<button on:click={toggle}>Toggle Theme</button>

Svelte 5 Runes

Svelte 5 introduces runes, a set of compiler-level primitives that provide universal fine-grained reactivity. Runes replace reactive declarations ($:), export let for props, and stores for many use cases.

$state & $derived

<script>
  // $state: reactive state (replaces let)
  let count = $state(0);
  let items = $state(["apple", "banana"]);

  // $derived: computed value (replaces $:)
  let doubled = $derived(count * 2);
  let total = $derived(items.length);

  // Complex derived expression
  let summary = $derived.by(() => {
    if (count > 10) return "Many clicks";
    if (count > 5) return "Some clicks";
    return "Few clicks";
  });

  // Deep reactivity in Svelte 5
  function addItem() {
    items.push("cherry"); // works in Svelte 5!
  }
</script>

<button onclick={() => count++}>
  {count} (doubled: {doubled})
</button>
<p>{summary} β€” {total} items</p>

$effect

The $effect rune replaces reactive statements and onMount for side effects. It automatically tracks its dependencies and re-runs when they change.

<script>
  let count = $state(0);
  let title = $state("My App");

  // Auto-tracks dependencies
  $effect(() => {
    document.title = `\${title} (\${count})`;
  });

  // With cleanup
  $effect(() => {
    const id = setInterval(() => count++, 1000);
    return () => clearInterval(id);
  });

  // Pre-effect (runs before DOM update)
  $effect.pre(() => {
    console.log("about to update DOM");
  });
</script>

$props

The $props rune replaces export let for declaring component props in Svelte 5.

<!-- Svelte 5 component with $props -->
<script>
  // Destructure with defaults
  let { name, age = 25, role = "user", ...rest }
    = $props();

  // With TypeScript:
  // interface Props {
  //   name: string;
  //   age?: number;
  //   role?: "user" | "admin";
  // }
  // let { name, age = 25, role = "user" }
  //   = $props<Props>();
</script>

<div {...rest}>
  <h3>{name}</h3>
  <p>Age: {age} | Role: {role}</p>
</div>

Server-Side Rendering & Hydration

SvelteKit renders pages on the server by default and hydrates them on the client. This provides fast initial loads, good SEO, and progressive enhancement.

// src/routes/+page.server.ts
export const load = async ({ fetch }) => {
  const res = await fetch("/api/products");
  return { products: await res.json() };
};

// Page options
export const prerender = true;  // static at build
export const ssr = true;        // default: SSR on
export const csr = true;        // default: hydrate

// +page.svelte
<script>
  import { browser } from "$app/environment";
  export let data;
  if (browser) {
    console.log("Client-side hydrated!");
  }
</script>

{#each data.products as product}
  <div>{product.name} β€” ${product.price}</div>
{/each}

Component Composition Patterns

Svelte supports several composition patterns for building reusable, maintainable component architectures.

<!-- Compound Components: Tabs -->
<!-- Tabs.svelte -->
<script>
  import { setContext } from "svelte";
  import { writable } from "svelte/store";
  const activeTab = writable(0);
  setContext("tabs", { activeTab });
</script>
<div class="tabs"><slot /></div>

<!-- Tab.svelte -->
<script>
  import { getContext } from "svelte";
  export let index;
  const { activeTab } = getContext("tabs");
</script>
<button on:click={() => $activeTab = index}
  class:active={$activeTab === index}>
  <slot />
</button>

<!-- TabPanel.svelte -->
<script>
  import { getContext } from "svelte";
  export let index;
  const { activeTab } = getContext("tabs");
</script>
{#if $activeTab === index}
  <div class="panel"><slot /></div>
{/if}

<!-- Usage -->
<Tabs>
  <Tab index={0}>General</Tab>
  <Tab index={1}>Settings</Tab>
  <TabPanel index={0}>General content</TabPanel>
  <TabPanel index={1}>Settings content</TabPanel>
</Tabs>

Testing with Vitest & Playwright

Svelte components can be tested with Vitest for unit and integration tests, and Playwright for end-to-end tests. SvelteKit scaffolds both by default.

// counter.test.ts (Vitest + testing-library)
import { render, fireEvent } from
  "@testing-library/svelte";
import { describe, it, expect } from "vitest";
import Counter from "./Counter.svelte";

describe("Counter", () => {
  it("increments on click", async () => {
    const { getByText } = render(Counter,
      { props: { initial: 0 } });
    const btn = getByText("Count: 0");
    await fireEvent.click(btn);
    expect(getByText("Count: 1")).toBeTruthy();
  });
});

// e2e/home.test.ts (Playwright)
import { test, expect } from "@playwright/test";

test("home page loads", async ({ page }) => {
  await page.goto("/");
  await expect(page.locator("h1"))
    .toContainText("Welcome");
  await page.click("button:text(\"Increment\")");
  await expect(page.locator("[data-count]"))
    .toHaveAttribute("data-count", "1");
});

Performance Comparison

Svelte consistently outperforms React and Vue in benchmarks due to its compile-time approach and lack of virtual DOM overhead.

MetricSvelteReactVue
Bundle Size (min+gzip)1.6 KB44+ KB34+ KB
Runtime OverheadNone (compiled)Virtual DOMVirtual DOM
Startup Time (TTI)~50ms~120ms~100ms
Memory UsageLowMedium-HighMedium
DOM Update SpeedDirect mutationReconciliationProxy-based
JS Benchmark Score1.021.381.21
TodoMVC Lines~60~120~90
Learning CurveGentleModerateGentle-Moderate
Ecosystem SizeGrowingLargestLarge
TypeScriptFirst-classFirst-classFirst-class

Best Practices

  • Use reactive declarations ($:) for computed values instead of manual updates
  • Keep components small and focused on a single responsibility
  • Use stores for cross-component state, context for subtree state
  • Leverage SvelteKit form actions for progressive enhancement
  • Use transitions sparingly to avoid motion sickness issues
  • Prefer $derived over $effect for computed values in Svelte 5
  • Use load functions for data fetching instead of onMount
  • Enable prerendering for static content with export const prerender = true

Frequently Asked Questions

Is Svelte better than React?

Svelte offers smaller bundles, faster runtime performance, and simpler syntax compared to React. However, React has a larger ecosystem, more job opportunities, and more third-party libraries. Svelte is excellent for performance-critical applications and teams that value developer experience.

What is the difference between Svelte and SvelteKit?

Svelte is the component framework and compiler. SvelteKit is the full-stack application framework built on top of Svelte, providing routing, SSR, API routes, form actions, and deployment adapters. SvelteKit is to Svelte what Next.js is to React.

How does Svelte reactivity work without a virtual DOM?

Svelte compiles components into imperative JavaScript at build time. The compiler analyzes your code, identifies reactive dependencies, and generates surgical DOM updates. When a reactive variable changes, only the specific DOM nodes that depend on it are updated directly, bypassing the need for virtual DOM diffing.

What are Svelte 5 runes?

Runes are compiler-level primitives introduced in Svelte 5: $state for reactive state, $derived for computed values, $effect for side effects, and $props for component props. They provide fine-grained reactivity that works everywhere, not just in .svelte files, and replace the older $: syntax and export let pattern.

Can I use TypeScript with Svelte?

Yes, Svelte has first-class TypeScript support. Add lang="ts" to your script tags in .svelte files. SvelteKit projects scaffold with TypeScript by default, and the Svelte language server provides full IDE support for type checking, autocompletion, and refactoring.

How do Svelte stores compare to Redux or Zustand?

Svelte stores are much simpler than Redux. A writable store is just a few lines of code, and the $ auto-subscription syntax eliminates boilerplate. For most Svelte applications, built-in stores are sufficient. For complex state logic, custom stores provide similar encapsulation to Zustand slices.

Is SvelteKit ready for production?

Yes, SvelteKit reached version 1.0 in December 2022 and has been production-ready since then. It is used by companies like Apple, Spotify, The New York Times, and Ikea. SvelteKit 2.0 brought Vite 5, shallow routing, and improved performance.

How do I deploy a SvelteKit application?

SvelteKit uses adapters for deployment. adapter-auto detects the platform automatically. Dedicated adapters exist for Vercel, Netlify, Cloudflare Pages, Node.js servers, and static site generation. You can also build a Docker container using adapter-node.

𝕏 Twitterin LinkedIn
Was dit nuttig?

Blijf op de hoogte

Ontvang wekelijkse dev-tips en nieuwe tools.

Geen spam. Altijd opzegbaar.

Try These Related Tools

{ }JSON Formatter{ }CSS Minifier / Beautifier.*Regex Tester

Related Articles

React Hooks Complete Gids: useState, useEffect en Custom Hooks

Beheers React Hooks met praktische voorbeelden. useState, useEffect, useContext, useReducer, useMemo, useCallback, custom hooks en React 18+ concurrent hooks.

Angular Guide: Components, Services, RxJS, NgRx, and Angular 17+ Signals

Master Angular framework. Covers components and data binding, directives, dependency injection, reactive forms, RxJS observables, Angular Router with lazy loading, NgRx state management, and Angular 17 Signals.

Web Performance Optimization Guide: Core Web Vitals, Caching, and React/Next.js

Master web performance optimization. Covers Core Web Vitals (LCP, FID, CLS), image optimization, code splitting, caching strategies, React/Next.js performance, Lighthouse scoring, and real-world benchmarks.