svelte
Svelte is a UI framework that works as a compiler: you write declarative components in .svelte files and it compiles them into plain JavaScript that updates the DOM directly, with no virtual DOM diffing at runtime. Since version 5, reactivity is explicit through 'runes' ($state, $derived, $effect), which replaced the older implicit let-and-$: reactivity. The result is small output, fast updates, and components that read close to plain HTML, CSS, and JS. Most real apps pair it with SvelteKit, its official app framework.
Technically excellent and genuinely pleasant to write; with SvelteKit it is a complete, actively developed stack (87.8K stars, pushed this week). The honest tradeoff is ecosystem size and the Svelte 5 content gap: you trade React's endless libraries and answers for cleaner code you will write more of yourself.
Use it if
- You want less framework ceremony: state is a plain variable with $state, templates are HTML-like, and styles are scoped per component by default
- Runtime payload and update performance matter; compiled output is small (the runtime the compiler injects is around 10.8 KB gzipped) and updates are targeted, not diffed
- You are building a content site or app with SvelteKit and want SSR, routing, and progressive enhancement handled by the official stack
- Your team is small or includes people closer to HTML/CSS than to React idioms; the learning curve to productive is short
- You need the biggest hiring pool and component-library market; React's ecosystem (component kits, job candidates, Stack Overflow answers, AI training data) is several times larger
- Your team has deep React investment; Svelte 5 runes are a different mental model and the migration cost buys you little if React already works for you
- You rely on lots of pre-2024 Svelte tutorials and libraries: Svelte 5 changed the core model (runes, snippets, event attributes), so much older content and some older libraries teach deprecated patterns
- You want to render components without a build step; Svelte is a compiler, so there is no realistic drop-a-script-tag usage the way Vue or Preact allow
Setup reality
You never install just svelte in practice; you scaffold with npx sv create (SvelteKit) or a Vite template, which wires the compiler plugin for you. That part is smooth. The real friction in 2026 is the version 4 to 5 transition: docs and runes are current, but a large share of blog posts, Stack Overflow answers, and third-party components still show pre-runes syntax (export let, on:click, slots), so beginners constantly hit deprecated patterns. Runes only work in .svelte and .svelte.js/.svelte.ts files, which trips people up when they try to share reactive state from a plain .ts module.
Patterns
Declare reactive state with $statereactive-state
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
clicked {count} times
</button>Runes like $state are compiler keywords, not imports; they only work in .svelte and .svelte.js/.svelte.ts files.
Compute a value from other statederived-value
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let big = $derived.by(() => {
// multi-line derivations use $derived.by
return count > 10 ? 'big' : 'small';
});
</script>This replaces Svelte 4's $: label syntax; $derived values are read-only and recompute lazily.
Run a side effect when state changesside-effect
<script>
let query = $state('');
$effect(() => {
const id = setTimeout(() => search(query), 300);
return () => clearTimeout(id); // cleanup runs before re-run and on destroy
});
</script>$effect tracks whatever it reads synchronously; values read inside setTimeout or await are not tracked, a common surprise.
Accept props with $propscomponent-props
<script>
let { name, size = 'md', ...rest } = $props();
</script>
<span class={`badge-${size}`} {...rest}>{name}</span>This replaces 'export let name' from Svelte 4; defaults and rest props come straight from destructuring.
Bind an input, and make a prop bindabletwo-way-binding
<!-- Input.svelte -->
<script>
let { value = $bindable('') } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script>
import Input from './Input.svelte';
let name = $state('');
</script>
<Input bind:value={name} />In Svelte 5 a component prop must be declared $bindable() before a parent may bind: to it; plain props are one-way.
Handle DOM events and component callbackshandle-events
<script>
let { onRemove } = $props(); // callback prop instead of createEventDispatcher
</script>
<button onclick={() => onRemove?.()}>remove</button>
<input oninput={(e) => console.log(e.currentTarget.value)} />Svelte 5 uses plain event attributes (onclick, oninput); the on:click directive and createEventDispatcher are deprecated in favor of callback props.
Render a keyed listlist-rendering
<ul>
{#each todos as todo (todo.id)}
<li class:done={todo.done}>{todo.text}</li>
{:else}
<li>Nothing to do</li>
{/each}
</ul>The (todo.id) key matters: without it Svelte patches rows by index, which breaks animations and input state on reorder.
Conditionals and awaiting promises in markupconditional-and-async
{#if user}
<p>Hello {user.name}</p>
{:else}
<p>Signed out</p>
{/if}
{#await fetchPosts()}
<p>loading...</p>
{:then posts}
<p>{posts.length} posts</p>
{:catch err}
<p>failed: {err.message}</p>
{/await}{#await} re-runs if the promise expression changes; in SvelteKit, prefer load functions for data that needs SSR.
Pass markup to a component with snippetsreusable-markup-snippets
<!-- Card.svelte -->
<script>
let { header, children } = $props();
</script>
<div class="card">
{@render header?.()}
{@render children?.()}
</div>
<!-- usage -->
<Card>
{#snippet header()}<h2>Title</h2>{/snippet}
<p>Body content becomes the children snippet</p>
</Card>Snippets replace Svelte 4 slots; content not wrapped in a named {#snippet} arrives as the implicit children snippet.
Share reactive state across componentsshared-state-module
// counter.svelte.js
export const counter = $state({ value: 0 });
// AnyComponent.svelte
<script>
import { counter } from './counter.svelte.js';
</script>
<button onclick={() => counter.value++}>{counter.value}</button>The file must be named *.svelte.js or *.svelte.ts for runes to compile, and you must export an object and mutate its properties; reassigning an exported binding breaks reactivity.
Get a DOM node and run mount logicbind-element-lifecycle
<script>
import { onMount } from 'svelte';
let canvas; // populated via bind:this
onMount(() => {
const ctx = canvas.getContext('2d');
ctx.fillRect(0, 0, 100, 100);
return () => {/* cleanup on destroy */};
});
</script>
<canvas bind:this={canvas} width="200" height="200"></canvas>onMount still exists in Svelte 5 and only runs in the browser, which makes it the standard place for DOM-only code under SSR.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react | npm | You want the largest ecosystem, hiring pool, and third-party component supply, and accept more boilerplate |
| vue | npm | You want a similar single-file-component feel with a bigger ecosystem and optional no-build usage |
| solid-js | npm | You want fine-grained signal reactivity like runes but with JSX instead of a template compiler |
| preact | npm | You need React compatibility at a tiny size rather than a different component model |