Svelte: The Complete Guide for 2026

Published February 12, 2026 · 20 min read

Svelte takes a fundamentally different approach to building web interfaces. Instead of doing work in the browser with a virtual DOM, Svelte compiles your components into tight, imperative code that updates the DOM directly. With Svelte 5 introducing runes for explicit reactivity and SvelteKit maturing as a full-stack framework, there has never been a better time to learn Svelte. This guide covers everything from basic components to production SvelteKit applications.

⚙ Try it: Use our JSON Formatter to validate API responses while building Svelte apps, and check the JavaScript Cheat Sheet for quick syntax reference.

1. What Is Svelte and Why It's Different

Most frameworks do the bulk of their work in the browser. React and Vue use a virtual DOM to compute the minimum number of DOM changes needed when state updates. Svelte takes a different approach: it runs at build time, compiling your components into optimized JavaScript that manipulates the DOM directly.

This means:

  • No virtual DOM overhead — updates are compiled into direct DOM assignments
  • Smaller bundles — no framework runtime shipped to users (typically 2-5 KB vs React's 40+ KB)
  • Less boilerplate — reactive state is a language feature, not an API call
  • Scoped CSS by default — styles in a component only affect that component
  • Built-in transitions — animations are a first-class language feature

Svelte consistently tops developer satisfaction surveys and benchmarks for startup performance and memory usage.

2. Getting Started with SvelteKit

SvelteKit is the official framework for building Svelte applications. It handles routing, server-side rendering, and deployment out of the box:

# Create a new SvelteKit project
npx sv create my-app
cd my-app
npm install
npm run dev

The CLI asks you to choose a template (skeleton, demo, or library), whether to add TypeScript, and optional integrations like ESLint, Prettier, and testing. The project structure looks like this:

my-app/
  src/
    routes/          # File-based routing
      +page.svelte   # Home page
      +layout.svelte # Root layout
    lib/             # Shared code ($lib alias)
    app.html         # HTML shell
  static/            # Static assets
  svelte.config.js   # SvelteKit config
  vite.config.js     # Vite config

3. Component Basics

A .svelte file contains three sections: script (logic), markup (HTML), and style (CSS). All three are optional:

<script>
    let name = $state("world");
    let uppercased = $derived(name.toUpperCase());
</script>

<h1>Hello {name}!</h1>
<p>Uppercased: {uppercased}</p>
<input bind:value={name} />

<style>
    h1 {
        color: #ff3e00;
        font-size: 2rem;
    }
    /* This CSS is scoped to this component only */
</style>

Key points about Svelte components:

  • Curly braces {expression} embed JavaScript expressions in markup
  • Styles are scoped by default — the compiler adds unique class names
  • Use :global(selector) to write unscoped CSS when needed
  • Components are imported and used as custom HTML elements
<script>
    import Button from "./Button.svelte";
    import Card from "$lib/components/Card.svelte";
</script>

<Card title="Welcome">
    <p>Content inside the card</p>
    <Button onclick={() => alert("clicked!")}>Click me</Button>
</Card>

4. Reactivity — Svelte 5 Runes

Svelte 5 introduced runes, a set of compiler-level primitives that make reactivity explicit. They replace the implicit let-based reactivity from earlier versions.

$state — Reactive State

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

    // Object state (deeply reactive)
    let user = $state({ name: "Alice", age: 30 });

    // Array state
    let todos = $state([
        { text: "Learn Svelte", done: false },
        { text: "Build an app", done: false }
    ]);

    function addTodo(text) {
        todos.push({ text, done: false }); // Mutation works!
    }
</script>

<p>Count: {count}</p>
<button onclick={() => count++}>Increment</button>
<p>{user.name} is {user.age}</p>

Unlike React, you can mutate objects and arrays directly. Svelte's compiler tracks which properties are read and generates targeted updates.

$derived — Computed Values

<script>
    let items = $state([10, 20, 30, 40]);
    let total = $derived(items.reduce((sum, n) => sum + n, 0));
    let average = $derived(total / items.length);

    // For multi-line derivations use $derived.by()
    let stats = $derived.by(() => {
        const sorted = [...items].sort((a, b) => a - b);
        return {
            min: sorted[0],
            max: sorted[sorted.length - 1],
            median: sorted[Math.floor(sorted.length / 2)]
        };
    });
</script>

<p>Total: {total}, Average: {average}</p>
<p>Min: {stats.min}, Max: {stats.max}</p>

$effect — Side Effects

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

    // Runs whenever query changes, auto-tracks dependencies
    $effect(() => {
        if (query.length < 2) return;

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

        // Cleanup function runs before next effect and on destroy
        return () => controller.abort();
    });
</script>

$effect automatically detects which reactive values you read and re-runs when they change. The cleanup function (returned value) runs before the next execution and when the component is destroyed.

5. Props and Component Communication

Svelte 5 uses $props() to declare component inputs:

<!-- UserCard.svelte -->
<script>
    let { name, email, role = "user", children } = $props();
</script>

<div class="card">
    <h3>{name}</h3>
    <p>{email}</p>
    <span class="badge">{role}</span>
    {@render children?.()}
</div>

<!-- Usage -->
<UserCard name="Alice" email="alice@example.com" role="admin">
    <p>Extra content rendered as children</p>
</UserCard>

For TypeScript, you can type props directly:

<script lang="ts">
    interface Props {
        name: string;
        email: string;
        role?: "user" | "admin" | "moderator";
    }
    let { name, email, role = "user" }: Props = $props();
</script>

Callback Props

Pass functions down to handle child-to-parent communication:

<!-- Parent.svelte -->
<script>
    let items = $state([]);
    function handleAdd(item) {
        items.push(item);
    }
</script>
<ItemForm onadd={handleAdd} />

<!-- ItemForm.svelte -->
<script>
    let { onadd } = $props();
    let text = $state("");
</script>
<input bind:value={text} />
<button onclick={() => { onadd(text); text = ""; }}>Add</button>

6. Events and Event Handling

Svelte uses standard DOM event attributes with concise syntax:

<script>
    let pos = $state({ x: 0, y: 0 });
    let count = $state(0);
</script>

<!-- Basic events -->
<button onclick={() => count++}>Clicked {count} times</button>

<!-- Event with the event object -->
<div onmousemove={(e) => { pos.x = e.clientX; pos.y = e.clientY; }}>
    Mouse: {pos.x}, {pos.y}
</div>

<!-- Keyboard events -->
<input onkeydown={(e) => {
    if (e.key === "Enter") handleSubmit();
}} />

<!-- Form submission -->
<form onsubmit|preventDefault={handleSubmit}>
    <input bind:value={query} />
    <button type="submit">Search</button>
</form>

Svelte supports event modifiers like preventDefault, stopPropagation, once, self, and capture using the pipe syntax on legacy event directives, or you can call them directly in Svelte 5's inline handlers.

7. Conditional Rendering and Loops

Svelte uses template blocks instead of JSX ternaries or .map() calls:

Conditionals with {#if}

{#if user.loggedIn}
    <p>Welcome, {user.name}!</p>
    <button onclick={logout}>Log out</button>
{:else if user.banned}
    <p>Your account has been suspended.</p>
{:else}
    <button onclick={login}>Log in</button>
{/if}

Loops with {#each}

<script>
    let todos = $state([
        { id: 1, text: "Learn Svelte", done: true },
        { id: 2, text: "Build app", done: false },
        { id: 3, text: "Deploy", done: false }
    ]);
</script>

{#each todos as todo (todo.id)}
    <label>
        <input type="checkbox" bind:checked={todo.done} />
        <span class:done={todo.done}>{todo.text}</span>
    </label>
{:else}
    <p>No todos yet!</p>
{/each}

Always provide a key expression (todo.id) when items can be reordered or removed. The {:else} block renders when the array is empty.

Async with {#await}

<script>
    let dataPromise = fetch("/api/stats").then(r => r.json());
</script>

{#await dataPromise}
    <p>Loading...</p>
{:then data}
    <p>Users: {data.users}, Revenue: ${data.revenue}</p>
{:catch error}
    <p class="error">Failed to load: {error.message}</p>
{/await}

8. Stores — Shared State

Stores let you share reactive state between components that are not in a parent-child relationship:

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

// Writable store — can be set and updated
export const count = writable(0);

// Readable store — set externally, read-only for consumers
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 timeString = derived(time, ($time) =>
    $time.toLocaleTimeString()
);

In components, prefix a store with $ to auto-subscribe and auto-unsubscribe:

<script>
    import { count, timeString } from "./stores.js";
</script>

<p>Count: {$count}</p>
<button onclick={() => $count++}>Increment</button>
<button onclick={() => count.set(0)}>Reset</button>
<p>Time: {$timeString}</p>

Stores are also useful for global state like authentication, theme preferences, or shopping carts. In Svelte 5, you can also use $state in plain .svelte.js files for shared reactive state without the store API.

9. Lifecycle Hooks

Svelte provides lifecycle functions for running code at specific moments in a component's life:

<script>
    import { onMount, onDestroy, beforeUpdate, afterUpdate } from "svelte";

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

    onMount(() => {
        // Runs after the component is first rendered to the DOM
        const ws = new WebSocket("wss://api.example.com/chat");
        ws.onmessage = (e) => messages.push(JSON.parse(e.data));

        // Return a cleanup function (equivalent to onDestroy)
        return () => ws.close();
    });

    afterUpdate(() => {
        // Runs after any state change causes the DOM to update
        // Useful for scrolling to bottom after new messages
        el?.scrollTo(0, el.scrollHeight);
    });

    onDestroy(() => {
        // Runs when the component is removed from the DOM
        console.log("Component destroyed");
    });
</script>

<div bind:this={el} class="messages">
    {#each messages as msg}
        <p>{msg.text}</p>
    {/each}
</div>

Note: In Svelte 5, $effect often replaces onMount + onDestroy for reactive side effects. Use lifecycle hooks when you need non-reactive setup or teardown logic.

10. Transitions and Animations

Svelte has built-in transition directives that handle enter/exit animations declaratively:

<script>
    import { fade, fly, slide, scale } from "svelte/transition";
    import { flip } from "svelte/animate";
    let visible = $state(true);
    let items = $state([1, 2, 3, 4, 5]);
</script>

<button onclick={() => visible = !visible}>Toggle</button>

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

    <!-- Fly in from left, fade out -->
    <p in:fly={{ x: -200, duration: 400 }} out:fade>
        Flies in, fades out
    </p>

    <!-- Slide with custom easing -->
    <div transition:slide={{ duration: 300 }}>
        Slides open and shut
    </div>
{/if}

<!-- Animate list reordering -->
{#each items as item (item)}
    <div animate:flip={{ duration: 300 }}>
        Item {item}
    </div>
{/each}

You can also create custom transitions by returning a function with duration, delay, easing, and css or tick properties.

11. SvelteKit: Routing, Layouts, and Loading Data

SvelteKit uses file-based routing. Each directory in src/routes becomes a URL path:

src/routes/
  +page.svelte          # /
  +layout.svelte        # Shared layout for all pages
  about/
    +page.svelte        # /about
  blog/
    +page.svelte        # /blog
    [slug]/
      +page.svelte      # /blog/my-post (dynamic)
      +page.server.js   # Load data server-side

Layouts

<!-- src/routes/+layout.svelte -->
<script>
    let { children } = $props();
</script>

<nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
    <a href="/blog">Blog</a>
</nav>

<main>
    {@render children()}
</main>

<footer>Built with SvelteKit</footer>

Loading Data

// src/routes/blog/[slug]/+page.server.js
export async function load({ params, fetch }) {
    const res = await fetch(`/api/posts/${params.slug}`);
    if (!res.ok) throw error(404, "Post not found");
    const post = await res.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>

The load function runs on the server during SSR and on the client during navigation. Data is automatically passed as the data prop.

12. SvelteKit: Server-Side Rendering and API Routes

SvelteKit renders pages on the server by default, sending fully-formed HTML to the browser for fast first paint and SEO.

API Routes

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

export async function GET({ url }) {
    const page = Number(url.searchParams.get("page") ?? 1);
    const posts = await db.posts.findMany({
        skip: (page - 1) * 10, take: 10
    });
    return json(posts);
}

export async function POST({ request }) {
    const body = await request.json();
    if (!body.title) throw error(400, "Title required");
    const post = await db.posts.create({ data: body });
    return json(post, { status: 201 });
}

Form Actions

SvelteKit form actions handle form submissions server-side with progressive enhancement:

// src/routes/contact/+page.server.js
export const actions = {
    default: async ({ request }) => {
        const data = await request.formData();
        const email = data.get("email");
        const message = data.get("message");

        if (!email) return { success: false, error: "Email required" };
        await sendEmail({ to: "admin@example.com", from: email, body: message });
        return { success: true };
    }
};
<!-- src/routes/contact/+page.svelte -->
<script>
    import { enhance } from "$app/forms";
    let { form } = $props();
</script>

<form method="POST" use:enhance>
    <input name="email" type="email" required />
    <textarea name="message" required></textarea>
    <button type="submit">Send</button>
    {#if form?.success}<p>Message sent!</p>{/if}
    {#if form?.error}<p class="error">{form.error}</p>{/if}
</form>

The use:enhance action progressively enhances the form: it works without JavaScript and upgrades to AJAX submissions when JS is available.

13. Svelte vs React vs Vue

Feature Svelte React Vue
Approach Compiler Virtual DOM Virtual DOM + Compiler
Bundle size (min) ~2 KB ~42 KB ~33 KB
Reactivity Compile-time ($state) useState / signals Proxy-based (ref/reactive)
Styling Scoped by default CSS-in-JS / modules Scoped (optional)
Animations Built-in directives Third-party (Framer) Built-in <Transition>
Meta framework SvelteKit Next.js Nuxt
Learning curve Low Medium Low-Medium

Choose Svelte when you want minimal bundle size, fast performance, and less boilerplate. Choose React when you need the largest ecosystem and job market. Choose Vue when you want a balance of simplicity and ecosystem size. All three are excellent choices for production applications.

14. Best Practices

  • Use SvelteKit for new projects — it handles routing, SSR, and deployment. There is no reason to use standalone Svelte without it.
  • Prefer $state over stores — in Svelte 5, use $state in .svelte.js files for shared state. Stores still work but runes are the future.
  • Always key your {#each} blocks{#each items as item (item.id)} prevents DOM reuse bugs when lists change.
  • Colocate load functions — keep +page.server.js next to +page.svelte for clear data flow.
  • Use TypeScript — Svelte has first-class TS support. Typed props with $props() catch errors at build time.
  • Avoid $effect for derived state — if you are setting state inside an effect, use $derived instead. Effects are for side effects (DOM manipulation, network requests, logging).
  • Progressive enhancement — use use:enhance on forms so they work without JavaScript.
  • Keep components small — if a component exceeds 200 lines, extract logic into composable .svelte.js modules.
  • Use $lib alias — import shared code from $lib/ to avoid brittle relative paths.
  • Prerender static pages — add export const prerender = true to pages that do not need dynamic server rendering.

Frequently Asked Questions

What is Svelte and how is it different from React or Vue?

Svelte is a component framework that shifts work from the browser to a compile step. Unlike React and Vue, which use a virtual DOM to diff changes at runtime, Svelte compiles your components into efficient imperative JavaScript that surgically updates the DOM. This means no virtual DOM overhead, smaller bundle sizes, and faster runtime performance. Svelte components are written in .svelte files that combine HTML, CSS, and JavaScript in a single file with scoped styles by default.

How does Svelte compare to React in performance and developer experience?

Svelte produces smaller bundles because there is no framework runtime shipped to the browser. A typical Svelte app ships 2-5 KB of framework code compared to React's 40+ KB. Svelte is faster for updates because it compiles reactivity into direct DOM mutations instead of virtual DOM diffing. Developer experience is also simpler: no useState or useEffect, no hooks rules, and two-way binding is built in. However, React has a larger ecosystem, more third-party libraries, and a bigger job market.

What is SvelteKit and when should I use it?

SvelteKit is the official application framework for Svelte, similar to what Next.js is to React or Nuxt is to Vue. It provides file-based routing, server-side rendering (SSR), static site generation (SSG), API routes, form actions, and deployment adapters for various platforms. Use SvelteKit for any production Svelte application that needs routing, SEO, or server-side capabilities. It is the recommended way to start new Svelte projects.

What are Svelte 5 runes and how do they change reactivity?

Runes are Svelte 5's new reactivity primitives that replace the implicit let-based reactivity of Svelte 3/4. The main runes are $state() for reactive state, $derived() for computed values, $effect() for side effects, and $props() for component props. Runes make reactivity explicit and composable. Unlike the old system where any top-level let was reactive, runes work everywhere including in regular .js/.ts files, enabling better code organization and reuse outside of .svelte components.

Why is Svelte considered fast and what about its performance?

Svelte is fast because of its compile-time approach. The compiler analyzes your components and generates minimal JavaScript that updates only the specific DOM nodes that change. There is no virtual DOM diffing, no runtime reactivity system, and no framework overhead. Benchmarks consistently show Svelte outperforming React and Vue in startup time, memory usage, and update speed. The compiled output is also tree-shakeable, so unused features add zero bytes to your bundle.

Related Resources

Keep building: Svelte's compiler-first approach makes it one of the fastest frameworks available. Pair it with TypeScript for type safety, explore the React Hooks Guide to compare approaches, and use our JSON Formatter when working with API data in your SvelteKit load functions.

Related Resources

React Hooks: The Complete Guide
Compare Svelte runes with React's hooks system
Vue 3: The Complete Guide
See how Vue's Composition API compares to Svelte
Next.js Complete Guide
The React equivalent of SvelteKit for full-stack apps
TypeScript Complete Guide
Type-safe Svelte development with TypeScript