teta.so

Svelte 5: What's New and How to Migrate

Everything new in Svelte 5: runes ($state, $derived, $effect, $props), snippets, event handling, migration from Svelte 4, and performance.

Svelte 5 is the most significant update in the framework's history. Released in October 2024, it replaces Svelte's implicit reactivity system with an explicit, signal-based model called runes. The result is more predictable behavior, better TypeScript support, improved performance, and a reactivity system that works beyond component boundaries. If you are starting a new Svelte project in 2026, you are using Svelte 5 by default. If you are maintaining a Svelte 4 codebase, this guide covers what changed, why it changed, and how to migrate. Tools like Teta generate Svelte 5 code with runes by default, so you can start building with the latest syntax immediately.

What Changed in Svelte 5

Svelte 4 had a distinctive feature: reactivity was implicit. Declaring a variable with let made it reactive. Assigning to it triggered DOM updates. Reactive statements used the $: label syntax — a clever hack that repurposed a rarely-used JavaScript label as a reactive declaration.

<!-- Svelte 4 (old syntax) -->
<script>
  let count = 0;
  $: doubled = count * 2;
  $: console.log('Count changed:', count);
</script>

<button on:click={() => count++}>
  {count} doubled is {doubled}
</button>

This worked well for simple cases but had edge cases and limitations. Reactivity only worked at the top level of a component. You could not share reactive state between files. The $: syntax was confusing for newcomers and made complex reactive flows hard to reason about.

Svelte 5 replaces all of this with runes — explicit, composable reactivity primitives that use a function-call-like syntax starting with $.

<!-- Svelte 5 (new syntax) -->
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);

  $effect(() => {
    console.log('Count changed:', count);
  });
</script>

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

The behavior is the same, but the intent is explicit. You can see at a glance which values are reactive, which are derived, and which have side effects.

Runes: The New Reactivity System

$state

$state declares a reactive value. When the value changes, anything that reads it updates.

<script>
  let name = $state('world');
  let items = $state<string[]>([]);
</script>

<input bind:value={name} />
<p>Hello, {name}!</p>

For objects and arrays, $state creates a deeply reactive proxy. Mutations are tracked automatically:

<script>
  let todos = $state([
    { text: 'Learn Svelte 5', done: false },
    { text: 'Build something', done: false }
  ]);

  function toggle(index: number) {
    todos[index].done = !todos[index].done; // This triggers updates
  }
</script>

In Svelte 4, you had to reassign the array (todos = todos) to trigger an update. In Svelte 5, mutations are tracked automatically.

$derived

$derived creates a value that recomputes when its dependencies change. It replaces $: reactive declarations.

<script>
  let items = $state([1, 2, 3, 4, 5]);
  let filter = $state('all');

  let filtered = $derived(
    filter === 'even' ? items.filter(i => i % 2 === 0) :
    filter === 'odd' ? items.filter(i => i % 2 !== 0) :
    items
  );

  let count = $derived(filtered.length);
</script>

For complex derivations that need multiple statements, use $derived.by:

<script>
  let data = $state<Record<string, number>>({});

  let summary = $derived.by(() => {
    const values = Object.values(data);
    const total = values.reduce((sum, v) => sum + v, 0);
    const avg = values.length > 0 ? total / values.length : 0;
    return { total, avg, count: values.length };
  });
</script>

$effect

$effect runs a function whenever its reactive dependencies change. It replaces $: statements used for side effects.

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

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

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

Effects run after the DOM has updated, so you can safely reference DOM elements. The cleanup function (returned from the effect) runs before the next invocation and when the component unmounts.

$props

$props replaces export let for declaring component props.

<!-- Svelte 4 -->
<script>
  export let name;
  export let count = 0;
</script>

<!-- Svelte 5 -->
<script lang="ts">
  interface Props {
    name: string;
    count?: number;
    children?: import('svelte').Snippet;
  }

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

The destructuring syntax makes defaults, rest props, and TypeScript typing all straightforward:

<script lang="ts">
  let { class: className, children, ...rest }: {
    class?: string;
    children?: import('svelte').Snippet;
    [key: string]: unknown;
  } = $props();
</script>

<div class={className} {...rest}>
  {@render children?.()}
</div>

$bindable

For two-way binding of props (where a child component can update a parent's state), use $bindable:

<script lang="ts">
  let { value = $bindable('') }: { value: string } = $props();
</script>

<input bind:value={value} />

The parent can then use bind:value:

<script>
  import TextInput from './TextInput.svelte';
  let name = $state('');
</script>

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

Snippets: The New Composition Model

Snippets replace slots as the way to pass renderable content to components. They are more powerful and more explicit.

<!-- Defining and using a snippet in the same component -->
<script>
  let items = $state(['Apple', 'Banana', 'Cherry']);
</script>

{#snippet listItem(item: string, index: number)}
  <li>
    <strong>{index + 1}.</strong> {item}
  </li>
{/snippet}

<ul>
  {#each items as item, i}
    {@render listItem(item, i)}
  {/each}
</ul>

Snippets can be passed as props to child components, replacing named slots:

<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';

  let { header, children }: {
    header: Snippet;
    children: Snippet;
  } = $props();
</script>

<div class="card">
  <div class="card-header">
    {@render header()}
  </div>
  <div class="card-body">
    {@render children()}
  </div>
</div>
<!-- Usage -->
<Card>
  {#snippet header()}
    <h2>My Card Title</h2>
  {/snippet}

  <p>This is the card body content.</p>
</Card>

Event Handling Changes

Svelte 5 replaces the on: directive with standard HTML event attributes:

<!-- Svelte 4 -->
<button on:click={handleClick}>Click</button>
<button on:click|preventDefault={handleSubmit}>Submit</button>

<!-- Svelte 5 -->
<button onclick={handleClick}>Click</button>
<button onclick={(e) => { e.preventDefault(); handleSubmit(e); }}>Submit</button>

Component events also changed. Instead of createEventDispatcher, pass callback props:

<!-- Svelte 4 component -->
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();
</script>
<button on:click={() => dispatch('select', { id: 1 })}>Select</button>

<!-- Svelte 5 component -->
<script lang="ts">
  let { onSelect }: { onSelect: (id: number) => void } = $props();
</script>
<button onclick={() => onSelect(1)}>Select</button>

This is simpler, more explicit, and works better with TypeScript.

Migration from Svelte 4

Automatic Migration

Svelte provides a migration tool that handles most of the conversion:

npx sv migrate svelte-5

This converts export let to $props(), $: to $derived/$effect, on: directives to event attributes, slots to snippets, and createEventDispatcher to callback props.

What the Tool Does Not Handle

Some patterns require manual migration:

  • Complex $: blocks that mix derivations and side effects need to be split into separate $derived and $effect calls.
  • $$props and $$restProps need to be replaced with rest destructuring in $props().
  • Custom stores wrapping writable/readable may need rethinking (though the store API still works).

Gradual Migration

You do not have to migrate everything at once. Svelte 5 components can import and use Svelte 4 components, and vice versa. The compiler handles both syntaxes in the same project. This means you can migrate component by component, starting with the ones that benefit most from the new syntax.

Performance Improvements

Svelte 5 is faster than Svelte 4 across the board:

  • Fine-grained reactivity. Instead of re-running entire reactive blocks when any dependency changes, Svelte 5 tracks exactly which DOM nodes depend on each piece of state. Updates are surgically precise.
  • Smaller compiled output. The new compiler generates less code per component. For large applications, this reduces bundle size by 10-20%.
  • Faster hydration. The fine-grained tracking means hydration sets up fewer connections and runs faster.
  • Better memory usage. Reactive proxies share infrastructure, reducing per-component memory overhead.

In benchmarks, Svelte 5 is approximately 20-40% faster than Svelte 4 for updates and uses less memory for large component trees.

Breaking Changes

Beyond the syntax changes, there are a few breaking changes to be aware of:

  • Whitespace handling is more consistent. Extra whitespace in templates is trimmed more aggressively.
  • Null/undefined rendering. Svelte 5 renders null and undefined as empty strings, not the literal text "null" or "undefined".
  • CSS scoping. The :global() modifier behavior is slightly different. Global styles should use :global on the selector.
  • Transition directive. transition: still works but the recommended approach for new code is CSS transitions or the transition function from svelte/transition.

Most of these changes are caught by the migration tool or svelte-check. Run npx svelte-check after migration to find any remaining issues.

FAQ

What are runes in Svelte 5?

Runes are Svelte 5's reactivity primitives: $state (reactive values), $derived (computed values), $effect (side effects), $props (component props), and $bindable (two-way binding). They replace Svelte 4's implicit reactivity (let, $:, export let) with explicit declarations that are more predictable, composable, and TypeScript-friendly.

Is Svelte 5 backwards compatible?

Partially. Svelte 5 still supports Svelte 4 syntax in the same project, so you can migrate gradually. However, some behaviors changed (whitespace handling, null rendering, CSS scoping), and the new recommended patterns (runes, snippets, event attributes) differ from Svelte 4. The migration tool (npx sv migrate svelte-5) handles most conversions automatically.

How do I migrate from Svelte 4 to Svelte 5?

Run npx sv migrate svelte-5 to automatically convert most Svelte 4 patterns to Svelte 5 syntax. The tool handles export let to $props(), $: to $derived/$effect, on: to event attributes, and slots to snippets. After running the tool, use npx svelte-check to find any remaining issues that need manual attention.

What is $state in Svelte 5?

$state is the rune for declaring reactive values in Svelte 5. It replaces the implicit reactivity of let in Svelte 4. When you write let count = $state(0), the variable becomes reactive — any part of the template or any $derived or $effect that reads count will update when it changes. For objects and arrays, $state creates deep reactive proxies that track mutations automatically.

Should I use Svelte 5 for new projects?

Yes, absolutely. Svelte 5 is the current stable version and all new projects should use it. The runes system is more powerful and predictable than Svelte 4's implicit reactivity, TypeScript support is significantly better, and performance is improved across the board. Teta generates Svelte 5 code by default, so you can start building with the latest syntax right away.

Frequently Asked Questions

What are runes in Svelte 5?

Runes are Svelte 5's reactivity primitives: $state (reactive values), $derived (computed values), $effect (side effects), $props (component props), and $bindable (two-way binding). They replace Svelte 4's implicit reactivity ( let , $: , export let ) with explicit declarations that are more predictable, composable, and TypeScript-friendly.

Is Svelte 5 backwards compatible?

Partially. Svelte 5 still supports Svelte 4 syntax in the same project, so you can migrate gradually. However, some behaviors changed (whitespace handling, null rendering, CSS scoping), and the new recommended patterns (runes, snippets, event attributes) differ from Svelte 4. The migration tool ( npx sv migrate svelte-5 ) handles most conversions automatically.

How do I migrate from Svelte 4 to Svelte 5?

Run npx sv migrate svelte-5 to automatically convert most Svelte 4 patterns to Svelte 5 syntax. The tool handles export let to $props() , $: to $derived / $effect , on: to event attributes, and slots to snippets. After running the tool, use npx svelte-check to find any remaining issues that need manual attention.

What is $state in Svelte 5?

$state is the rune for declaring reactive values in Svelte 5. It replaces the implicit reactivity of let in Svelte 4. When you write let count = $state(0) , the variable becomes reactive — any part of the template or any $derived or $effect that reads count will update when it changes. For objects and arrays, $state creates deep reactive proxies that track mutations automatically.

Should I use Svelte 5 for new projects?

Yes, absolutely. Svelte 5 is the current stable version and all new projects should use it. The runes system is more powerful and predictable than Svelte 4's implicit reactivity, TypeScript support is significantly better, and performance is improved across the board. Teta generates Svelte 5 code by default, so you can start building with the latest syntax right away.

Ready to start building?

Create your next web app with AI-powered development tools.

Comece Grátis
← All articles