Svelte: Reactive UIs with No Virtual DOM
Every few years, a frontend framework comes along that challenges our assumptions about how UIs should be built. Svelte is one of those rare paradigm shifts. While React, Vue, and Angular ship a runtime to the browser — a JavaScript engine that manages state updates and reconciles the Virtual DOM at runtime — Svelte takes a radically different approach: it compiles your components to highly optimized, imperative JavaScript at build time. The result? No runtime overhead, no Virtual DOM diffing, and bundles that are often 70–90% smaller than equivalent React apps.
In this guide, you'll learn everything you need to go from zero to building production-ready Svelte applications, including SvelteKit for full-stack development. Whether you're an experienced React developer looking to branch out or a frontend newcomer evaluating your options, Svelte's elegant syntax and compiler magic will fundamentally change how you think about building user interfaces.
In the Stack Overflow Developer Survey, Svelte has ranked as the most loved web framework for three consecutive years. It powers high-traffic sites at Apple, The New York Times, Square, and Spotify. With over 80,000 GitHub stars and SvelteKit reaching stable v1.0, Svelte is no longer just a curiosity — it's a production-ready choice.
The Philosophy Behind Svelte
To understand why Svelte exists, you need to understand the problem it's solving. React, Vue, and Angular all ship a JavaScript runtime to the browser that manages reactivity. When state changes, the framework computes a diff between the old Virtual DOM tree and the new one, then applies the minimal set of changes to the real DOM. This approach is powerful but comes with costs:
- Bundle size: Even a "Hello World" React app includes ~40KB of framework code (minified + gzip)
- Memory overhead: Maintaining a Virtual DOM tree in memory doubles the representation of your UI
- CPU cost: Diffing algorithms run on every state change, even when the output is identical
- Cognitive complexity: Understanding hooks rules, dependency arrays, and memoization to avoid re-renders
Svelte's creator Rich Harris asked a fundamental question: what if the framework didn't need to be present at runtime at all? Instead of doing the work in the browser, Svelte does the work at compile time — transforming your .svelte files into efficient vanilla JavaScript that directly manipulates the DOM when state changes. No diffing, no runtime reconciliation, just surgical DOM updates.
Svelte's compile-time approach vs. traditional runtime frameworks
Svelte vs React vs Vue: A Comparison
| Feature | Svelte | React | Vue 3 |
|---|---|---|---|
| Approach | Compile-time | Runtime (Virtual DOM) | Runtime (Virtual DOM) |
| Bundle size (Hello World) | ~4KB | ~42KB | ~22KB |
| Reactivity | Assignments trigger updates | useState / hooks | ref() / reactive() |
| Learning curve | Low (HTML-like syntax) | Medium (JSX, hooks) | Low-Medium |
| Full-stack solution | SvelteKit | Next.js | Nuxt.js |
| TypeScript support | Built-in | Excellent | Excellent |
Setting Up Your First Svelte Project
The recommended way to start a Svelte project in 2026 is with SvelteKit, Svelte's official application framework (similar to how Next.js is to React). If you only need a simple single-page app, you can also scaffold with Vite directly.
Create a SvelteKit Project
Use the official SvelteKit template. It includes routing, server-side rendering, and TypeScript support out of the box.
# Create a new SvelteKit project
npm create svelte@latest my-svelte-app
# Follow the prompts:
# - Template: Skeleton project
# - TypeScript: Yes (recommended)
# - ESLint, Prettier: Yes
cd my-svelte-app
npm install
npm run dev
Alternative: Pure Svelte with Vite
For a simpler SPA without server-side rendering, use the Vite template. This is lighter but lacks SvelteKit's routing and SSR.
# Using Vite's Svelte template
npm create vite@latest my-svelte-spa -- --template svelte-ts
cd my-svelte-spa
npm install
npm run dev
Explore the Project Structure
Open the project in your editor. In SvelteKit, all routes live in src/routes/. Your first component to edit is src/routes/+page.svelte.
Install the official Svelte for VS Code extension (svelte.svelte-vscode) for syntax highlighting, IntelliSense, and auto-formatting. It also integrates with the Svelte Language Server for real-time type checking in .svelte files.
Anatomy of a Svelte Component
A Svelte component is a .svelte file with three optional sections: a <script> block for logic, a <style> block for CSS (automatically scoped to the component), and the HTML template. Unlike JSX, you write actual HTML — no className instead of class, no htmlFor instead of for.
Component Structure
<script lang="ts">
// Reactive variable — just a regular let declaration
let count: number = 0;
function increment() {
count += 1; // This assignment triggers a DOM update!
}
function reset() {
count = 0;
}
</script>
<!-- Template: plain HTML with {expressions} -->
<div class="counter">
<h2>Count: {count}</h2>
<button on:click={increment}>Increment</button>
<button on:click={reset} disabled={count === 0}>Reset</button>
</div>
<style>
/* Styles are automatically scoped — won't leak out */
.counter {
display: flex;
flex-direction: column;
gap: 12px;
padding: 24px;
}
button {
padding: 8px 16px;
background: #FF3E00;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
</style>
Notice that count is just a let variable — there's no useState hook or ref() call. Svelte's compiler detects which variables are referenced in the template and generates code to update the DOM whenever those variables are reassigned. The assignment count += 1 is all it takes.
Props and Component Communication
Svelte uses the export let syntax to declare props. This looks unusual at first, but it's Svelte's way of saying "this variable can be passed in from a parent component."
<script lang="ts">
// Props are declared with `export let`
export let label: string;
export let count: number = 0; // Default value
export let variant: 'primary' | 'secondary' = 'primary';
</script>
<span class="badge {variant}">
{label}
{#if count > 0}
<sup>{count}</sup>
{/if}
</span>
<style>
.badge { padding: 4px 10px; border-radius: 12px; font-size: 0.85rem; }
.primary { background: #FF3E00; color: white; }
.secondary { background: #e0e0e0; color: #333; }
</style>
<script lang="ts">
import Badge from './Badge.svelte';
let notifications = 5;
</script>
<Badge label="Inbox" count={notifications} variant="primary" />
<Badge label="Archive" /> <!-- Uses defaults -->
Template Directives
Svelte's template syntax is clean and expressive. Here are the directives you'll use daily:
<!-- if / else if / else -->
{#if user.role === 'admin'}
<AdminPanel />
{:else if user.role === 'editor'}
<EditorPanel />
{:else}
<ReadOnlyView />
{/if}
<!-- each block with index and key -->
{#each products as product, i (product.id)}
<div class="product-card">
<span>{i + 1}.</span>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
{:else}
<p>No products found.</p>
{/each}
<script lang="ts">
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
let userPromise = fetchUser(1);
</script>
{#await userPromise}
<p>Loading...</p>
{:then user}
<h2>{user.name}</h2>
{:catch error}
<p class="error">{error.message}</p>
{/await}
<script lang="ts">
let name = '';
let agreed = false;
let selectedColor = 'red';
</script>
<!-- bind:value creates two-way data binding -->
<input bind:value={name} placeholder="Your name" />
<input type="checkbox" bind:checked={agreed} />
<select bind:value={selectedColor}>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<p>Hello, {name}! Agreed: {agreed}</p>
Svelte's Reactive Declarations
One of Svelte's most powerful features is the $: label — a JavaScript labelled statement that Svelte repurposes for reactive declarations. Any statement prefixed with $: will re-run automatically whenever its dependencies change.
Reactive Statements
<script lang="ts">
let firstName = 'Mayur';
let lastName = 'Dabhi';
// $: creates a reactive declaration — recomputes when deps change
$: fullName = `${firstName} ${lastName}`;
$: initials = `${firstName[0]}${lastName[0]}`;
let items: string[] = [];
let filter = '';
// Complex reactive statement — runs when items or filter changes
$: filteredItems = items.filter(item =>
item.toLowerCase().includes(filter.toLowerCase())
);
// Reactive statement (not just expression)
$: {
if (filteredItems.length === 0 && filter !== '') {
console.log('No items match the filter:', filter);
}
}
// Reactive statement that depends on a condition
$: document.title = `${filteredItems.length} results for "${filter}"`;
</script>
Svelte tracks reactivity through assignments, not mutations. If you do items.push('new item'), Svelte won't detect the change because no assignment occurred. Instead, reassign the array: items = [...items, 'new item'] — or assign it back to itself after mutation: items.push('x'); items = items;. This is the most common gotcha for developers coming from Vue.
Svelte Stores: Shared State
When you need to share state between components that aren't in a parent-child relationship, Svelte provides stores — observable objects that any component can subscribe to. The API is minimal: writable, readable, and derived.
Creating and Using Stores
import { writable, derived } from 'svelte/store';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
// writable store — can be read and written from anywhere
export const cartItems = writable<CartItem[]>([]);
// derived store — computed from cartItems, updates automatically
export const cartTotal = derived(cartItems, ($items) =>
$items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
export const cartCount = derived(cartItems, ($items) =>
$items.reduce((sum, item) => sum + item.quantity, 0)
);
// Helper functions that update the store
export function addToCart(item: Omit<CartItem, 'quantity'>) {
cartItems.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
export function removeFromCart(id: number) {
cartItems.update(items => items.filter(i => i.id !== id));
}
<script lang="ts">
import { cartCount, cartTotal, removeFromCart } from '../stores/cart';
// The $ prefix auto-subscribes to a store.
// Svelte handles subscribe() and unsubscribe() lifecycle for you.
// $cartCount and $cartTotal are always the current values.
</script>
<button class="cart-btn">
Cart ({$cartCount} items) — ${$cartTotal.toFixed(2)}
</button>
Svelte stores enable shared state without prop drilling or Context API boilerplate
Built-in Transitions and Animations
One of the most delightful aspects of Svelte is how effortless animations are. While React requires libraries like Framer Motion or React Spring, Svelte ships with built-in transition and animation directives that apply CSS transitions when elements enter or leave the DOM.
<script lang="ts">
import { fade, fly, slide, scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
let visible = true;
let items = ['Apple', 'Banana', 'Cherry'];
function addItem() {
items = [...items, `Item ${items.length + 1}`];
}
function removeItem(index: number) {
items = items.filter((_, i) => i !== index);
}
</script>
<button on:click={() => visible = !visible}>Toggle</button>
{#if visible}
<!-- fade: opacity 0→1 on enter, 1→0 on leave -->
<div transition:fade={{ duration: 300 }}>
Fades in and out
</div>
<!-- fly: moves from y=50px while fading -->
<p in:fly="{{ y: 50, duration: 400 }}" out:fade>
Flies in from below
</p>
{/if}
<!-- flip animates list reorders smoothly -->
<ul>
{#each items as item, i (item)}
<li animate:flip={{ duration: 300 }}>
{item}
<button on:click={() => removeItem(i)}>✕</button>
</li>
{/each}
</ul>
Svelte transitions are just functions that return { delay, duration, easing, css } — meaning you can write fully custom transitions. The css field is a function that receives a t value from 0 to 1 and returns a CSS string, making it trivial to create uniquely branded animations without any external dependencies.
SvelteKit: Full-Stack Development
SvelteKit is to Svelte what Next.js is to React — a full-stack application framework built on top of Svelte. It provides file-based routing, server-side rendering, API routes, and adapters for deploying to Node.js, Cloudflare Workers, Vercel, Netlify, and more.
File-Based Routing
In SvelteKit, every +page.svelte file inside src/routes/ becomes a route. The file system is the router:
| File Path | URL Route | Description |
|---|---|---|
src/routes/+page.svelte |
/ |
Home page |
src/routes/blog/+page.svelte |
/blog |
Blog listing |
src/routes/blog/[slug]/+page.svelte |
/blog/:slug |
Dynamic blog post |
src/routes/+layout.svelte |
All routes | Shared layout (nav, footer) |
src/routes/api/users/+server.ts |
/api/users |
API endpoint |
Load Functions and Server-Side Data
SvelteKit separates data fetching from rendering using +page.server.ts (runs only on the server) and +page.ts (runs on both client and server). Data returned from load() is passed as the data prop to your page component.
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`);
if (!res.ok) {
throw error(404, `Post "${params.slug}" not found`);
}
const post = await res.json();
return {
post,
// This data is passed as `data` prop to +page.svelte
};
};
<script lang="ts">
import type { PageData } from './$types';
// data is automatically typed from the load function
export let data: PageData;
const { post } = data;
</script>
<svelte:head>
<title>{post.title} | My Blog</title>
<meta name="description" content={post.excerpt} />
</svelte:head>
<article>
<h1>{post.title}</h1>
<time datetime={post.publishedAt}>{post.formattedDate}</time>
<div class="content">{@html post.htmlContent}</div>
</article>
API Routes with Form Actions
SvelteKit's form actions provide a clean pattern for handling form submissions without JavaScript — they progressively enhance to use fetch when JS is available, but work with plain HTML forms as a baseline. This is excellent for accessibility and performance.
import type { Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email') as string;
const message = data.get('message') as string;
if (!email || !message) {
return fail(400, { error: 'Email and message are required.' });
}
// Send email, save to DB, etc.
await sendContactEmail({ email, message });
throw redirect(303, '/contact/success');
}
};
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<!-- use:enhance progressively enhances the form with fetch -->
<form method="POST" use:enhance>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<input name="email" type="email" placeholder="your@email.com" required />
<textarea name="message" placeholder="Your message..." required></textarea>
<button type="submit">Send Message</button>
</form>
When to Choose Svelte
Svelte excels in specific scenarios and has some trade-offs to be aware of before committing to it for your next project.
Svelte is a Great Choice When:
- Bundle size matters: Embedded widgets, blog sites, e-commerce product pages — where payload size directly impacts conversion rates
- Animations are central: Data visualizations, interactive dashboards, media-rich sites where you'd otherwise reach for GSAP or Framer Motion
- Developer experience is a priority: Smaller, more focused codebases where the team values simplicity over ecosystem breadth
- SEO and performance are critical: SvelteKit's SSR and static generation rivals Next.js while shipping significantly less JavaScript
- New projects: Greenfield apps where you're not tied to an existing React/Vue component library
Svelte's ecosystem is smaller than React's — fewer third-party component libraries, less StackOverflow coverage, and fewer Svelte-native UI kits. If your project depends on a React-specific library (e.g., React DnD, a specific charting library), factor in migration cost. For large enterprise teams with existing React expertise, the retraining cost may outweigh the performance benefits.
"Svelte is not a framework. It's a language. A way of describing UIs that the compiler turns into efficient JavaScript."
— Rich Harris, Creator of Svelte
Svelte represents a genuine evolution in how we think about frontend frameworks. By shifting the complexity from runtime to compile time, it achieves performance that runtime frameworks simply cannot match — and it does so while being arguably easier to learn and use. If you haven't tried Svelte yet, build a small project with it this week. The elegance of the syntax and the speed of the output will surprise you.