The compiler that disappears. Write components as .svelte files — HTML, CSS, and JavaScript that compiles to surgical, framework-free DOM updates. No virtual DOM. No runtime overhead. Just the code your app actually needs.
The essential patterns at a glance. Pin this section and reach for it daily.
Scaffold a SvelteKit app with Vite
npx sv create my-app
Start development with hot reload
npm run dev -- --open
Compile for production
npm run build
Preview the production build locally
npm run preview
Add integrations via the CLI
npx sv add tailwindcss
Run svelte-check for diagnostics
npx svelte-check
<!-- Counter.svelte -->
<script>
let count = $state(0); // reactive state (Svelte 5)
let doubled = $derived(count * 2); // derived value
function increment() {
count++; // just mutate — it's reactive
}
</script>
<button onclick={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>Doubled: {doubled}</p>
<style>
button { background: #ff3e00; color: white; }
</style>
| Rune | Purpose | Example |
|---|---|---|
$state |
Declare reactive state | let count = $state(0) |
$state.raw |
Non-deeply-reactive state | let items = $state.raw([]) |
$derived |
Computed from other state | let doubled = $derived(count * 2) |
$derived.by |
Complex derived (function body) | let total = $derived.by(() => { ... }) |
$effect |
Side effects when deps change | $effect(() => { console.log(count) }) |
$effect.pre |
Run before DOM update | $effect.pre(() => { ... }) |
$props |
Declare component props | let { name, age } = $props() |
$bindable |
Allow two-way binding on a prop | let { value = $bindable() } = $props() |
$inspect |
Debug reactive values | $inspect(count) |
$host |
Access custom element host | $host().dispatchEvent(...) |
Svelte is a compiler, not a runtime. It shifts the work from the browser to the build step, producing tight, dependency-free JavaScript that surgically updates the DOM.
React, Vue, and Angular ship a runtime library to the browser. That runtime interprets your components, diffs a virtual DOM, and patches changes. Svelte takes a fundamentally different approach: your .svelte files are compiled away at build time into plain JavaScript that directly manipulates the DOM.
svelte.min.js in your production bundle — just the compiled output of your components.
// Ship framework + your code to browser
import { createApp } from 'framework';
// Runtime diffs virtual DOM on every
// state change, then patches real DOM
createApp(App).mount('#app');
// No framework in bundle — just your code
// Compiler generates targeted DOM ops:
element.textContent = count;
// Only updates exactly what changed
// No diffing. No virtual DOM.
Svelte components require dramatically less boilerplate than React or Vue. Reactivity is built into the language, not layered on via hooks or APIs.
Virtual DOM diffing is overhead. Svelte compiles your declarative code into imperative DOM operations that run at native speed.
Reactivity is baked into the language via runes ($state, $derived). Assignments trigger updates — no setState calls needed.
CSS in <style> blocks is automatically scoped to the component. No CSS-in-JS libraries, no naming conventions, no leaking styles.
Simple things are simple. A basic component is just HTML. Add reactivity, props, and logic only when you need them.
Because there is no runtime overhead, Svelte apps tend to be significantly smaller than equivalent React/Vue apps, especially for smaller projects.
Svelte 5 introduced runes, a major evolution of the reactivity system. The old $: reactive declarations and export let props still work in legacy mode, but runes are the recommended approach for all new code.
| Concept | Svelte 4 (Legacy) | Svelte 5 (Runes) |
|---|---|---|
| Reactive state | let count = 0 (top-level) |
let count = $state(0) |
| Derived values | $: doubled = count * 2 |
let doubled = $derived(count * 2) |
| Side effects | $: { console.log(count) } |
$effect(() => { console.log(count) }) |
| Props | export let name |
let { name } = $props() |
| Events | createEventDispatcher() |
Callback props: let { onclick } = $props() |
A .svelte file contains three optional sections: <script> for logic, markup for structure, and <style> for scoped CSS. Order does not matter, but convention places script first.
<!-- UserCard.svelte -->
<script>
// 1. LOGIC — imports, state, functions
let { name, role, avatar } = $props();
let expanded = $state(false);
</script>
<!-- 2. MARKUP — template with reactive expressions -->
<div class="card" class:expanded>
<img src={avatar} alt={name} />
<h2>{name}</h2>
<span>{role}</span>
<button onclick={() => expanded = !expanded}>
{expanded ? 'Less' : 'More'}
</button>
</div>
<!-- 3. STYLE — scoped CSS (won't leak) -->
<style>
.card { padding: 1rem; border: 1px solid #ddd; }
.expanded { max-height: none; }
h2 { color: #ff3e00; } /* only affects THIS h2 */
</style>
A <script module> block runs once per module, not per component instance. Use it for shared logic, module-level constants, or named exports.
<script module>
// Runs once — shared across all instances
export const THEMES = ['light', 'dark', 'auto'];
let instanceCount = 0;
</script>
<script>
// Runs per instance
instanceCount++;
console.log(`Instance #${instanceCount}`);
</script>
Anything inside curly braces {...} in the markup is a JavaScript expression.
<!-- Text interpolation -->
<p>Hello, {name}!</p>
<!-- Attribute binding -->
<img src={url} alt={description} />
<!-- Shorthand when name === value -->
<img {src} {alt} />
<!-- Spread props -->
<Widget {...config} />
<!-- HTML string (use carefully) -->
{@html rawHtmlString}
<!-- Debug output -->
{@debug name, count}
<!-- Render raw content -->
{@const area = width * height}
<p>Area: {area}</p>
<style>
/* Scoped to this component */
p { color: #333; }
/* Escape to global scope */
:global(body) { margin: 0; }
/* Global within scoped context */
.wrapper :global(p) { font-size: 14px; }
/* Conditional class using class: directive */
</style>
<!-- Class directives -->
<div class:active={isActive}>...</div>
<div class:active>...</div> <!-- shorthand: uses `active` variable -->
<div class="{base} {size}">...</div> <!-- string interpolation -->
Svelte 5 runes are compiler instructions that declare reactive state. They look like function calls, but the compiler transforms them into efficient reactive primitives at build time.
Declare reactive state with $state. When the value changes, anything that reads it updates automatically. Objects and arrays are deeply reactive by default.
<script>
// Primitive state
let count = $state(0);
let name = $state('world');
// Object state — deeply reactive
let user = $state({
name: 'Alice',
scores: [100, 95, 88]
});
// Mutating nested properties triggers updates
user.name = 'Bob'; // reactive!
user.scores.push(92); // reactive!
// Non-deeply-reactive (reference equality only)
let data = $state.raw([1, 2, 3]);
data = [...data, 4]; // must replace, not mutate
</script>
<p>{count}</p> <!-- re-renders when count changes -->
<p>{user.name}</p> <!-- re-renders when name changes -->
$state.raw for large datasets, immutable data from APIs, or when you want to opt out of deep reactivity for performance. With $state.raw, only reassignment (not mutation) triggers updates.
Derived values recompute automatically when their dependencies change. Use $derived for simple expressions and $derived.by for complex logic.
<script>
let items = $state([
{ name: 'Apple', price: 1.20, qty: 3 },
{ name: 'Bread', price: 2.50, qty: 1 },
]);
// Simple expression
let count = $derived(items.length);
// Complex computation with $derived.by
let total = $derived.by(() => {
return items.reduce((sum, item) =>
sum + item.price * item.qty, 0
);
});
// Derived from derived — it chains
let totalWithTax = $derived(total * 1.08);
</script>
<p>{count} items, total: ${total.toFixed(2)}</p>
Effects run after the DOM updates whenever their dependencies change. They automatically track which reactive values they read.
<script>
let query = $state('');
let results = $state([]);
// Auto-tracks `query` — re-runs when it changes
$effect(() => {
if (query.length < 3) return;
const controller = new AbortController();
fetch(`/api/search?q=${query}`, {
signal: controller.signal
})
.then(r => r.json())
.then(data => results = data);
// Cleanup function — runs before next effect or on destroy
return () => controller.abort();
});
// $effect.pre — runs BEFORE DOM update
$effect.pre(() => {
console.log('About to update DOM, count is:', count);
});
</script>
$effect reads, or you will create an infinite loop. If you need to update state in an effect, make sure the write does not re-trigger the effect.
In Svelte 4 and earlier, reactivity was triggered by top-level let assignments and $: reactive declarations. This still works in compatibility mode.
<script>
// Svelte 4 — reactive by assignment
let count = 0;
// Reactive declaration — re-runs when `count` changes
$: doubled = count * 2;
// Reactive statement block
$: {
console.log('count is', count);
console.log('doubled is', doubled);
}
// Reactive if
$: if (count >= 10) {
alert('High count!');
}
</script>
Components communicate downward through props and upward through callback props (Svelte 5) or dispatched events (Svelte 4).
<!-- Greeting.svelte -->
<script>
// Destructure props with defaults
let {
name,
greeting = 'Hello', // default value
children, // snippet (replaces slots)
...rest // remaining props
} = $props();
</script>
<div {...rest}>
<h1>{greeting}, {name}!</h1>
{@render children?.()}
</div>
<!-- Usage -->
<Greeting name="Alice" class="fancy">
<p>Welcome back!</p>
</Greeting>
In Svelte 5, events are just callback props. Pass functions down, call them from children.
<!-- SearchBox.svelte -->
<script>
let { onsearch, onreset } = $props();
let query = $state('');
</script>
<input bind:value={query} />
<button onclick={() => onsearch(query)}>Search</button>
<button onclick={onreset}>Reset</button>
<!-- Parent.svelte -->
<SearchBox
onsearch={(q) => console.log('Searching:', q)}
onreset={() => results = []}
/>
<!-- Toggle.svelte -->
<script>
let { checked = $bindable(false) } = $props();
</script>
<label>
<input type="checkbox" bind:checked />
{checked ? 'On' : 'Off'}
</label>
<!-- Parent can bind: -->
<Toggle bind:checked={darkMode} />
Svelte 5 replaces slots with snippets — composable, renderable blocks of markup.
<!-- Card.svelte -->
<script>
let { header, children, footer } = $props();
</script>
<div class="card">
{#if header}
<header>{@render header()}</header>
{/if}
<main>{@render children()}</main>
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</div>
<!-- Usage with named snippets -->
<Card>
{#snippet header()}
<h2>My Title</h2>
{/snippet}
<p>Default content goes here (children).</p>
{#snippet footer()}
<small>Footer text</small>
{/snippet}
</Card>
<!-- Card.svelte (Svelte 4) -->
<div class="card">
<slot name="header">Default header</slot>
<slot>Default content</slot>
<slot name="footer" />
</div>
<!-- Usage -->
<Card>
<h2 slot="header">Title</h2>
<p>Body content</p>
</Card>
Svelte uses block-level template syntax for conditionals, loops, and async resolution — no JSX, no v-directives.
{#if loggedIn}
<Dashboard user={currentUser} />
{:else if registering}
<RegisterForm />
{:else}
<LoginForm />
{/if}
<!-- Basic list -->
{#each items as item}
<p>{item.name}</p>
{/each}
<!-- With index -->
{#each items as item, i}
<p>{i + 1}. {item.name}</p>
{/each}
<!-- KEYED — critical for animations & state -->
{#each items as item (item.id)}
<TodoItem {item} />
{/each}
<!-- Destructured -->
{#each items as { id, name, done } (id)}
<li class:done>{name}</li>
{/each}
<!-- Empty state -->
{#each results as result}
<Result {result} />
{:else}
<p>No results found.</p>
{/each}
(item.id), Svelte uses index-based diffing, which can cause bugs with animations, component state, and transitions. Always provide a unique key for dynamic lists.
<!-- Full three-state pattern -->
{#await fetchUser(id)}
<Spinner />
{:then user}
<h1>{user.name}</h1>
{:catch error}
<p class="error">{error.message}</p>
{/await}
<!-- Skip loading state (show nothing until resolved) -->
{#await fetchUser(id) then user}
<h1>{user.name}</h1>
{/await}
<!-- Skip loading, handle error -->
{#await fetchData() then data}
<Chart {data} />
{:catch err}
<Error message={err.message} />
{/await}
<!-- Destroys and recreates when `userId` changes -->
{#key userId}
<UserProfile id={userId} />
{/key}
<!-- Useful for re-triggering intro transitions -->
{#key selectedTab}
<div transition:fade>
{tabContent}
</div>
{/key}
Stores are reactive containers for state that lives outside components. They are especially useful for cross-component state sharing. In Svelte 5, you can also use $state in plain .svelte.js modules as an alternative.
// stores.js
import { writable } from 'svelte/store';
export const count = writable(0);
export const user = writable(null);
// API:
count.set(10); // replace value
count.update(n => n + 1); // transform value
const unsubscribe = count.subscribe( // listen
value => console.log(value)
);
<!-- Component.svelte -->
<script>
import { count } from './stores.js';
// $ prefix auto-subscribes (and unsubscribes on destroy)
</script>
<p>Count: {$count}</p>
<button onclick={() => $count++}>+1</button>
import { readable } from 'svelte/store';
export const time = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
// Return cleanup function
return () => clearInterval(interval);
});
export const mouse = readable({ x: 0, y: 0 }, (set) => {
const handler = (e) => set({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
});
import { derived } from 'svelte/store';
import { items, taxRate } from './stores.js';
// From a single store
export const itemCount = derived(
items,
$items => $items.length
);
// From multiple stores
export const total = derived(
[items, taxRate],
([$items, $taxRate]) => {
const subtotal = $items.reduce((s, i) => s + i.price, 0);
return subtotal * (1 + $taxRate);
}
);
In Svelte 5, you can use runes in .svelte.js (or .svelte.ts) files. This is often simpler than stores for shared state.
// counter.svelte.js
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count++;
}
export function reset() {
count = 0;
}
<!-- Any component can import and use it -->
<script>
import { getCount, increment } from './counter.svelte.js';
</script>
<p>{getCount()}</p>
<button onclick={increment}>+1</button>
Two-way data bindings connect DOM elements to component state. One directive, many applications.
<!-- Text input -->
<input bind:value={name} />
<!-- Numeric (auto-coerces to number) -->
<input type="number" bind:value={age} />
<input type="range" bind:value={volume} min="0" max="100" />
<!-- Checkbox -->
<input type="checkbox" bind:checked={agreed} />
<!-- Checkbox group (bind to array) -->
<input type="checkbox" bind:group={flavors} value="vanilla" />
<input type="checkbox" bind:group={flavors} value="chocolate" />
<!-- Radio group -->
<input type="radio" bind:group={color} value="red" />
<input type="radio" bind:group={color} value="blue" />
<!-- Select -->
<select bind:value={selected}>
{#each options as opt}
<option value={opt.id}>{opt.label}</option>
{/each}
</select>
<!-- Multi-select (bind to array) -->
<select multiple bind:value={selectedItems}>
...
</select>
<!-- Textarea -->
<textarea bind:value={content} />
<!-- Contenteditable -->
<div contenteditable="true" bind:innerHTML={html} />
<!-- Read-only dimension bindings -->
<div bind:clientWidth={w} bind:clientHeight={h}>
<p>{w}px × {h}px</p>
</div>
<!-- Element reference -->
<canvas bind:this={canvasEl} />
<!-- Media bindings -->
<video
bind:currentTime={time}
bind:duration
bind:paused
bind:volume
bind:muted
>
<source src={videoUrl} type="video/mp4" />
</video>
<!-- Scroll position -->
<div bind:scrollY={y}>...</div>
<!-- Bind to a prop marked with $bindable -->
<ColorPicker bind:color={selectedColor} />
<!-- Bind to the component instance itself -->
<MyComponent bind:this={componentRef} />
Svelte has first-class support for transitions, animations, and motion. No third-party animation libraries needed for most use cases.
<script>
import { fade, fly, slide, scale, blur, draw }
from 'svelte/transition';
let visible = $state(true);
</script>
<!-- Both in and out -->
{#if visible}
<div transition:fade>Fades in and out</div>
{/if}
<!-- With parameters -->
{#if visible}
<div transition:fly={{ y: 200, duration: 400 }}>
Flies in from below
</div>
{/if}
<!-- Separate in/out transitions -->
{#if visible}
<div
in:fly={{ y: -50 }}
out:fade={{ duration: 200 }}
>
Flies in, fades out
</div>
{/if}
| Transition | Parameters | Effect |
|---|---|---|
fade |
delay, duration, easing | Opacity 0 ↔ 1 |
fly |
x, y, delay, duration, easing, opacity | Translate + fade |
slide |
delay, duration, easing, axis | Height/width collapse |
scale |
start, delay, duration, easing, opacity | Scale + fade |
blur |
amount, delay, duration, easing, opacity | Blur + fade |
draw |
delay, duration, easing, speed | SVG path drawing |
function typewriter(node, { speed = 1 }) {
const text = node.textContent;
const duration = text.length / (speed * 0.015);
return {
duration,
tick(t) {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
<!-- Usage -->
{#if visible}
<p transition:typewriter>This types out letter by letter</p>
{/if}
<script>
import { flip } from 'svelte/animate';
import { fade } from 'svelte/transition';
</script>
<!-- Items smoothly animate to new positions -->
{#each items as item (item.id)}
<div
animate:flip={{ duration: 300 }}
in:fade
out:fade
>
{item.name}
</div>
{/each}
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const progress = tweened(0, {
duration: 400,
easing: cubicOut
});
</script>
<progress value={$progress} />
<button onclick={() => progress.set(1)}>Go</button>
<script>
import { spring } from 'svelte/motion';
const coords = spring({ x: 50, y: 50 }, {
stiffness: 0.1,
damping: 0.25
});
</script>
<svg onmousemove={(e) => coords.set({x: e.clientX, y: e.clientY})}>
<circle cx={$coords.x} cy={$coords.y} r="10" />
</svg>
SvelteKit is the full-stack framework for Svelte. File-based routing, server-side rendering, API routes, form actions, and adapters for every deployment target.
my-app/
├── src/
│ ├── lib/ // $lib alias — shared code
│ │ ├── components/
│ │ ├── server/ // $lib/server — server-only
│ │ └── utils.js
│ ├── params/ // param matchers
│ ├── routes/ // file-based routing
│ │ ├── +layout.svelte // root layout
│ │ ├── +layout.server.js // layout load (server)
│ │ ├── +page.svelte // home page
│ │ ├── +page.server.js // page load + actions
│ │ ├── +error.svelte // error page
│ │ ├── about/
│ │ │ └── +page.svelte // /about
│ │ └── blog/
│ │ ├── +page.svelte // /blog
│ │ └── [slug]/
│ │ ├── +page.svelte // /blog/:slug
│ │ └── +page.server.js
│ ├── app.html // HTML shell template
│ ├── app.css // global CSS
│ └── hooks.server.js // server hooks
├── static/ // served as-is
├── svelte.config.js
├── vite.config.js
└── package.json
| File | Purpose | Runs On |
|---|---|---|
+page.svelte |
Page component (UI) | Client + Server (SSR) |
+page.js |
Universal load function | Client + Server |
+page.server.js |
Server-only load + form actions | Server only |
+layout.svelte |
Shared layout (wraps child pages) | Client + Server |
+layout.js |
Layout universal load | Client + Server |
+layout.server.js |
Layout server load | Server only |
+error.svelte |
Error boundary page | Client + Server |
+server.js |
API route (GET, POST, etc.) | Server only |
// Dynamic routes
src/routes/blog/[slug]/+page.svelte // /blog/hello-world
src/routes/[category]/[id]/+page.svelte // /tech/42
// Optional params
src/routes/[[lang]]/about/+page.svelte // /about or /en/about
// Rest params (catch-all)
src/routes/docs/[...path]/+page.svelte // /docs/a/b/c
// Param matchers
src/routes/items/[id=integer]/+page.svelte
// src/params/integer.js:
export function match(param) {
return /^\d+$/.test(param);
}
// Route groups (share layout, no URL segment)
src/routes/(auth)/login/+page.svelte
src/routes/(auth)/register/+page.svelte
src/routes/(auth)/+layout.svelte // shared auth layout
// +page.server.js — server-only load
import { db } from '$lib/server/database';
import { error } from '@sveltejs/kit';
export async function load({ params, url, cookies, locals }) {
const post = await db.posts.findUnique({
where: { slug: params.slug }
});
if (!post) {
error(404, 'Post not found');
}
return {
post,
readCount: cookies.get('read_count') ?? 0
};
}
<!-- +page.svelte — receives data from load -->
<script>
let { data } = $props();
</script>
<h1>{data.post.title}</h1>
{@html data.post.content}
// +page.js — universal load (runs client + server)
export async function load({ fetch, params }) {
// SvelteKit's `fetch` deduplicates on server
const res = await fetch(`/api/posts/${params.slug}`);
return { post: await res.json() };
}
// +page.server.js
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
// Default action (POST to this page)
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
if (!email) {
return fail(400, { email, missing: true });
}
await createUser(email);
redirect(303, '/welcome');
},
// Named action (POST with ?/login)
login: async ({ request }) => { ... },
register: async ({ request }) => { ... },
};
<!-- +page.svelte -->
<script>
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<!-- use:enhance progressively enhances the form -->
<form method="POST" use:enhance>
<input name="email" value={form?.email ?? ''} />
{#if form?.missing}
<p class="error">Email is required</p>
{/if}
<button>Submit</button>
</form>
<!-- Named action -->
<form method="POST" action="?/login" use:enhance>
...
</form>
// src/routes/api/posts/+server.js
import { json, error } from '@sveltejs/kit';
export async function GET({ url }) {
const page = url.searchParams.get('page') ?? '1';
const posts = await getPosts(Number(page));
return json(posts);
}
export async function POST({ request, locals }) {
if (!locals.user) {
error(401, 'Unauthorized');
}
const body = await request.json();
const post = await createPost(body);
return json(post, { status: 201 });
}
export async function DELETE({ params }) {
await deletePost(params.id);
return new Response(null, { status: 204 });
}
// Per-page rendering mode (in +page.js or +layout.js)
export const ssr = true; // default — server-side render
export const csr = true; // default — hydrate on client
export const prerender = true; // generate static HTML at build
export const trailingSlash = 'always';
// Fully static site (in root +layout.js)
export const prerender = true; // all pages become static HTML
// svelte.config.js — choose adapter
import adapter from '@sveltejs/adapter-auto';
// Other adapters:
// @sveltejs/adapter-node — Node.js server
// @sveltejs/adapter-static — full SSG
// @sveltejs/adapter-vercel — Vercel serverless
// @sveltejs/adapter-cloudflare — Cloudflare Pages/Workers
// @sveltejs/adapter-netlify — Netlify Functions
export default {
kit: {
adapter: adapter()
}
};
adapter-auto detects your deployment platform (Vercel, Cloudflare, Netlify) and uses the correct adapter automatically. For custom servers, switch to adapter-node.
// src/hooks.server.js
export async function handle({ event, resolve }) {
// Runs for every request
const session = event.cookies.get('session');
if (session) {
event.locals.user = await getUserFromSession(session);
}
const response = await resolve(event, {
// Transform HTML (e.g., inject lang attribute)
transformPageChunk({ html }) {
return html.replace('%lang%', event.locals.lang ?? 'en');
}
});
return response;
}
export function handleError({ error, event, status, message }) {
console.error(error);
return { message: 'Something went wrong' };
}
<script>
import { goto, invalidate, invalidateAll }
from '$app/navigation';
import { page, navigating }
from '$app/state';
// Programmatic navigation
goto('/dashboard');
goto('/login', { replaceState: true });
// Re-run load functions
invalidate('/api/posts'); // by URL
invalidate('app:posts'); // by custom identifier
invalidateAll(); // everything
// Current page state
console.log(page.url.pathname); // '/blog/hello'
console.log(page.params); // { slug: 'hello' }
</script>
<!-- Loading indicator -->
{#if navigating.to}
<LoadingBar />
{/if}
Component lifecycle hooks, the context API for dependency injection, and special elements for accessing window, body, and head.
<script>
import { onMount, onDestroy, beforeUpdate, afterUpdate,
tick, untrack } from 'svelte';
// Runs after first render (client only, not during SSR)
onMount(() => {
const interval = setInterval(fetchData, 5000);
// Return cleanup (like useEffect return in React)
return () => clearInterval(interval);
});
// Runs when component is destroyed
onDestroy(() => {
console.log('Goodbye!');
});
// tick() — wait for pending DOM updates to flush
async function handleInput() {
text = text.toUpperCase();
await tick();
// DOM is now updated — safe to measure/read
}
// untrack() — read reactive value without tracking
$effect(() => {
console.log(count); // tracked
untrack(() => name); // NOT tracked
});
</script>
Context allows ancestor components to share data with all descendants without prop drilling. It is set during component initialization and read by any child.
<!-- ThemeProvider.svelte -->
<script>
import { setContext } from 'svelte';
let { children } = $props();
const theme = $state({
mode: 'dark',
primary: '#ff3e00',
toggle() { this.mode = this.mode === 'dark' ? 'light' : 'dark'; }
});
setContext('theme', theme);
</script>
{@render children()}
<!-- Any descendant -->
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
</script>
<p>Current mode: {theme.mode}</p>
<button onclick={theme.toggle}>Toggle</button>
<!-- Window events and bindings -->
<svelte:window
onkeydown={handleKeydown}
onscroll={handleScroll}
bind:innerWidth={w}
bind:innerHeight={h}
bind:scrollY={y}
/>
<!-- Body class -->
<svelte:body onmouseenter={...} onmouseleave={...} />
<!-- Document head -->
<svelte:head>
<title>{pageTitle}</title>
<meta name="description" content={description} />
</svelte:head>
<!-- Dynamic component -->
<svelte:component this={selectedComponent} {...props} />
<!-- Dynamic element tag -->
<svelte:element this={tag} {...attrs}>
Content
</svelte:element>
<!-- Self-referencing (recursive components) -->
<svelte:self {...childProps} />
<!-- Boundary for error handling -->
<svelte:boundary onerror={(e) => console.error(e)}>
<RiskyComponent />
{#snippet failed(error, reset)}
<p>Error: {error.message}</p>
<button onclick={reset}>Retry</button>
{/snippet}
</svelte:boundary>
TypeScript support, actions, custom use: directives, performance patterns, and the broader Svelte ecosystem.
Svelte has first-class TypeScript support. Use lang="ts" in your script tag.
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
let { user, onselect }: {
user: User;
onselect: (user: User) => void;
} = $props();
let count: number = $state(0);
let items: User[] = $state([]);
</script>
// Typing load functions (+page.server.ts)
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
return {
post: await getPost(params.slug)
};
};
// Auto-generated types from SvelteKit
// .svelte-kit/types/ contains $types for each route
$types for each route, giving you type-safe load functions, actions, and page data without manual typing. Import from ./$types.
Actions are functions that run when an element is mounted. They are Svelte's escape hatch for imperative DOM manipulation.
// actions.js
export function clickOutside(node, callback) {
function handleClick(event) {
if (!node.contains(event.target)) {
callback();
}
}
document.addEventListener('click', handleClick, true);
return {
// Called when the parameter changes
update(newCallback) {
callback = newCallback;
},
// Called when element is removed
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
export function tooltip(node, text) {
const tip = document.createElement('div');
tip.textContent = text;
tip.className = 'tooltip';
node.addEventListener('mouseenter', () => node.appendChild(tip));
node.addEventListener('mouseleave', () => tip.remove());
return {
update(newText) { tip.textContent = newText; },
destroy() { tip.remove(); }
};
}
<!-- Usage -->
<script>
import { clickOutside, tooltip } from './actions.js';
</script>
<div use:clickOutside={() => open = false}>
Dropdown
</div>
<button use:tooltip={'Click to save'}>Save</button>
// .env
PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgres://...
// Server-only (import from $env/static/private)
import { DATABASE_URL } from '$env/static/private';
// Client-safe (must start with PUBLIC_)
import { PUBLIC_API_URL } from '$env/static/public';
// Dynamic (read at runtime, not build time)
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';
Avoid deep reactivity for large arrays/objects from APIs. Use $state.raw and replace the whole reference on update.
Always use (item.id) keys in {#each} blocks. Without keys, Svelte uses index-based diffing that can cause unnecessary re-renders.
Use dynamic import() with {#await} to code-split heavy components. SvelteKit handles route-level splitting automatically.
Prefer $derived over $effect that writes to state. Derived values are synchronous and avoid cascading re-renders.
// svelte.config.js — enable custom element compilation
export default {
compilerOptions: {
customElement: true
}
};
<!-- MyWidget.svelte -->
<svelte:options customElement="my-widget" />
<script>
let { name = 'World' } = $props();
</script>
<p>Hello, {name}!</p>
<!-- Use anywhere as standard HTML -->
<my-widget name="Svelte"></my-widget>
| Category | Library | Purpose |
|---|---|---|
| Framework | @sveltejs/kit |
Full-stack framework (routing, SSR, API) |
| Styling | TailwindCSS, UnoCSS | Utility-first CSS (works natively) |
| UI Libraries | Skeleton, shadcn-svelte, Melt UI | Component libraries & headless primitives |
| Forms | Superforms, Formsnap | Form validation & actions integration |
| Auth | Lucia, Auth.js | Authentication & sessions |
| Database | Drizzle, Prisma | Type-safe ORM / query builder |
| Testing | Vitest, Playwright, @testing-library/svelte | Unit, integration, and E2E |
| State | Built-in stores, TanStack Query | Global state & async data management |
| Animation | Built-in transitions, Motion One | Declarative animations |
| Mobile | Capacitor, Tauri | Native mobile/desktop apps |
<script>
let count = $state(0);
let user = $state({ name: 'Alice' });
// Logs to console whenever values change
$inspect(count); // logs count on every change
$inspect(count, user); // logs both
// Custom handler (e.g., breakpoint)
$inspect(count).with((type, value) => {
if (type === 'update') debugger;
});
// Stripped from production builds automatically
</script>