Skip to main content
โšก Calmops

Svelte 5 Complete Guide: The Future of Frontend Development

Introduction

Svelte has always been different from other frontend frameworks. While React and Vue use a virtual DOM to reconcile changes, Svelte compiles your code to efficient vanilla JavaScript that updates the DOM directly. The result? Blazing-fast applications with minimal bundle sizes.

Svelte 5, released in 2026, represents the biggest evolution of the framework yet. It introduces runes, a new reactivity system that makes state management more intuitive and powerful. It brings snippets for reusable template fragments, improved performance, and a more consistent developer experience.

This comprehensive guide walks you through everything you need to know about Svelte 5. Whether you’re new to Svelte or migrating from Svelte 4, you’ll find practical examples, best practices, and migration strategies.


What Makes Svelte Different

The Compilation Approach

Unlike React or Vue, Svelte compiles your components at build time:

Framework Approach Runtime
React Virtual DOM ~40KB
Vue Reactive proxies ~25KB
Svelte Direct DOM ~5KB

Svelte shifts work from the browser to the build process, resulting in:

  • Smaller bundle sizes
  • Faster runtime performance
  • No framework overhead

Why Svelte 5 Matters

Svelte 5 isn’t just incremental improvementsโ€”it’s a fundamental shift in how you write reactive code:

  • Runes: A new primitives-based reactivity system
  • Snippets: Reusable template fragments
  • Better TypeScript: Improved type inference
  • Performance: Even faster updates and smaller bundles

Getting Started with Svelte 5

Installation

Create a new Svelte 5 project:

# Using the new CLI
npm create svelte@latest my-app
cd my-app
npm install

# Select "Svelte 5" when prompted

Project Structure

my-app/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ lib/
โ”‚   โ”‚   โ””โ”€โ”€ components/
โ”‚   โ”‚       โ””โ”€โ”€ Counter.svelte
โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ””โ”€โ”€ +page.svelte
โ”‚   โ””โ”€โ”€ app.html
โ”œโ”€โ”€ static/
โ”œโ”€โ”€ svelte.config.js
โ”œโ”€โ”€ vite.config.js
โ””โ”€โ”€ package.json

Understanding Runes

Runes are the heart of Svelte 5’s reactivity system. They’re special functions that tell Svelte how to track and update state.

The Core Runes

$state: Reactive Variables

<script>
  let count = $state(0);
  
  function increment() {
    count += 1;
  }
</script>

<button onclick={increment}>
  Clicks: {count}
</button>

Unlike Svelte 4’s let declarations, $state explicitly marks variables as reactive. This makes the reactivity system more explicit and easier to understand.

$derived: Computed Values

<script>
  let count = $state(0);
  
  // Automatically updates when count changes
  let doubled = $derived(count * 2);
  let isEven = $derived(count % 2 === 0);
</script>

<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Is Even: {isEven}</p>

$effect: Side Effects

<script>
  let count = $state(0);
  
  // Runs when count changes
  $effect(() => {
    console.log(`Count is now: ${count}`);
    
    // Cleanup function
    return () => {
      console.log('Cleanup');
    };
  });
</script>

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

$props: Component Properties

<script>
  let { name = 'World', count = 0, ...rest } = $props();
</script>

<h1>Hello, {name}!</h1>
<p>Count: {count}</p>

$bindable: Two-Way Binding

<!-- Child: Modal.svelte -->
<script>
  let { open = $bindable(false), title = '' } = $props();
</script>

{#if open}
  <dialog open>
    <h2>{title}</h2>
    <button onclick={() => open = false}>Close</button>
  </dialog>
{/if}
<!-- Parent -->
<script>
  import Modal from './Modal.svelte';
  
  let showModal = $state(false);
</script>

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

<Modal bind:open={showModal} title="Hello!" />

Snippets: Reusable Template Fragments

Snippets replace slots and named slots with a more intuitive syntax.

Basic Snippets

<script>
  let { items = [] } = $props();
  
  function renderItem(item) {
    return `<li>${item}</li>`;
  }
</script>

{#snippet itemTemplate(item)}
  <li class="item">
    <span class="name">{item.name}</span>
    <span class="price">${item.price}</span>
  </li>
{/snippet}

<ul>
  {#each items as item}
    {@render itemTemplate(item)}
  {/each}
</ul>

Snippets with Parameters

{#snippet card(title, content)}
  <div class="card">
    <h3>{title}</h3>
    <p>{content}</p>
  </div>
{/snippet}

{@render card('Feature 1', 'Description 1')}
{@render card('Feature 2', 'Description 2')}

Passing Snippets as Props

<!-- List.svelte -->
<script>
  let { items = [], renderItem } = $props();
</script>

<ul>
  {#each items as item}
    {@render renderItem(item)}
  {/each}
</ul>
<!-- Usage -->
<script>
  import List from './List.svelte';
  
  let items = $state(['Apple', 'Banana', 'Cherry']);
  
  {#snippet fruitItem(name)}
    <li>๐ŸŽ {name}</li>
  {/snippet}
</script>

<List {items} renderItem={fruitItem} />

Migrating from Svelte 4

If you’re coming from Svelte 4, here’s what you need to know.

Key Changes

Svelte 4 Svelte 5
let count = 0 let count = $state(0)
$: doubled = count * 2 let doubled = $derived(count * 2)
$: { console.log(count) } $effect(() => console.log(count))
export let name let { name } = $props()
<slot /> {@render children()}

Step-by-Step Migration

1. Update State Declarations

<!-- Svelte 4 -->
<script>
  let count = 0;
  let items = [];
  let user = { name: 'John' };
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  let items = $state([]);
  let user = $state({ name: 'John' });
</script>

2. Convert Computed Values

<!-- Svelte 4 -->
<script>
  let count = 0;
  $: doubled = count * 2;
  $: names = items.map(i => i.name);
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  let items = $state([]);
  
  let doubled = $derived(count * 2);
  let names = $derived(items.map(i => i.name));
</script>

3. Update Effects

<!-- Svelte 4 -->
<script>
  let count = 0;
  $: {
    console.log(count);
    localStorage.setItem('count', count);
  }
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  
  $effect(() => {
    console.log(count);
    localStorage.setItem('count', count);
  });
</script>

4. Migrate Props

<!-- Svelte 4 -->
<script>
  export let title = 'Default';
  export let count = 0;
</script>

<!-- Svelte 5 -->
<script>
  let { title = 'Default', count = 0, ...rest } = $props();
</script>

5. Migrate Slots to Snippets

<!-- Svelte 4 -->
<!-- Button.svelte -->
<button class="btn">
  <slot>Click me</slot>
</button>

<!-- Usage -->
<Button>Submit</Button>

<!-- Svelte 5 -->
<!-- Button.svelte -->
<script>
  let { children, ...rest } = $props();
</script>

<button class="btn">
  {@render children?.() ?? 'Click me'}
</button>

<!-- Usage -->
<Button>Submit</Button>

Using the Migration Tool

Svelte provides a codemod for automatic migration:

npx svelte-migrate@latest svelte-5

Advanced Svelte 5 Patterns

Deep Reactivity with $state

<script>
  // Deep reactive - nested changes trigger updates
  let user = $state({
    profile: {
      name: 'John',
      settings: {
        theme: 'dark'
      }
    }
  });
  
  function updateTheme(theme) {
    // This will trigger updates anywhere user is used
    user.profile.settings.theme = theme;
  }
</script>

$derived.by for Complex Calculations

<script>
  let items = $state([
    { name: 'Apple', price: 1.5, quantity: 3 },
    { name: 'Banana', price: 0.5, quantity: 6 }
  ]);
  
  let summary = $derived.by(() => {
    const total = items.reduce((sum, item) => 
      sum + (item.price * item.quantity), 0
    );
    const count = items.length;
    const average = count > 0 ? total / count : 0;
    
    return { total, count, average };
  });
</script>

<p>Total: ${summary.total.toFixed(2)}</p>
<p>Items: {summary.count}</p>
<p>Average: ${summary.average.toFixed(2)}</p>

Stores in Svelte 5

While runes are the new default, stores still work:

<script>
  import { writable } from 'svelte/store';
  
  const count = writable(0);
</script>

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

However, runes are recommended for new code:

<script>
  // Preferred in Svelte 5
  let count = $state(0);
</script>

Component Patterns

Props with Validation

<script>
  import { type ComponentProps } from 'svelte';
  
  let { 
    name, 
    age, 
    email = $bindable() 
  } = $props();
  
  // Runtime validation
  if (age < 0 || age > 150) {
    throw new Error('Invalid age');
  }
</script>

Event Handling

<script>
  let { onClick, onHover, children } = $props();
  
  function handleClick(e) {
    onClick?.(e);
  }
</script>

<button onclick={handleClick}>
  {@render children?.()}
</button>

Conditional Classes

<script>
  let { active = false, variant = 'primary' } = $props();
  
  let className = $derived(
    ['btn', `btn-${variant}`, active && 'active']
      .filter(Boolean)
      .join(' ')
  );
</script>

<button class={className}>
  <slot />
</button>

Performance Optimization

Using $effect Carefully

Effects run after every render. Use them sparingly:

<script>
  // Good: LocalStorage sync
  let theme = $state('dark');
  
  $effect(() => {
    localStorage.setItem('theme', theme);
  });
  
  // Better: Only run when specific value changes
  $effect(() => {
    document.documentElement.setAttribute('data-theme', theme);
  });
</script>

Fine-Grained Reactivity

Svelte 5’s reactivity is fine-grained:

<script>
  let user = $state({
    name: 'John',
    email: '[email protected]'
  });
  
  // Only re-renders when name changes
  function updateName(newName) {
    user.name = newName;
  }
  
  // Only re-renders when email changes  
  function updateEmail(newEmail) {
    user.email = newEmail;
  }
</script>

<h1>{user.name}</h1>      <!-- Updates independently -->
<p>{user.email}</p>        <!-- Updates independently -->

Svelte 5 with TypeScript

TypeScript is Built In

<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
  }
  
  let user = $state<User>({
    id: 1,
    name: 'John',
    email: '[email protected]'
  });
  
  function greet(name: string): string {
    return `Hello, ${name}!`;
  }
</script>

Typing Props

<script lang="ts">
  interface Props {
    title: string;
    count?: number;
    onSave?: (value: string) => void;
  }
  
  let { 
    title, 
    count = 0, 
    onSave,
    ...rest 
  }: Props = $props();
</script>

Typing Snippets

<script lang="ts">
  import type { Snippet } from 'svelte';
  
  interface Props {
    items: string[];
    renderItem: Snippet<[string]>;
  }
  
  let { items, renderItem }: Props = $props();
</script>

{#each items as item}
  {@render renderItem(item)}
{/each}

External Resources

Official Documentation

Learning Resources

Community


Conclusion

Svelte 5 represents a significant evolution in frontend development. The new runes system makes reactivity more explicit and powerful, while snippets provide a cleaner way to create reusable template fragments.

Key takeaways:

  1. Runes are the future: Use $state, $derived, $effect, and $props for all new code
  2. Migration is gradual: Svelte 4 code continues to work; migrate when comfortable
  3. Performance is built in: Fine-grained reactivity means less work for you
  4. TypeScript is first-class: Full TypeScript support out of the box

Whether you’re building a small widget or a full-stack application, Svelte 5 provides the tools you need to ship faster with smaller, more performant code.


Comments