Back in early 2020, I wrote a blog post titled “Svelte 3: The Compiler as Your Framework”. Like many developers at the time, I was blown away by Rich Harris’s compiler-centric approach that promised to solve the virtual DOM overhead of React while offering a delightfully simple developer experience. The “write less code” mantra resonated with me, and for a handful of smaller projects, Svelte proven to be beyond useful.

But then, as projects grew more complex and team considerations came into play, Svelte gradually slipped from my daily toolkit. The ecosystem wasn’t quite there yet. Component libraries were sparse compared to React’s thriving marketplace. Finding developers experienced with Svelte was challenging. The “real world” pushed me back toward React and occasionally Vue for client work.

Fast forward to today, and Svelte 5 has finally officially landed. When the Svelte team announced its release last month, my interest was immediately piqued.

Runes: Reactivity Reimagined

The most significant change in Svelte 5 is the introduction of “runes” — a new reactivity system that fundamentally changes how state management works. If you’re unfamiliar with the term, the Svelte team defines runes as “a letter or mark used as a mystical or magic symbol,” which feels appropriate given how they transform Svelte’s internals.

In Svelte 3 and 4, reactivity was largely implicit. When you updated a variable, Svelte would magically know to update the DOM. This approach was elegant but had limitations when dealing with more complex state management scenarios.

The new runes-based system makes reactivity explicit using special symbols like $: for derived values, $state() for reactive variables, and $effect() for side effects. Here’s a simple example:

function TaskTracker() {
  let tasks = $state([
    { id: 1, text: 'Learn Svelte 5', completed: false },
    { id: 2, text: 'Build a demo app', completed: false }
  ]);
  
  // Derived state using runes
  let completedCount = $derived(tasks.filter(t => t.completed).length);
  let pendingCount = $derived(tasks.length - completedCount);
  
  $effect(() => {
    if (completedCount === tasks.length && tasks.length > 0) {
      console.log('All tasks completed! 🎉');
    }
  });

  return {
    addTask(text) {
      tasks = [...tasks, { id: Date.now(), text, completed: false }];
    },
    toggleTask(id) {
      tasks = tasks.map(task => 
        task.id === id ? { ...task, completed: !task.completed } : task
      );
    },
    get tasks() { return tasks; },
    get progress() { return `${completedCount}/${tasks.length} completed`; }
  };
}

This approach feels more explicit and composable than before. It reminds me somewhat of React’s hooks, but with Svelte’s trademark less-boilerplate approach. The main benefit is that these runes work consistently across both component and non-component code, making it easier to extract logic.

I admit I initially approached runes with skepticism. Another syntax to learn? More magic? But my perspective completely shifted when I realized what they enable outside of .svelte files. For the first time, I can create reactive logic in standard JavaScript modules that can be imported into Svelte templates while fully encapsulating their reactivity.

This is revolutionary for testing. I can now write vitest tests against these external reactive components, something that was awkward or impossible before. As “Introducing runes” from the early days, explains: “Having code behave one way inside .svelte files and another inside .js can make it hard to refactor code… With runes, reactivity extends beyond the boundaries of your .svelte files.”

// shoppingCart.js - a fully testable reactive module
export function createShoppingCart() {
  const items = $state([]);
  const isPromoApplied = $state(false);
  
  // Derived values
  const subtotal = $derived(
    items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  );
  
  const discount = $derived(isPromoApplied ? subtotal * 0.1 : 0);
  const total = $derived(subtotal - discount);
  
  function addItem(product, quantity = 1) {
    const existingItem = items.find(item => item.id === product.id);
    
    if (existingItem) {
      items = items.map(item => 
        item.id === product.id 
          ? {...item, quantity: item.quantity + quantity}
          : item
      );
    } else {
      items = [...items, {...product, quantity}];
    }
  }
  
  return {
    get items() { return items; },
    get subtotal() { return subtotal; },
    get total() { return total; },
    addItem,
    applyPromo(valid) { isPromoApplied = valid; }
  };
}

// And in your component:
import { createShoppingCart } from './shoppingCart.js';

function CheckoutComponent() {
  const cart = createShoppingCart();
  // Use cart.items, cart.total and methods in your component
}

While many frontend developers rely primarily on TypeScript to guarantee correctness (and the Svelte community has had a complex relationship with TypeScript), I’ve always preferred the confidence that comes from robust testing. Svelte 5 finally bridges this gap in a way no other mainstream framework has managed.

Crucially, this system remains a compile-time transformation — Svelte is still generating highly optimized vanilla JavaScript rather than shipping a runtime framework. The performance benefits that initially attracted me to Svelte 3 remain intact, but with a more flexible state management model.

Snippets: Composition Without the Complexity

Another game-changer is the new snippet feature, which provides a more intuitive way to build reusable UI elements:

{#snippet todoItem(todo)}
  <div class="flex items-center gap-2">
    <input type="checkbox" bind:checked={todo.completed} />
    <span class:line-through={todo.completed}>
      {todo.title}
    </span>
    <button on:click={() => removeTodo(todo.id)}>×</button>
  </div>
{/snippet}

<main>
  {#each todos as todo (todo.id)}
    {@render todoItem(todo)}
  {/each}
</main>

This solves one of my long-standing frustrations with Svelte — the awkwardness of component composition compared to frameworks like React. Snippets provide a clean, declarative way to define and reuse template fragments without the overhead of creating full components.

The mental model is straightforward: define a snippet with parameters, then render it wherever needed with the {@render ...} syntax. No more slot spaghetti or prop drilling just to extract a small piece of UI logic.

The New CLI: A Unified Command Line Interface

Perhaps one of the most practical quality-of-life improvements is the introduction of the new sv CLI that unifies the Svelte development experience. While Svelte has had various CLI tools in the past (including those offered through SvelteKit), the new dedicated CLI brings a more cohesive approach to development workflows.

The sv command serves as a single entrypoint for Svelte-related tasks, with various subcommands that handle different aspects of the development cycle:

# Creating a new project
npm create svelte@latest my-project

# Using the sv CLI
npx sv check   # Type checking
npx sv format  # Formatting Svelte files
npx sv build   # Building for production

What I particularly appreciate is how the CLI integrates with existing tooling while providing Svelte-specific optimizations. For instance, the build command produces highly optimized bundles tailored specifically for Svelte’s compiler output.

For those of us working with Svelte in production environments, having a standardized CLI that handles everything from type checking to builds represents a significant step forward in maturity for the ecosystem.

Migration: Not as Painful as Expected

One of my concerns about Svelte 5 was backward compatibility. The introduction of runes represents a significant shift in Svelte’s mental model, and I worried it might break existing codebases.

Thankfully, the Svelte team has been remarkably thoughtful about this transition. As sveltekit.io notes, “Starting a new project with Svelte 5… they give you the option to opt into Svelte 5 features, including runes.” You can even scope runes on a component-by-component basis.

In my testing, migrating a small Svelte 4 application to Svelte 5 was relatively painless. The old reactive syntax still works, and you can gradually adopt runes as you update components. The comprehensive migration guide provides clear examples for each pattern that needs updating.

The Ecosystem: Growing Rapidly

One of the reasons I originally drifted away from Svelte was its limited ecosystem compared to React. While it’s still not at React’s level (and may never need to be), the situation has improved dramatically.

Major UI libraries like TailwindCSS work seamlessly with Svelte 5. Component libraries such as Skeleton, Carbon, and Melt UI have either already updated for Svelte 5 or have release candidates available. The SvelteKit framework (Svelte’s answer to Next.js) has been updated to fully leverage Svelte 5’s capabilities.

The TypeScript support is now excellent, with proper typing for all the new runes and snippets features. This was another pain point in earlier versions that has been thoroughly addressed.

So, Where Does Svelte 5 Fit in My Toolkit?

After spending time with Svelte 5, I’m thoroughly impressed, but I’m not quite ready to make it my default framework for all projects. The reality of client work often requires pragmatic choices based on team familiarity, long-term maintenance, and ecosystem maturity.

That said, Svelte 5 now has a definite place in my recommendations. For clients looking for exceptional performance on lightweight sites, rapid development cycles, or projects where bundle size matters (like content-heavy marketing sites or progressive web apps), Svelte is now my honest recommendation. As swyx.io noted way back in 2020, “Svelte for Sites, React for Apps” had a certain wisdom to it — but with Svelte 5’s improvements, that boundary is increasingly blurry.

The testability improvements are particularly compelling for projects that need to maintain high quality over time. When a client values clean code and maintainability as much as initial development speed, Svelte 5 offers a surprisingly mature solution.

What truly stands out is how Svelte 5 manages to add power and flexibility without sacrificing its core simplicity. That’s a rare achievement in the framework space, where feature additions often come with corresponding increases in complexity.

If you, like me, were once enamored with Svelte but drifted away as other frameworks evolved, now might be the perfect time to take another look. This innovative framework has returned to the spotlight, and while it may not dethrone the incumbents for every use case, it’s carved out a compelling niche that’s increasingly hard to ignore.