Svelte is a compiler-based JavaScript framework with no virtual DOM — components compile to vanilla JS. Svelte 5 introduced runes, a new reactivity primitive. This cheat sheet covers everything from basic syntax to SvelteKit routing with copy-ready examples.
Quick reference
The 25 patterns that cover 95% of everyday Svelte development.
| Pattern | Example |
|---|---|
| Reactive state | let count = $state(0) |
| Derived value | let doubled = $derived(count * 2) |
| Side effect | $effect(() => { console.log(count) }) |
| Props declaration | let { name, age = 0 } = $props() |
| Bind variable | <input bind:value={text} /> |
| Conditional | {#if show}<p>hi</p>{/if} |
| Each loop | {#each items as item (item.id)} |
| Await block | {#await promise then data} |
| Event handler | <button onclick={() => count++}> |
| Snippet | {#snippet row(item)}{/snippet} |
| Render snippet | {@render row(item)} |
| Component import | import Card from './Card.svelte' |
| Writable store | const n = writable(0) |
| Store subscribe | $n (auto-subscribe with $) |
| Class binding | class:active={isActive} |
| Style binding | style:color={color} |
| Two-way prop | bind:value={val} on component |
| Lifecycle | onMount(() => { ... }) |
| Transition | transition:fade={{ duration: 300 }} |
| Animation | animate:flip={{ duration: 200 }} |
| Action | use:tooltip={{ text: 'hi' }} |
| SvelteKit page | +page.svelte |
| Load data | export async function load({ fetch }) |
| Form action | export const actions = { default } |
| Route param | $page.params.id |
Components
Basic component
<!-- Greeting.svelte -->
<script lang="ts">
let { name, greeting = 'Hello' }: { name: string; greeting?: string } = $props()
</script>
<p>{greeting}, {name}!</p>
<style>
p { font-weight: bold; }
</style>
<!-- App.svelte -->
<script lang="ts">
import Greeting from './Greeting.svelte'
</script>
<Greeting name="Alice" greeting="Hi" />
Props with $props()
<script lang="ts">
// Svelte 5: destructure $props()
let { title, count = 0, items = [] }: {
title: string
count?: number
items?: string[]
} = $props()
// Rest props (pass through to element)
let { class: className, ...rest } = $props()
</script>
<div class={className} {...rest}>
<h2>{title}</h2>
<p>Count: {count}</p>
</div>
Snippets (Svelte 5 slots replacement)
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte'
let { header, children }: {
header?: Snippet
children: Snippet
} = $props()
</script>
<div class="card">
{#if header}
<div class="card-header">{@render header()}</div>
{/if}
<div class="card-body">{@render children()}</div>
</div>
<!-- Using Card -->
<Card>
{#snippet header()}
<h2>My Card</h2>
{/snippet}
<p>Card content here</p>
</Card>
Snippets with parameters
<!-- Table.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte'
let { rows, rowSnippet }: {
rows: { id: number; name: string }[]
rowSnippet: Snippet<[{ id: number; name: string }]>
} = $props()
</script>
<table>
{#each rows as row (row.id)}
<tr>{@render rowSnippet(row)}</tr>
{/each}
</table>
<Table {rows}>
{#snippet rowSnippet(row)}
<td>{row.id}</td>
<td>{row.name}</td>
{/snippet}
</Table>
Runes (Svelte 5 reactivity)
$state
<script lang="ts">
// Primitive — reactive
let count = $state(0)
// Object — deep reactive (proxy)
let user = $state({ name: 'Alice', age: 30 })
// Array — deep reactive
let items = $state<string[]>([])
function addItem(item: string) {
items.push(item) // mutations tracked automatically
user.name = 'Bob' // nested mutations tracked
}
// $state.raw — not proxied, replaced wholesale
let big = $state.raw(new Float64Array(1_000_000))
</script>
<button onclick={() => count++}>{count}</button>
<p>{user.name} is {user.age}</p>
$derived
<script lang="ts">
let count = $state(0)
// Simple expression
let doubled = $derived(count * 2)
// Complex computation — use $derived.by()
let stats = $derived.by(() => {
const values = [1, 2, 3, count]
return {
sum: values.reduce((a, b) => a + b, 0),
avg: values.reduce((a, b) => a + b, 0) / values.length
}
})
</script>
<p>{count} × 2 = {doubled}</p>
<p>Sum: {stats.sum}, Avg: {stats.avg}</p>
$effect
<script lang="ts">
let count = $state(0)
let element = $state<HTMLElement>()
// Runs after DOM update when dependencies change
$effect(() => {
console.log('count changed:', count)
// Return cleanup function (optional)
return () => console.log('cleanup')
})
// $effect.pre — runs before DOM update
$effect.pre(() => {
console.log('before DOM update')
})
// Don't track dependency inside $effect.root
const cleanup = $effect.root(() => {
$effect(() => { /* runs until cleanup() called */ })
})
</script>
$props and $bindable
<!-- Counter.svelte -->
<script lang="ts">
// $bindable makes a prop two-way bindable
let { count = $bindable(0) } = $props()
</script>
<button onclick={() => count++}>{count}</button>
<!-- Parent -->
<script lang="ts">
let n = $state(0)
</script>
<Counter bind:count={n} />
<p>Parent sees: {n}</p>
Template syntax
Conditionals
{#if user.isAdmin}
<AdminPanel />
{:else if user.isModerator}
<ModPanel />
{:else}
<UserView />
{/if}
Each loops
<!-- Basic loop with key -->
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
<!-- With index -->
{#each items as item, i (item.id)}
<li>{i + 1}. {item.name}</li>
{/each}
<!-- Empty fallback -->
{#each items as item (item.id)}
<li>{item.name}</li>
{:else}
<p>No items found.</p>
{/each}
<!-- Destructuring -->
{#each entries as [key, value] (key)}
<p>{key}: {value}</p>
{/each}
Await blocks
<script lang="ts">
let promise = $state(fetchUser(1))
</script>
{#await promise}
<p>Loading…</p>
{:then user}
<p>Hello {user.name}</p>
{:catch error}
<p>Error: {error.message}</p>
{/await}
<!-- Shorthand: skip loading state -->
{#await promise then user}
<p>Hello {user.name}</p>
{/await}
Key block (force re-render)
<!-- Re-mounts component when id changes -->
{#key userId}
<UserProfile {userId} />
{/key}
Bindings
Form bindings
<script lang="ts">
let name = $state('')
let age = $state(0)
let agreed = $state(false)
let role = $state('viewer')
let tags = $state<string[]>([])
</script>
<!-- Text input -->
<input bind:value={name} type="text" />
<!-- Number input (auto-converts to number) -->
<input bind:value={age} type="number" />
<!-- Checkbox -->
<input bind:checked={agreed} type="checkbox" />
<!-- Radio group -->
<input bind:group={role} type="radio" value="admin" />
<input bind:group={role} type="radio" value="viewer" />
<!-- Multi-select checkbox group -->
<input bind:group={tags} type="checkbox" value="svelte" />
<input bind:group={tags} type="checkbox" value="react" />
<!-- Textarea -->
<textarea bind:value={name}></textarea>
<!-- Select -->
<select bind:value={role}>
<option value="admin">Admin</option>
<option value="viewer">Viewer</option>
</select>
Element bindings
<script lang="ts">
let el = $state<HTMLElement>()
let width = $state(0)
let height = $state(0)
let scrollY = $state(0)
</script>
<!-- DOM reference -->
<div bind:this={el}></div>
<!-- Dimensions (read-only) -->
<div bind:clientWidth={width} bind:clientHeight={height}></div>
<!-- Window scroll -->
<svelte:window bind:scrollY />
Events
DOM events (Svelte 5)
<script lang="ts">
function handleClick(e: MouseEvent) {
console.log(e.target)
}
function handleInput(e: Event) {
const target = e.target as HTMLInputElement
console.log(target.value)
}
</script>
<!-- Inline handler -->
<button onclick={() => console.log('clicked')}>Click</button>
<!-- Named handler -->
<button onclick={handleClick}>Click</button>
<!-- With event modifiers via JS -->
<button onclick={(e) => { e.preventDefault(); submit() }}>Submit</button>
<!-- Keyboard events -->
<input
onkeydown={(e) => { if (e.key === 'Enter') submit() }}
oninput={handleInput}
/>
Component events (Svelte 5)
<!-- Button.svelte — use callback props instead of createEventDispatcher -->
<script lang="ts">
let { onclick }: { onclick?: () => void } = $props()
</script>
<button {onclick}>Click me</button>
<!-- Parent -->
<Button onclick={() => console.log('clicked')} />
Lifecycle
<script lang="ts">
import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte'
// Runs once after component mounts (browser only)
onMount(() => {
const timer = setInterval(() => { /* ... */ }, 1000)
// Return cleanup (called on destroy)
return () => clearInterval(timer)
})
// Runs before component is destroyed
onDestroy(() => {
console.log('cleanup')
})
// Runs before DOM updates
beforeUpdate(() => { /* ... */ })
// Runs after DOM updates
afterUpdate(() => { /* ... */ })
// Wait for DOM to update
async function updateAndRead() {
someState = 'new value'
await tick() // DOM is now updated
console.log(el.textContent)
}
</script>
Stores
<!-- stores.ts -->
<script lang="ts">
import { writable, readable, derived, get } from 'svelte/store'
// Writable store
export const count = writable(0)
// Readable store (external source)
export const time = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000)
return () => clearInterval(interval)
})
// Derived store
export const doubled = derived(count, ($count) => $count * 2)
// Derived from multiple stores
export const combined = derived(
[count, time],
([$count, $time]) => `${$count} at ${$time.toLocaleTimeString()}`
)
// Read store value outside component
const current = get(count)
</script>
<!-- Component.svelte -->
<script lang="ts">
import { count, doubled } from './stores'
// $ prefix auto-subscribes and auto-unsubscribes
</script>
<p>Count: {$count}</p>
<p>Doubled: {$doubled}</p>
<button onclick={() => count.update(n => n + 1)}>+1</button>
<button onclick={() => count.set(0)}>Reset</button>
Transitions and animations
<script lang="ts">
import { fade, fly, slide, scale, blur, draw, crossfade } from 'svelte/transition'
import { flip } from 'svelte/animate'
import { cubicOut } from 'svelte/easing'
let show = $state(true)
let items = $state(['a', 'b', 'c'])
</script>
<!-- Fade in/out -->
{#if show}
<div transition:fade={{ duration: 300 }}>Fades</div>
{/if}
<!-- Fly from below -->
{#if show}
<div transition:fly={{ y: 20, duration: 400, easing: cubicOut }}>Flies</div>
{/if}
<!-- In and out separately -->
{#if show}
<div in:fly={{ y: -20 }} out:fade>Different in/out</div>
{/if}
<!-- FLIP animation for lists -->
{#each items as item (item)}
<div animate:flip={{ duration: 200 }}>{item}</div>
{/each}
Actions
<!-- actions.ts -->
<script lang="ts">
import type { Action } from 'svelte/action'
// Tooltip action
export const tooltip: Action<HTMLElement, string> = (node, text) => {
// Setup
node.setAttribute('title', text)
return {
update(newText) {
node.setAttribute('title', newText)
},
destroy() {
node.removeAttribute('title')
}
}
}
// Click outside action
export const clickOutside: Action<HTMLElement, () => void> = (node, callback) => {
function handle(e: MouseEvent) {
if (!node.contains(e.target as Node)) callback()
}
document.addEventListener('click', handle)
return {
destroy() {
document.removeEventListener('click', handle)
}
}
}
</script>
<script lang="ts">
import { tooltip, clickOutside } from './actions'
let open = $state(false)
</script>
<button use:tooltip={'Click to open'}>Hover me</button>
{#if open}
<div use:clickOutside={() => (open = false)}>
Dropdown content
</div>
{/if}
Special elements
<!-- Window and document event listeners -->
<svelte:window
bind:scrollY={scrollY}
onkeydown={(e) => { if (e.key === 'Escape') close() }}
/>
<svelte:document
ontouchstart={handleTouch}
/>
<!-- Body attributes -->
<svelte:body class:modal-open={isOpen} />
<!-- Head tags -->
<svelte:head>
<title>Page Title</title>
<meta name="description" content="Page description" />
</svelte:head>
<!-- Dynamic component -->
<script lang="ts">
import A from './A.svelte'
import B from './B.svelte'
let Current = $state(A)
</script>
<svelte:component this={Current} />
<!-- Self-referential (recursive) component -->
<svelte:self depth={depth + 1} />
SvelteKit
File structure
src/
├── routes/
│ ├── +layout.svelte # Root layout (wraps all pages)
│ ├── +layout.ts # Root layout load function
│ ├── +page.svelte # Home page (/)
│ ├── +page.ts # Home page data loading
│ ├── +error.svelte # Error page
│ ├── about/
│ │ └── +page.svelte # /about
│ ├── blog/
│ │ ├── +page.svelte # /blog (list)
│ │ └── [slug]/
│ │ ├── +page.svelte # /blog/:slug
│ │ └── +page.ts # Load single post
│ └── api/
│ └── users/
│ └── +server.ts # REST endpoint
├── lib/
│ ├── components/
│ └── server/ # Server-only code
└── app.html
Load functions
// +page.ts — runs on server and client
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ fetch, params, url }) => {
const res = await fetch(`/api/posts/${params.slug}`)
if (!res.ok) throw error(404, 'Post not found')
const post = await res.json()
return { post }
}
// +page.server.ts — server only (access DB directly)
import type { PageServerLoad } from './$types'
import { db } from '$lib/server/db'
export const load: PageServerLoad = async ({ params, locals }) => {
const user = locals.user // set by hooks.server.ts
const post = await db.post.findUnique({ where: { slug: params.slug } })
if (!post) throw error(404)
return { post, user }
}
<!-- +page.svelte -->
<script lang="ts">
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
</script>
<h1>{data.post.title}</h1>
API routes
// src/routes/api/users/+server.ts
import type { RequestHandler } from './$types'
import { json, error } from '@sveltejs/kit'
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get('page') ?? 1)
const users = await db.user.findMany({ skip: (page - 1) * 10, take: 10 })
return json(users)
}
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json()
// validate body...
const user = await db.user.create({ data: body })
return json(user, { status: 201 })
}
Form actions
// +page.server.ts
import type { Actions } from './$types'
import { fail, redirect } from '@sveltejs/kit'
export const actions: Actions = {
login: async ({ request, cookies }) => {
const data = await request.formData()
const email = data.get('email') as string
const password = data.get('password') as string
if (!email || !password) {
return fail(400, { email, missing: true })
}
const user = await authenticate(email, password)
if (!user) return fail(401, { email, invalid: true })
cookies.set('session', user.sessionId, { path: '/' })
redirect(303, '/dashboard')
}
}
<!-- +page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms'
import type { ActionData } from './$types'
let { form }: { form: ActionData } = $props()
</script>
<form method="POST" action="?/login" use:enhance>
<input name="email" type="email" value={form?.email ?? ''} />
<input name="password" type="password" />
{#if form?.missing}<p>Please fill all fields</p>{/if}
{#if form?.invalid}<p>Invalid credentials</p>{/if}
<button type="submit">Log in</button>
</form>
Navigation and routing
<script lang="ts">
import { goto, invalidate, preloadData } from '$app/navigation'
import { page } from '$app/stores'
// Current route info
// $page.url, $page.params, $page.route.id, $page.status
</script>
<!-- Link with prefetch -->
<a href="/about" data-sveltekit-preload-data>About</a>
<!-- Programmatic navigation -->
<button onclick={() => goto('/dashboard')}>Go to dashboard</button>
<button onclick={() => goto('/blog', { replaceState: true })}>Replace</button>
<!-- Active link -->
<a href="/about" class:active={$page.url.pathname === '/about'}>About</a>
Common patterns
Data fetching with loading state
<script lang="ts">
type User = { id: number; name: string }
let users = $state<User[]>([])
let loading = $state(false)
let error = $state<string | null>(null)
async function fetchUsers() {
loading = true
error = null
try {
const res = await fetch('/api/users')
if (!res.ok) throw new Error('Failed to fetch')
users = await res.json()
} catch (e) {
error = (e as Error).message
} finally {
loading = false
}
}
// onMount alternative using $effect
$effect(() => {
fetchUsers()
})
</script>
{#if loading}
<p>Loading…</p>
{:else if error}
<p>Error: {error}</p>
{:else}
{#each users as user (user.id)}
<p>{user.name}</p>
{/each}
{/if}
Context API (pass data without prop drilling)
<!-- Parent.svelte -->
<script lang="ts">
import { setContext } from 'svelte'
const theme = $state({ primary: '#ff3e00', dark: false })
setContext('theme', theme)
</script>
<Child />
<!-- DeepChild.svelte -->
<script lang="ts">
import { getContext } from 'svelte'
const theme = getContext<{ primary: string; dark: boolean }>('theme')
</script>
<p style:color={theme.primary}>Themed text</p>
Reusable store pattern (context store)
// counterStore.ts — one store per component tree
import { writable } from 'svelte/store'
import { getContext, setContext } from 'svelte'
const KEY = Symbol('counter')
export function createCounter(initial = 0) {
const count = writable(initial)
return {
subscribe: count.subscribe,
increment: () => count.update(n => n + 1),
reset: () => count.set(0)
}
}
export function setCounterContext(counter: ReturnType<typeof createCounter>) {
setContext(KEY, counter)
}
export function getCounterContext() {
return getContext<ReturnType<typeof createCounter>>(KEY)
}
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
let count = 0 in Svelte 5 |
Not reactive without $state |
let count = $state(0) |
Mutating a $state object reference |
Loses reactivity | Mutate properties: obj.name = 'x', not obj = { name: 'x' } |
Using createEventDispatcher in Svelte 5 |
Deprecated | Use callback props: let { onclick } = $props() |
<slot> in Svelte 5 |
Deprecated | Use {#snippet} and {@render} |
Forgetting (key) in {#each} |
Inefficient DOM updates | {#each items as item (item.id)} |
$effect with no cleanup for subscriptions |
Memory leak | Return cleanup: return () => unsub() |
| Accessing DOM before mount | undefined error |
Use onMount or bind:this |
| Using stores when runes suffice | Extra complexity | Prefer $state/$derived inside components |
Svelte 4 → Svelte 5 migration
| Svelte 4 | Svelte 5 |
|---|---|
export let name |
let { name } = $props() |
$: doubled = count * 2 |
let doubled = $derived(count * 2) |
$: { console.log(count) } |
$effect(() => { console.log(count) }) |
<slot> |
{#snippet children()}{/snippet} + {@render children()} |
createEventDispatcher |
Callback props |
on:click |
onclick |
bind:this on component |
bind:this still works |
FAQ
Q: Svelte vs React — what are the key differences?
Svelte compiles to vanilla JS at build time — no runtime framework, no virtual DOM. Components are smaller, faster to hydrate, and require less boilerplate. React has a larger ecosystem and more job market demand. Svelte is excellent for performance-critical apps and smaller teams.
Q: When should I use stores vs runes ($state)?
Use $state/$derived inside components — they're simpler and co-located. Use stores (writable, readable, derived) for state shared across multiple components that don't share a parent, or for state that lives outside the component tree.
Q: How do I share state between routes in SvelteKit?
Use a Svelte store (writable) in src/lib/stores.ts. Import it in both routes. For server data, put it in locals via hooks.server.ts and access via load functions.
Q: Are Svelte 5 runes browser-only?
No — runes work in SSR (SvelteKit) and on the client. $state, $derived, and $effect are all compile-time transforms, not runtime APIs.
Q: How do I handle authentication in SvelteKit?
Use hooks.server.ts to validate the session cookie and populate locals.user. Then in protected +page.server.ts files, check locals.user and throw redirect(303, '/login') if not authenticated.
Q: Does Svelte have TypeScript support?
Yes — use <script lang="ts"> in .svelte files. SvelteKit projects use tsconfig.json. The $props() rune works naturally with TypeScript generics for full type safety.