Getting started
Introduction
Svelte is a compiler that generates minimal JavaScript code for reactive UIs. Svelte 5 introduces runes for explicit reactivity.
Installation (Svelte 5)
# Create new SvelteKit project
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
Installation (Standalone Svelte)
# Vite + Svelte
npm create vite@latest my-app -- --template svelte
cd my-app
npm install
npm run dev
First Component
<script>
let count = $state(0);
function increment() {
count++;
}
</script>
<button onclick={increment}>
Clicks: {count}
</button>
<style>
button {
font-size: 1.4rem;
}
</style>
Svelte 5 Runes (Reactivity)
$state (Reactive State)
<script>
// Basic state
let count = $state(0);
// Object state (deep reactivity)
let user = $state({
name: 'Alice',
age: 30
});
// Array state
let items = $state([1, 2, 3]);
</script>
<button onclick={() => count++}>
{count}
</button>
<button onclick={() => user.age++}>
{user.name} is {user.age}
</button>
Replaces let for reactive variables.
$derived (Computed Values)
<script>
let count = $state(0);
// Simple derived
let doubled = $derived(count * 2);
// Complex derived
let message = $derived(() => {
if (count > 10) return 'High';
if (count > 5) return 'Medium';
return 'Low';
});
</script>
<p>{count} × 2 = {doubled}</p>
<p>Status: {message}</p>
Replaces $: reactive statements.
$effect (Side Effects)
<script>
let count = $state(0);
// Run when count changes
$effect(() => {
console.log(`Count is ${count}`);
document.title = `Count: ${count}`;
});
// Effect with cleanup
$effect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(timer);
});
</script>
Replaces $: for side effects.
$props (Component Props)
<script>
// Destructure props
let { name, age = 18 } = $props();
// With types (TypeScript)
let {
count = 0,
onIncrement
}: {
count?: number;
onIncrement?: () => void;
} = $props();
</script>
<p>{name} is {age} years old</p>
<button onclick={onIncrement}>
Count: {count}
</button>
Replaces export let for props.
$bindable (Two-Way Binding)
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let value = $state('');
</script>
<Child bind:value />
<p>Parent: {value}</p>
<!-- Child.svelte -->
<script>
let { value = $bindable('') } = $props();
</script>
<input bind:value />
Allows parent to bind to child props.
Component Basics
Component Structure
<script>
// Logic
let name = $state('world');
function greet() {
alert(`Hello ${name}!`);
}
</script>
<!-- Markup -->
<h1>Hello {name}!</h1>
<button onclick={greet}>Greet</button>
<style>
/* Scoped styles */
h1 {
color: purple;
}
</style>
Three sections: script, markup, style.
Script Context
<script context="module">
// Shared between all instances
let counter = 0;
export function sharedUtil() {
return 'shared';
}
</script>
<script>
// Per-instance
let id = counter++;
</script>
<p>Instance {id}</p>
TypeScript Support
<script lang="ts">
interface User {
name: string;
age: number;
}
let user = $state<User>({
name: 'Alice',
age: 30
});
let { config }: {
config: AppConfig
} = $props();
</script>
Props & Bindings
Passing Props
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let count = $state(0);
</script>
<Child
name="Alice"
{count}
active={true}
/>
<!-- Child.svelte -->
<script>
let {
name,
count,
active
} = $props();
</script>
Shorthand: {count} = count={count}.
Input Bindings
<script>
let text = $state('');
let checked = $state(false);
let selected = $state('');
let number = $state(0);
</script>
<input bind:value={text} />
<input type="checkbox" bind:checked />
<select bind:value={selected}>
<option>A</option>
<option>B</option>
</select>
<input type="number" bind:value={number} />
Component Bindings
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let value = $state('');
</script>
<Child bind:value />
<p>Bound: {value}</p>
<!-- Child.svelte -->
<script>
let { value = $bindable('') } = $props();
</script>
<input bind:value />
Element Bindings
<script>
let element = $state();
let scrollY = $state(0);
let width = $state(0);
$effect(() => {
if (element) {
console.log(element);
}
});
</script>
<div bind:this={element}>
<h1>Element</h1>
</div>
<svelte:window bind:scrollY />
<div bind:clientWidth={width}>
Width: {width}
</div>
Logic Blocks
{#if} Conditional
<script>
let count = $state(0);
</script>
{#if count > 10}
<p>Count is high</p>
{:else if count > 5}
<p>Count is medium</p>
{:else}
<p>Count is low</p>
{/if}
{#each} Loops
<script>
let items = $state([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
]);
</script>
{#each items as item (item.id)}
<p>{item.name}</p>
{/each}
<!-- With index -->
{#each items as item, i (item.id)}
<p>{i}: {item.name}</p>
{/each}
<!-- With else -->
{#each items as item (item.id)}
<p>{item.name}</p>
{:else}
<p>No items</p>
{/each}
Always use key for dynamic lists.
{#await} Promises
<script>
let promise = $state(fetchData());
async function fetchData() {
const res = await fetch('/api/data');
return await res.json();
}
</script>
{#await promise}
<p>Loading...</p>
{:then data}
<p>Data: {data.value}</p>
{:catch error}
<p>Error: {error.message}</p>
{/await}
<!-- Short form -->
{#await promise then data}
<p>{data.value}</p>
{/await}
{#key} Reset
<script>
let value = $state(0);
</script>
{#key value}
<Component />
{/key}
Destroys and recreates on key change.
Events
DOM Events
<script>
let count = $state(0);
function handleClick(event) {
console.log(event);
count++;
}
</script>
<button onclick={handleClick}>
Click me
</button>
<!-- Inline -->
<button onclick={() => count++}>
{count}
</button>
<!-- With modifiers -->
<button onclick|preventDefault={handleClick}>
Submit
</button>
Svelte 5 uses lowercase onclick, not on:click.
Event Modifiers
<button onclick|preventDefault={handle}>
Prevent default
</button>
<button onclick|stopPropagation={handle}>
Stop propagation
</button>
<button onclick|once={handle}>
Fire once
</button>
<button onclick|capture={handle}>
Capture phase
</button>
<!-- Chain modifiers -->
<button onclick|preventDefault|stopPropagation={handle}>
Both
</button>
Custom Events (Svelte 4)
<!-- Child.svelte (Svelte 4) -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function notify() {
dispatch('message', {
text: 'Hello'
});
}
</script>
<button on:click={notify}>Send</button>
<!-- Parent.svelte -->
<Child on:message={handleMessage} />
Callbacks (Svelte 5)
<!-- Child.svelte -->
<script>
let { onMessage } = $props();
function notify() {
onMessage?.({ text: 'Hello' });
}
</script>
<button onclick={notify}>Send</button>
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
function handleMessage(detail) {
console.log(detail.text);
}
</script>
<Child onMessage={handleMessage} />
Svelte 5 prefers callbacks over events.
Stores
Writable Store
// stores.ts
import { writable } from "svelte/store";
export const count = writable(0);
export const user = writable({
name: "Alice",
age: 30,
});
<!-- Component.svelte -->
<script>
import { count } from './stores';
// Auto-subscribe with $
$count++;
// Manual subscribe
const unsubscribe = count.subscribe(value => {
console.log(value);
});
</script>
<p>Count: {$count}</p>
<button onclick={() => $count++}>
Increment
</button>
$ prefix auto-subscribes/unsubscribes.
Readable Store
import { readable } from "svelte/store";
export const time = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return () => clearInterval(interval);
});
<script>
import { time } from './stores';
</script>
<p>Time: {$time.toLocaleTimeString()}</p>
Derived Store
import { derived } from "svelte/store";
import { count } from "./stores";
export const doubled = derived(count, ($count) => $count * 2);
// Multiple stores
export const sum = derived([a, b], ([$a, $b]) => $a + $b);
// Async derived
export const users = derived(
userIds,
($userIds, set) => {
fetchUsers($userIds).then(set);
},
[], // Initial value
);
Custom Store
import { writable } from "svelte/store";
function createCounter() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update((n) => n + 1),
decrement: () => update((n) => n - 1),
reset: () => set(0),
};
}
export const counter = createCounter();
<script>
import { counter } from './stores';
</script>
<p>{$counter}</p>
<button onclick={counter.increment}>+</button>
<button onclick={counter.decrement}>-</button>
<button onclick={counter.reset}>Reset</button>
Lifecycle
onMount
<script>
import { onMount } from 'svelte';
let data = $state();
onMount(async () => {
const res = await fetch('/api/data');
data = await res.json();
// Cleanup
return () => {
console.log('unmounting');
};
});
</script>
{#if data}
<p>{data.value}</p>
{/if}
Runs after component mounted to DOM.
onDestroy
<script>
import { onDestroy } from 'svelte';
const interval = setInterval(() => {
console.log('tick');
}, 1000);
onDestroy(() => {
clearInterval(interval);
});
</script>
beforeUpdate & afterUpdate
<script>
import { beforeUpdate, afterUpdate } from 'svelte';
beforeUpdate(() => {
console.log('before DOM updates');
});
afterUpdate(() => {
console.log('after DOM updates');
});
</script>
tick
<script>
import { tick } from 'svelte';
let count = $state(0);
async function increment() {
count++;
await tick(); // Wait for DOM update
console.log('DOM updated');
}
</script>
Awaits next DOM update.
SvelteKit Basics
File-Based Routing
src/routes/
├── +page.svelte # /
├── about/
│ └── +page.svelte # /about
├── blog/
│ ├── +page.svelte # /blog
│ └── [slug]/
│ └── +page.svelte # /blog/:slug
└── api/
└── +server.ts # /api endpoint
+page.svelte
<!-- src/routes/+page.svelte -->
<script>
let { data } = $props();
</script>
<h1>{data.title}</h1>
<p>{data.content}</p>
+page.ts (Load Function)
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`);
const post = await res.json();
return {
post,
};
};
+page.server.ts (Server Load)
// src/routes/blog/+page.server.ts
import type { PageServerLoad } from "./$types";
import { db } from "$lib/db";
export const load: PageServerLoad = async () => {
const posts = await db.posts.findMany();
return {
posts,
};
};
Runs only on server, can access DB.
+layout.svelte
<!-- src/routes/+layout.svelte -->
<script>
import Header from '$lib/Header.svelte';
import Footer from '$lib/Footer.svelte';
let { data, children } = $props();
</script>
<Header user={data.user} />
<main>
{@render children()}
</main>
<Footer />
+layout.ts
// src/routes/+layout.ts
import type { LayoutLoad } from "./$types";
export const load: LayoutLoad = async ({ fetch }) => {
const res = await fetch("/api/user");
const user = await res.json();
return {
user,
};
};
Data available to all child routes.
+server.ts (API Routes)
// src/routes/api/posts/+server.ts
import type { RequestHandler } from "./$types";
import { json } from "@sveltejs/kit";
export const GET: RequestHandler = async () => {
const posts = await db.posts.findMany();
return json(posts);
};
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json();
const post = await db.posts.create({ data });
return json(post, { status: 201 });
};
SvelteKit Navigation
Links
<script>
import { page } from '$app/stores';
</script>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a
href="/blog"
class:active={$page.url.pathname === '/blog'}
>
Blog
</a>
</nav>
Programmatic Navigation
<script>
import { goto } from '$app/navigation';
function navigate() {
goto('/about');
}
function goBack() {
goto(-1); // History back
}
</script>
<button onclick={navigate}>Go to About</button>
<button onclick={goBack}>Back</button>
Prefetching
<a href="/slow-page" data-sveltekit-preload-data>
Preload on hover
</a>
<a href="/page" data-sveltekit-preload-data="tap">
Preload on tap
</a>
<a href="/page" data-sveltekit-reload>
Full page reload
</a>
Invalidation
<script>
import { invalidate, invalidateAll } from '$app/navigation';
async function refresh() {
// Re-run load functions that depend on URL
await invalidate('/api/posts');
// Re-run all load functions
await invalidateAll();
}
</script>
Transitions & Animations
Built-in Transitions
<script>
import { fade, fly, slide, scale } from 'svelte/transition';
let visible = $state(true);
</script>
{#if visible}
<div transition:fade>
Fades in and out
</div>
<div transition:fly={{ y: 200, duration: 500 }}>
Flies in from bottom
</div>
<div transition:slide>
Slides in and out
</div>
<div transition:scale={{ start: 0.5 }}>
Scales in and out
</div>
{/if}
In/Out Transitions
<script>
import { fade, fly } from 'svelte/transition';
</script>
{#if visible}
<div
in:fly={{ y: 200 }}
out:fade
>
Different transitions
</div>
{/if}
Custom Transitions
<script>
function typewriter(node, { speed = 1 }) {
const text = node.textContent;
const duration = text.length / (speed * 0.01);
return {
duration,
tick: t => {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
</script>
<p transition:typewriter={{ speed: 1 }}>
This text will type out
</p>
Animate Directive
<script>
import { flip } from 'svelte/animate';
import { fade } from 'svelte/transition';
let items = $state([1, 2, 3, 4, 5]);
function shuffle() {
items = items.sort(() => Math.random() - 0.5);
}
</script>
<button onclick={shuffle}>Shuffle</button>
{#each items as item (item)}
<div animate:flip={{ duration: 300 }}>
{item}
</div>
{/each}
FLIP animation for list reordering.
Tweened
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const progress = tweened(0, {
duration: 400,
easing: cubicOut
});
</script>
<progress value={$progress}></progress>
<button onclick={() => progress.set(0)}>0%</button>
<button onclick={() => progress.set(0.5)}>50%</button>
<button onclick={() => progress.set(1)}>100%</button>
Spring
<script>
import { spring } from 'svelte/motion';
const coords = spring({ x: 0, y: 0 }, {
stiffness: 0.1,
damping: 0.25
});
function handleMousemove(event) {
coords.set({ x: event.clientX, y: event.clientY });
}
</script>
<svelte:window onmousemove={handleMousemove} />
<div style="
position: absolute;
left: {$coords.x}px;
top: {$coords.y}px;
">
🎯
</div>
Actions
Basic Action
<script>
function tooltip(node, text) {
const tooltip = document.createElement('div');
tooltip.textContent = text;
function mouseenter() {
document.body.appendChild(tooltip);
}
function mouseleave() {
tooltip.remove();
}
node.addEventListener('mouseenter', mouseenter);
node.addEventListener('mouseleave', mouseleave);
return {
destroy() {
node.removeEventListener('mouseenter', mouseenter);
node.removeEventListener('mouseleave', mouseleave);
}
};
}
</script>
<button use:tooltip="Tooltip text">
Hover me
</button>
Action with Parameters
<script>
function longpress(node, duration = 500) {
let timer;
function handleMousedown() {
timer = setTimeout(() => {
node.dispatchEvent(new CustomEvent('longpress'));
}, duration);
}
function handleMouseup() {
clearTimeout(timer);
}
node.addEventListener('mousedown', handleMousedown);
node.addEventListener('mouseup', handleMouseup);
return {
update(newDuration) {
duration = newDuration;
},
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('mouseup', handleMouseup);
}
};
}
</script>
<button
use:longpress={1000}
onlongpress={() => console.log('long pressed')}
>
Press and hold
</button>
Reusable Actions
// actions.ts
export function clickOutside(node: HTMLElement) {
function handleClick(event: MouseEvent) {
if (!node.contains(event.target as Node)) {
node.dispatchEvent(new CustomEvent("outclick"));
}
}
document.addEventListener("click", handleClick, true);
return {
destroy() {
document.removeEventListener("click", handleClick, true);
},
};
}
<script>
import { clickOutside } from './actions';
let open = $state(false);
</script>
{#if open}
<div
use:clickOutside
onoutclick={() => open = false}
>
Modal content
</div>
{/if}
Slots & Snippets
Basic Slot (Svelte 4)
<!-- Card.svelte -->
<div class="card">
<slot />
</div>
<!-- Usage -->
<Card>
<h1>Title</h1>
<p>Content</p>
</Card>
Named Slots (Svelte 4)
<!-- Card.svelte -->
<div class="card">
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
<!-- Usage -->
<Card>
<h1 slot="header">Title</h1>
<p>Main content</p>
<button slot="footer">Close</button>
</Card>
Snippets (Svelte 5)
<script>
let { header, footer } = $props();
</script>
<div class="card">
<header>
{@render header()}
</header>
<main>
{@render children()}
</main>
<footer>
{@render footer()}
</footer>
</div>
<!-- Usage -->
<Card>
{#snippet header()}
<h1>Title</h1>
{/snippet}
<p>Main content</p>
{#snippet footer()}
<button>Close</button>
{/snippet}
</Card>
Svelte 5 replaces slots with snippets.
Snippet Props
<!-- List.svelte -->
<script>
let { items, row } = $props();
</script>
<ul>
{#each items as item (item.id)}
<li>{@render row(item)}</li>
{/each}
</ul>
<!-- Usage -->
<script>
let items = $state([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]);
</script>
<List {items}>
{#snippet row(item)}
<strong>{item.name}</strong>
{/snippet}
</List>
Compiler Benefits
Why Svelte is Fast
Svelte compiles components to highly optimized vanilla JavaScript at build time, eliminating the need for a virtual DOM.
Key advantages:
- No runtime overhead: No virtual DOM diffing
- Smaller bundle size: Only what you use
- Surgical updates: Direct DOM manipulation
- True reactivity: Compile-time analysis
Bundle Size Comparison
| Framework | Hello World | TodoMVC |
|---|---|---|
| Svelte | ~2KB | ~7KB |
| Vue | ~16KB | ~30KB |
| React | ~40KB | ~50KB |
Performance Features
- Automatic code splitting: Per-route in SvelteKit
- CSS scoping: Unused CSS removed
- Tree shaking: Dead code elimination
- Ahead-of-time compilation: No runtime compilation
Developer Experience
- Less boilerplate: No hooks, no HOCs
- Intuitive reactivity: Just assign values
- Built-in animations: No extra libraries
- TypeScript support: First-class support
Common Patterns
Form Handling
<script>
let formData = $state({
name: '',
email: '',
message: ''
});
let errors = $state({});
function validate() {
errors = {};
if (!formData.name) {
errors.name = 'Name required';
}
if (!formData.email.includes('@')) {
errors.email = 'Invalid email';
}
return Object.keys(errors).length === 0;
}
async function handleSubmit(event) {
event.preventDefault();
if (validate()) {
const res = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData)
});
// Handle response
}
}
</script>
<form onsubmit={handleSubmit}>
<input
bind:value={formData.name}
placeholder="Name"
/>
{#if errors.name}
<p class="error">{errors.name}</p>
{/if}
<input
type="email"
bind:value={formData.email}
placeholder="Email"
/>
{#if errors.email}
<p class="error">{errors.email}</p>
{/if}
<textarea
bind:value={formData.message}
placeholder="Message"
/>
<button type="submit">Send</button>
</form>
Modal with Portal
<!-- Modal.svelte -->
<script>
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
let { open = false, onClose } = $props();
let portal;
onMount(() => {
portal = document.getElementById('portal');
});
</script>
{#if open && portal}
<div class="backdrop" transition:fade onclick={onClose}>
<div
class="modal"
onclick|stopPropagation
>
{@render children()}
</div>
</div>
{/if}
Infinite Scroll
<script>
import { onMount } from 'svelte';
let items = $state([]);
let page = $state(1);
let loading = $state(false);
let sentinel;
async function loadMore() {
if (loading) return;
loading = true;
const res = await fetch(`/api/items?page=${page}`);
const newItems = await res.json();
items = [...items, ...newItems];
page++;
loading = false;
}
onMount(() => {
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
loadMore();
}
});
observer.observe(sentinel);
return () => observer.disconnect();
});
</script>
<ul>
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
</ul>
<div bind:this={sentinel}></div>
{#if loading}
<p>Loading...</p>
{/if}
Context API
<!-- Parent.svelte -->
<script>
import { setContext } from 'svelte';
import Child from './Child.svelte';
let theme = $state('dark');
setContext('theme', {
getTheme: () => theme,
setTheme: (value) => theme = value
});
</script>
<Child />
<!-- Child.svelte -->
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
let currentTheme = $derived(theme.getTheme());
</script>
<button onclick={() => theme.setTheme('light')}>
Current: {currentTheme}
</button>
Gotchas & Tips
Reactivity Pitfalls
<script>
// ❌ Won't trigger reactivity (Svelte 4)
let arr = [];
arr.push(1); // Mutation doesn't trigger update
// ✅ Reassign to trigger
arr = [...arr, 1];
// ✅ Or use $state (Svelte 5)
let arr = $state([]);
arr.push(1); // Works with $state!
</script>
Component Props
<script>
// ❌ Don't mutate props directly
let { count } = $props();
count++; // Antipattern
// ✅ Use callback or $bindable
let { count = $bindable(0) } = $props();
count++; // OK with $bindable
</script>
Async/Await in {#each}
<!-- ❌ Can't await in each -->
{#each promises as promise}
{#await promise then data}
<p>{data}</p>
{/await}
{/each}
<!-- ✅ Await first, then iterate -->
{#await Promise.all(promises) then results}
{#each results as data}
<p>{data}</p>
{/each}
{/await}
Key Blocks for Resets
<script>
let id = $state(1);
</script>
<!-- Component persists when id changes -->
<Component {id} />
<!-- Component recreated when id changes -->
{#key id}
<Component />
{/key}
Store Subscriptions
<script>
import { count } from './stores';
// ❌ Don't forget to unsubscribe
count.subscribe(value => {
console.log(value);
}); // Memory leak!
// ✅ Use $ or manually unsubscribe
const unsubscribe = count.subscribe(value => {
console.log(value);
});
onDestroy(unsubscribe);
// ✅ Or just use $
$: console.log($count);
</script>
Also see
- Svelte Official Documentation - Comprehensive guide and API reference
- SvelteKit Documentation - Full-stack framework built on Svelte
- Svelte 5 Runes Documentation - New reactivity system
- Svelte Tutorial - Interactive step-by-step tutorial
- Svelte Examples - Official example components
- Svelte REPL - Online playground for testing Svelte code
- Awesome Svelte - Curated list of Svelte resources
- SvelteKit Crash Course - Beginner-friendly guide