↑
Introduction
Reactivity
Props
Logic
Events
Bindings
Classes
Actions
Transitions
Advanced Reactivity
Reusing Content
Advanced Bindings
Context API
Special Elements
SvelteKit Introduction
Introduction to Svelte
What Is Svelte?
Svelte is a modern, compiler-based JavaScript framework for building user interfaces.
Unlike Vue or React, Svelte shifts work from the browser to a compile step at build time, producing highly optimized vanilla JavaScript.
Svelte's ecosystem includes:
SvelteKit which is a full-stack application framework (routing, SSR, etc.)
Svelte Stores for built-in state management
Vite is the recommended build tool
How Svelte Works
Svelte compiles your components into efficient imperative code that surgically updates the DOM.
There is no virtual DOM — Svelte generates code that directly manipulates the DOM when state changes.
A Svelte component contains:
a <script> block for JavaScript logic
HTML markup (the template)
an optional <style> block for scoped CSS
<script>
let message = "Hello Svelte!";
</script>
<h1>{message}</h1>
<style>
h1 {
color: #ff3e00;
}
</style>
The text inside { } automatically updates when message changes.
Getting Started (Project Setup)
The recommended way to start a Svelte project is using SvelteKit:
npx sv create my-app
cd my-app/
npm install
npm run dev
For a simpler standalone Svelte project without it:
npm create vite@latest my-app -- --template svelte
cd my-app/
npm install
npm run dev
Core Concepts of Svelte
Reactivity
Svelte's reactivity is built into the language — just assign a new value to trigger updates.
<script>
let count = 0;
function increment() {
count += 1; // This automatically updates the DOM
}
</script>
<button on:click={increment}>
Clicked {count} times
</button>
Reactive Declarations ($:)
Use $: to create reactive statements that re-run when dependencies change.
<script>
let count = 0;
$: doubled = count * 2;
$: console.log("Count is now", count);
</script>
<p>{count} doubled is {doubled}</p>
Event Handling
<button on:click={handleClick}>Click me</button>
<!-- With inline handler -->
<button on:click={() => count++}>Increase</button>
<!-- With modifiers -->
<button on:click|once|preventDefault={handleClick}>Submit</button>
on:click is Svelte's event directive syntax.
Modifiers like |once, |preventDefault, |stopPropagation can be chained.
Two-Way Binding with bind:
<script>
let name = "";
</script>
<input bind:value={name} />
<p>Hello {name}!</p>
Typing into the input updates name immediately.
Conditionals and Loops
<!-- Conditionals -->
{#if loggedIn}
<p>Welcome back!</p>
{:else}
<p>Please log in.</p>
{/if}
<!-- Loops -->
{#each items as item, index (item.id)}
<li>{index}: {item.name}</li>
{/each}
{#if}, {:else}, {/if} are Svelte's conditional blocks.
{#each} iterates over arrays; the (item.id) part is a keyed each block for efficient updates.
Component Structure
Svelte components are stored in .svelte files.
Each file is a single component with script, markup, and styles together.
<!-- Greeting.svelte -->
<script>
export let name = "World";
</script>
<h1>Hello {name}!</h1>
<style>
h1 {
font-family: Georgia, serif;
color: #333;
}
</style>
export let declares a prop that can be passed from parent components.
Styles are scoped by default , they only affect this component.
Using Components
Import and use components like regular ES modules:
<script>
import Greeting from "./Greeting.svelte";
</script>
<Greeting name="Svelte" />
<Greeting /> <!-- Uses default "World" -->
Key Differences from Other Frameworks
Feature
Svelte
Vue / React
Compilation
Compiles to vanilla JS at build time
Ships a runtime library to the browser
Virtual DOM
No virtual DOM
Uses virtual DOM for diffing
Reactivity
Built into the language ($:)
Requires hooks or reactive APIs
Bundle Size
Typically smaller
Includes framework runtime
Learning Curve
Closer to vanilla HTML/CSS/JS
Framework-specific patterns
Svelte 5 Runes (New Reactivity)
Svelte 5 introduced Runes , a new reactivity system using special functions:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
function increment() {
count++;
}
</script>
<button onclick={increment}>
{count} × 2 = {doubled}
</button>
$state() creates reactive state.
$derived() creates computed values.
$effect() runs side effects when dependencies change.
Note: In Svelte 5, event handlers use onclick instead of on:click.
Svelte Reactivity
What Is Reactivity in Svelte?
Reactivity means that when your data changes, the UI automatically updates to reflect those changes.
In Svelte, reactivity is built into the language — you don't need special APIs or hooks.
Svelte's compiler analyzes your code and generates efficient JavaScript that updates the DOM only where necessary.
Assignment-Based Reactivity
In Svelte, assignments trigger reactivity. Simply reassigning a variable updates the DOM.
<script>
let count = 0;
function increment() {
count = count + 1; // Assignment triggers update
}
</script>
<button on:click={increment}>
Count: {count}
</button>
This also works with shorthand operators:
count += 1; // Reactive
count++; // Reactive
count--; // Reactive
Reactivity with Arrays and Objects
Svelte's reactivity is triggered by assignments , not mutations.
Methods like push(), pop(), splice() do not trigger reactivity by themselves:
<script>
let items = ["Apple", "Banana"];
function addItem() {
items.push("Cherry"); // This does NOT trigger update!
}
</script>
To trigger reactivity, you must reassign the variable:
<script>
let items = ["Apple", "Banana"];
function addItem() {
items.push("Cherry");
items = items; // Reassignment triggers update
}
// Or more idiomatically:
function addItemBetter() {
items = [...items, "Cherry"]; // Spread creates new array
}
</script>
let user = { name: "Alice", age: 25 };
user.age = 26; // Does NOT trigger update
user = user; // Triggers update
// Or:
user = { ...user, age: 26 }; // Triggers update
Reactive Declarations ($:)
The $: label creates reactive declarations — values that automatically recalculate when their dependencies change.
<script>
let count = 0;
$: doubled = count * 2;
$: quadrupled = doubled * 2;
</script>
<p>{count} × 2 = {doubled}</p>
<p>{count} × 4 = {quadrupled}</p>
Svelte automatically tracks which variables are used and re-runs the declaration when any dependency changes.
Reactive declarations can depend on other reactive declarations (like quadrupled depends on doubled).
Reactive Statements
You can also use $: to run arbitrary statements reactively:
<script>
let count = 0;
$: console.log("Count changed to:", count);
$: if (count >= 10) {
alert("Count is getting high!");
count = 0;
}
$: {
console.log("Running reactive block");
console.log("Current count:", count);
}
</script>
These statements re-run whenever any referenced variable changes.
Reactive Stores
For state that needs to be shared across components, Svelte provides stores .
A store is an object with a subscribe method that allows reactive access to a value.
Svelte provides three built-in store types.
writable — Read and write
// stores.js
import { writable } from "svelte/store";
export const count = writable(0);
<script>
import { count } from "./stores.js";
function increment() {
count.update(n => n + 1);
}
function reset() {
count.set(0);
}
</script>
set(value) — Sets the store to a new value.
update(fn) — Updates based on current value.
readable — Read only
import { readable } from "svelte/store";
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
});
The second argument is a function that receives set and returns a cleanup function.
derived — Derived from other stores
import { derived } from "svelte/store";
import { count } from "./stores.js";
export const doubled = derived(count, $count => $count * 2);
// Derived from multiple stores:
export const total = derived(
[storeA, storeB],
([$a, $b]) => $a + $b
);
Auto-Subscription with $ Prefix
Inside Svelte components, you can access store values using the $ prefix:
<script>
import { count } from "./stores.js";
</script>
<p>The count is {$count}</p>
<button on:click={() => $count++}>
Increment
</button>
$count automatically subscribes to the store and unsubscribes when the component is destroyed.
You can also assign directly to $count — it's equivalent to calling count.set().
<script>
import { name } from "./stores.js";
</script>
<input bind:value={$name}>
Custom Stores
You can create custom stores with domain-specific logic:
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.js";
</script>
<p>{$counter}</p>
<button on:click={counter.increment}>+</button>
<button on:click={counter.decrement}>-</button>
<button on:click={counter.reset}>Reset</button>
Svelte 5 Runes (New Reactivity System)
Svelte 5 introduces Runes , a new reactivity system that replaces $: and provides more explicit control.
$state — Reactive State
<script>
let count = $state(0);
function increment() {
count++; // Directly mutate, it's reactive!
}
</script>
<button onclick={increment}>
Count: {count}
</button>
With $state, even mutations on arrays and objects are reactive:
let items = $state(["Apple", "Banana"]);
items.push("Cherry"); // This IS reactive in Svelte 5!
let user = $state({ name: "Alice", age: 25 });
user.age = 26; // This IS reactive in Svelte 5!
$derived — Computed Values
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
</script>
<p>{count} × 2 = {doubled}</p>
<p>{count} × 4 = {quadrupled}</p>
$derived replaces the $: reactive declarations.
$effect — Side Effects
<script>
let count = $state(0);
$effect(() => {
console.log("Count is now:", count);
});
$effect(() => {
// Runs when component mounts
console.log("Mounted!");
return () => {
// Cleanup when component unmounts
console.log("Unmounted!");
};
});
</script>
$effect automatically tracks dependencies and re-runs when they change.
Return a function for cleanup (like useEffect in React).
$props — Component Props
<script>
let { name, age = 18 } = $props();
</script>
<p>{name} is {age} years old.</p>
$props replaces export let for declaring component props in Svelte 5.
Comparison: Svelte 4 vs Svelte 5 Reactivity
Feature
Svelte 4
Svelte 5 (Runes)
Reactive variable
let count = 0;
let count = $state(0);
Computed value
$: doubled = count * 2;
let doubled = $derived(count * 2);
Side effect
$: console.log(count);
$effect(() => console.log(count));
Props
export let name;
let { name } = $props();
Array/Object mutation
Requires reassignment
Direct mutation works
Event handlers
on:click={fn}
onclick={fn}
Svelte Props
What Are Props?
Props (short for properties) are how you pass data from a parent component to a child component .
Props allow components to be reusable and configurable.
In Svelte, props flow one direction : from parent to child (unidirectional data flow).
Declaring Props (Svelte 4)
In Svelte 4, use export let to declare a prop:
<!-- Greeting.svelte -->
<script>
export let name;
</script>
<h1>Hello {name}!</h1>
The parent component passes the prop as an attribute:
<!-- App.svelte -->
<script>
import Greeting from "./Greeting.svelte";
</script>
<Greeting name="Alice" />
<Greeting name="Bob" />
Default Values
You can assign a default value to a prop:
<script>
export let name = "World";
export let age = 18;
export let active = false;
</script>
<p>{name} is {age} years old.</p>
<!-- Using defaults -->
<Greeting /> <!-- name="World", age=18 -->
<Greeting name="Alice" /> <!-- name="Alice", age=18 -->
<Greeting name="Bob" age={25} />
Passing Different Data Types
Strings can be passed directly, but other types require curly braces:
<!-- String -->
<User name="Alice" />
<!-- Number -->
<User age={25} />
<!-- Boolean -->
<User active={true} />
<User active /> <!-- Shorthand for active={true} -->
<!-- Array -->
<List items={["Apple", "Banana", "Cherry"]} />
<!-- Object -->
<Profile user={{ name: "Alice", age: 25 }} />
<!-- Variable -->
<script>
let username = "Alice";
</script>
<User name={username} />
Shorthand Props
When the prop name matches the variable name, use the shorthand:
<script>
let name = "Alice";
let age = 25;
</script>
<!-- Instead of this: -->
<User name={name} age={age} />
<!-- Use shorthand: -->
<User {name} {age} />
Spread Props
You can spread an object to pass multiple props at once:
<script>
import User from "./User.svelte";
const userData = {
name: "Alice",
age: 25,
email: "alice@example.com"
};
</script>
<!-- Instead of: -->
<User name={userData.name} age={userData.age} email={userData.email} />
<!-- Use spread: -->
<User {...userData} />
Receiving All Props with $$props and $$restProps
$$props contains all props passed to a component:
<script>
export let name;
export let age;
// $$props = { name: "...", age: ..., ...anyOtherProps }
console.log($$props);
</script>
$$restProps contains props that were not explicitly declared:
<!-- Button.svelte -->
<script>
export let variant = "primary";
// Any other props (class, id, disabled, etc.) go to $$restProps
</script>
<button class="btn btn-{variant}" {...$$restProps}>
<slot />
</button>
<!-- Usage -->
<Button variant="danger" disabled id="submit-btn">
Submit
</Button>
This is useful for wrapper components that forward attributes to native elements.
Reactive Props
Props are reactive — when the parent updates a prop, the child re-renders:
<!-- Parent.svelte -->
<script>
import Counter from "./Counter.svelte";
let count = 0;
</script>
<button on:click={() => count++}>Increment in Parent</button>
<Counter value={count} />
<!-- Counter.svelte -->
<script>
export let value;
$: console.log("Value changed to:", value);
</script>
<p>Count: {value}</p>
Readonly Props (One-Way Binding)
By default, props are one-way : parent → child.
Modifying a prop inside the child does not affect the parent:
<!-- Child.svelte -->
<script>
export let count = 0;
function increment() {
count++; // Only changes local copy, not parent's value
}
</script>
<button on:click={increment}>{count}</button>
To update the parent, use events or two-way binding (covered later).
Props with TypeScript (Svelte 4)
Add types to props using TypeScript:
<script lang="ts">
export let name: string;
export let age: number = 18;
export let active: boolean = false;
export let items: string[] = [];
export let user: { name: string; email: string } | null = null;
</script>
For complex types, define interfaces:
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
export let user: User;
export let users: User[] = [];
</script>
Svelte 5: Props with $props
In Svelte 5, use the $props rune instead of export let:
<!-- Greeting.svelte (Svelte 5) -->
<script>
let { name } = $props();
</script>
<h1>Hello {name}!</h1>
Multiple props with defaults:
<script>
let { name = "World", age = 18, active = false } = $props();
</script>
Rest props (similar to $$restProps):
<script>
let { name, age, ...rest } = $props();
</script>
<div {...rest}>
{name} is {age}
</div>
Svelte 5: Props with TypeScript
Type your props using TypeScript with $props:
<script lang="ts">
interface Props {
name: string;
age?: number;
active?: boolean;
}
let { name, age = 18, active = false }: Props = $props();
</script>
<script lang="ts">
import type { HTMLButtonAttributes } from "svelte/elements";
interface Props extends HTMLButtonAttributes {
variant?: "primary" | "secondary" | "danger";
}
let { variant = "primary", ...rest }: Props = $props();
</script>
<button class="btn btn-{variant}" {...rest}>
<slot />
</button>
Comparison: Svelte 4 vs Svelte 5 Props
Feature
Svelte 4
Svelte 5
Basic prop
export let name;
let { name } = $props();
Default value
export let name = "World";
let { name = "World" } = $props();
Multiple props
Multiple export let lines
Destructure from $props()
Rest props
$$restProps
let { a, ...rest } = $props();
All props
$$props
let props = $props();
Svelte Logic
What Is Svelte Logic?
Svelte provides special logic blocks in templates to handle conditional rendering, loops, and asynchronous data.
These blocks use a syntax with {#...} to open, {:...} for continuation, and {/...} to close.
The main logic blocks are:
{#if} — Conditional rendering
{#each} — Looping over arrays
{#await} — Handling promises
{#key} — Forcing re-render on value change
Conditional Rendering with {#if}
Use {#if} to conditionally render content:
<script lang="ts">
let loggedIn: boolean = false;
</script>
{#if loggedIn}
<p>Welcome back!</p>
{/if}
<button onclick={() => loggedIn = !loggedIn}>
Toggle Login
</button>
The content inside {#if} only renders when the condition is true.
{:else} Block
Use {:else} to render alternative content when the condition is false:
<script lang="ts">
let loggedIn: boolean = false;
</script>
{#if loggedIn}
<p>Welcome back, user!</p>
<button onclick={() => loggedIn = false}>Log Out</button>
{:else}
<p>Please log in.</p>
<button onclick={() => loggedIn = true}>Log In</button>
{/if}
{:else if} Block
Chain multiple conditions with {:else if}:
<script lang="ts">
let score: number = 75;
</script>
{#if score >= 90}
<p>Grade: A</p>
{:else if score >= 80}
<p>Grade: B</p>
{:else if score >= 70}
<p>Grade: C</p>
{:else if score >= 60}
<p>Grade: D</p>
{:else}
<p>Grade: F</p>
{/if}
Nested Conditionals
You can nest {#if} blocks inside each other:
<script lang="ts">
let loggedIn: boolean = true;
let isAdmin: boolean = true;
</script>
{#if loggedIn}
<p>Welcome!</p>
{#if isAdmin}
<p>You have admin privileges.</p>
<button>Access Admin Panel</button>
{:else}
<p>You are a regular user.</p>
{/if}
{:else}
<p>Please log in.</p>
{/if}
Looping with {#each}
Use {#each} to iterate over arrays:
<script lang="ts">
let fruits: string[] = ["Apple", "Banana", "Cherry"];
</script>
<ul>
{#each fruits as fruit}
<li>{fruit}</li>
{/each}
</ul>
{#each} with Index
Access the current index as the second parameter:
<script lang="ts">
let fruits: string[] = ["Apple", "Banana", "Cherry"];
</script>
<ul>
{#each fruits as fruit, index}
<li>{index + 1}. {fruit}</li>
{/each}
</ul>
Keyed {#each} Blocks
When items can be added, removed, or reordered, use a key to help Svelte track each item:
<script lang="ts">
interface Todo {
id: number;
text: string;
done: boolean;
}
let todos: Todo[] = [
{ id: 1, text: "Learn Svelte", done: false },
{ id: 2, text: "Build an app", done: false },
{ id: 3, text: "Deploy", done: false }
];
</script>
<ul>
{#each todos as todo (todo.id)}
<li>{todo.text}</li>
{/each}
</ul>
The (todo.id) part is the key — it should be a unique identifier.
Keys ensure correct DOM updates when the list changes (especially important for animations and component state).
{#each} with Destructuring
Destructure objects directly in the each block:
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
let users: User[] = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" }
];
</script>
<ul>
{#each users as { id, name, email } (id)}
<li>{name} - {email}</li>
{/each}
</ul>
{:else} in {#each}
Render fallback content when the array is empty:
<script lang="ts">
let todos: string[] = [];
</script>
<ul>
{#each todos as todo}
<li>{todo}</li>
{:else}
<li>No todos yet. Add one!</li>
{/each}
</ul>
Iterating Over Objects
Use Object.entries() or Object.keys() to iterate over objects:
<script lang="ts">
const scores: Record<string, number> = {
Alice: 95,
Bob: 82,
Charlie: 78
};
</script>
<ul>
{#each Object.entries(scores) as [name, score]}
<li>{name}: {score}</li>
{/each}
</ul>
Handling Promises with {#await}
Use {#await} to handle asynchronous data directly in templates:
<script lang="ts">
interface User {
id: number;
name: string;
}
async function fetchUser(): Promise<User> {
const response = await fetch("https://api.example.com/user/1");
return response.json();
}
let userPromise: Promise<User> = fetchUser();
</script>
{#await userPromise}
<p>Loading...</p>
{:then user}
<p>Hello, {user.name}!</p>
{:catch error}
<p>Error: {error.message}</p>
{/await}
{#await promise} — Shows while pending
{:then value} — Shows when resolved
{:catch error} — Shows when rejected
{#await} Without Loading State
If you don't need a loading state, skip directly to {:then}:
<script lang="ts">
let dataPromise: Promise<string> = Promise.resolve("Hello!");
</script>
{#await dataPromise then data}
<p>{data}</p>
{/await}
Nothing renders until the promise resolves.
{#await} Without Catch
You can omit {:catch} if you don't need error handling in the template:
<script lang="ts">
interface Post {
title: string;
body: string;
}
async function fetchPost(): Promise<Post> {
const res = await fetch("https://api.example.com/posts/1");
return res.json();
}
let postPromise: Promise<Post> = fetchPost();
</script>
{#await postPromise}
<p>Loading post...</p>
{:then post}
<h2>{post.title}</h2>
<p>{post.body}</p>
{/await}
Refreshing {#await} Data
Reassign the promise to trigger a refresh:
<script lang="ts">
interface User {
id: number;
name: string;
}
async function fetchRandomUser(): Promise<User> {
const id = Math.floor(Math.random() * 10) + 1;
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
let userPromise: Promise<User> = fetchRandomUser();
function refresh(): void {
userPromise = fetchRandomUser(); // Reassign to refetch
}
</script>
<button onclick={refresh}>Load Random User</button>
{#await userPromise}
<p>Loading...</p>
{:then user}
<p>{user.name}</p>
{:catch error}
<p>Failed to load user.</p>
{/await}
Forcing Re-render with {#key}
The {#key} block destroys and recreates its content when the key value changes:
<script lang="ts">
let userId: number = 1;
</script>
<input type="number" bind:value={userId} min="1" />
{#key userId}
<UserProfile id={userId} />
{/key}
When userId changes, the UserProfile component is destroyed and a new instance is created.
This resets all component state and re-runs lifecycle functions.
{#key} for Animations
{#key} is useful for triggering intro/outro transitions:
<script lang="ts">
import { fade } from "svelte/transition";
let count: number = 0;
</script>
<button onclick={() => count++}>Increment</button>
{#key count}
<p transition:fade>{count}</p>
{/key}
Every time count changes, the paragraph fades out and a new one fades in.
Combining Logic Blocks
Logic blocks can be combined for complex rendering:
<script lang="ts">
interface Post {
id: number;
title: string;
published: boolean;
}
async function fetchPosts(): Promise<Post[]> {
const res = await fetch("https://api.example.com/posts");
return res.json();
}
let postsPromise: Promise<Post[]> = fetchPosts();
let showOnlyPublished: boolean = false;
</script>
<label>
<input type="checkbox" bind:checked={showOnlyPublished} />
Show only published
</label>
{#await postsPromise}
<p>Loading posts...</p>
{:then posts}
<ul>
{#each posts as post (post.id)}
{#if !showOnlyPublished || post.published}
<li>
{post.title}
{#if !post.published}
<span>(Draft)</span>
{/if}
</li>
{/if}
{/each}
</ul>
{:catch}
<p>Failed to load posts.</p>
{/await}
Logic Blocks Summary
Block
Purpose
Syntax
{#if}
Conditional rendering
{#if condition}...{/if}
{:else}
Else branch
{#if}...{:else}...{/if}
{:else if}
Else-if branch
{#if}...{:else if}...{/if}
{#each}
Loop over arrays
{#each array as item}...{/each}
{#each} + key
Keyed loop
{#each array as item (key)}...{/each}
{#await}
Handle promises
{#await promise}...{:then}...{:catch}...{/await}
{#key}
Force re-render
{#key value}...{/key}
Svelte Events
What Are Events in Svelte?
Events allow components to respond to user interactions and communicate with each other.
Svelte supports:
DOM events — Native browser events like click, input, submit
Component events — Custom events dispatched from child to parent
Svelte 4 uses on:event syntax, while Svelte 5 uses onevent syntax.
DOM Event Handling (Svelte 4)
Use on: directive to listen to DOM events:
<script lang="ts">
let count: number = 0;
function handleClick(): void {
count++;
}
</script>
<button on:click={handleClick}>
Clicked {count} times
</button>
<!-- Mouse events -->
<button on:click={handleClick}>Click</button>
<div on:dblclick={handleDoubleClick}>Double click</div>
<div on:mouseenter={handleMouseEnter}>Hover me</div>
<div on:mouseleave={handleMouseLeave}>Leave me</div>
<div on:mousemove={handleMouseMove}>Move inside</div>
<!-- Keyboard events -->
<input on:keydown={handleKeyDown} />
<input on:keyup={handleKeyUp} />
<input on:keypress={handleKeyPress} />
<!-- Form events -->
<input on:input={handleInput} />
<input on:change={handleChange} />
<input on:focus={handleFocus} />
<input on:blur={handleBlur} />
<form on:submit={handleSubmit}>...</form>
<!-- Other events -->
<div on:scroll={handleScroll}>...</div>
<img on:load={handleLoad} on:error={handleError} />
DOM Event Handling (Svelte 5)
In Svelte 5, use standard HTML attribute syntax:
<script lang="ts">
let count: number = $state(0);
function handleClick(): void {
count++;
}
</script>
<button onclick={handleClick}>
Clicked {count} times
</button>
<!-- Svelte 5 event syntax -->
<button onclick={handleClick}>Click</button>
<div ondblclick={handleDoubleClick}>Double click</div>
<input onkeydown={handleKeyDown} />
<input oninput={handleInput} />
<form onsubmit={handleSubmit}>...</form>
Inline Event Handlers
You can define handlers inline using arrow functions:
<script lang="ts">
let count: number = $state(0);
let message: string = $state("");
</script>
<!-- Svelte 5 syntax -->
<button onclick={() => count++}>Increment</button>
<button onclick={() => count = 0}>Reset</button>
<button onclick={() => alert("Hello!")}>Alert</button>
<input oninput={(e) => message = e.currentTarget.value} />
<!-- Svelte 4 syntax -->
<button on:click={() => count++}>Increment</button>
<button on:click={() => count = 0}>Reset</button>
The Event Object
Event handlers receive the native DOM event object:
<script lang="ts">
function handleClick(event: MouseEvent): void {
console.log("Clicked at:", event.clientX, event.clientY);
console.log("Target:", event.target);
console.log("Current target:", event.currentTarget);
}
function handleKeyDown(event: KeyboardEvent): void {
console.log("Key pressed:", event.key);
console.log("Key code:", event.code);
if (event.key === "Enter") {
console.log("Enter pressed!");
}
}
function handleInput(event: Event): void {
const target = event.currentTarget as HTMLInputElement;
console.log("Input value:", target.value);
}
</script>
<button onclick={handleClick}>Click me</button>
<input onkeydown={handleKeyDown} />
<input oninput={handleInput} />
Event Modifiers (Svelte 4)
Svelte 4 provides modifiers to alter event behavior:
<!-- preventDefault: Prevents default browser action -->
<form on:submit|preventDefault={handleSubmit}>
<button type="submit">Submit</button>
</form>
<!-- stopPropagation: Stops event from bubbling up -->
<div on:click={handleOuter}>
<button on:click|stopPropagation={handleInner}>Click</button>
</div>
<!-- once: Handler runs only once -->
<button on:click|once={handleClick}>Click once</button>
<!-- capture: Use capture phase instead of bubble -->
<div on:click|capture={handleClick}>...</div>
<!-- self: Only trigger if event.target is the element itself -->
<div on:click|self={handleClick}>
<button>Clicking here won't trigger parent</button>
</div>
<!-- passive: Improves scroll performance -->
<div on:scroll|passive={handleScroll}>...</div>
<!-- nonpassive: Explicitly not passive -->
<div on:touchstart|nonpassive={handleTouch}>...</div>
<!-- trusted: Only trigger for user-initiated events -->
<button on:click|trusted={handleClick}>...</button>
Modifiers can be chained:
<form on:submit|preventDefault|stopPropagation={handleSubmit}>
...
</form>
<button on:click|once|capture={handleClick}>...</button>
Event Modifiers (Svelte 5)
Svelte 5 doesn't have the modifier syntax. Use standard JavaScript instead:
<script lang="ts">
function handleSubmit(event: SubmitEvent): void {
event.preventDefault();
// Handle form submission
}
function handleInnerClick(event: MouseEvent): void {
event.stopPropagation();
// Handle click
}
</script>
<form onsubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
<div onclick={handleOuter}>
<button onclick={handleInnerClick}>Click</button>
</div>
For once, use a wrapper or state:
<script lang="ts">
let clicked: boolean = $state(false);
function handleOnce(): void {
if (clicked) return;
clicked = true;
console.log("This runs only once");
}
</script>
<button onclick={handleOnce}>Click once</button>
For capture and passive, use onclickcapture or handle in $effect:
<!-- Capture phase -->
<div onclickcapture={handleClick}>...</div>
Component Events (Svelte 4)
Components can dispatch custom events to their parent using createEventDispatcher:
<!-- Button.svelte -->
<script lang="ts">
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher<{
click: void;
customEvent: { message: string };
}>();
function handleClick(): void {
dispatch("click");
dispatch("customEvent", { message: "Hello from child!" });
}
</script>
<button on:click={handleClick}>
<slot />
</button>
<!-- Parent.svelte -->
<script lang="ts">
import Button from "./Button.svelte";
function handleCustomEvent(event: CustomEvent<{ message: string }>): void {
console.log(event.detail.message);
}
</script>
<Button on:click={() => console.log("Clicked!")} on:customEvent={handleCustomEvent}>
Click me
</Button>
Component Events (Svelte 5)
In Svelte 5, use callback props instead of createEventDispatcher:
<!-- Button.svelte -->
<script lang="ts">
interface Props {
onclick?: () => void;
onCustomEvent?: (data: { message: string }) => void;
children?: any;
}
let { onclick, onCustomEvent, children }: Props = $props();
function handleClick(): void {
onclick?.();
onCustomEvent?.({ message: "Hello from child!" });
}
</script>
<button onclick={handleClick}>
{@render children?.()}
</button>
<!-- Parent.svelte -->
<script lang="ts">
import Button from "./Button.svelte";
function handleCustomEvent(data: { message: string }): void {
console.log(data.message);
}
</script>
<Button onclick={() => console.log("Clicked!")} onCustomEvent={handleCustomEvent}>
Click me
</Button>
Event Forwarding (Svelte 4)
Forward DOM events from a child element without handling them:
<!-- Button.svelte -->
<script lang="ts">
export let variant: string = "primary";
</script>
<!-- Forward click event to parent -->
<button class="btn btn-{variant}" on:click>
<slot />
</button>
<!-- Parent.svelte -->
<script lang="ts">
import Button from "./Button.svelte";
</script>
<Button on:click={() => console.log("Button clicked!")}>
Click me
</Button>
The on:click without a value forwards the event.
Forward multiple events:
<input
on:input
on:change
on:focus
on:blur
on:keydown
/>
Event Forwarding (Svelte 5)
In Svelte 5, use rest props to forward events:
<!-- Button.svelte -->
<script lang="ts">
import type { HTMLButtonAttributes } from "svelte/elements";
interface Props extends HTMLButtonAttributes {
variant?: string;
}
let { variant = "primary", children, ...rest }: Props = $props();
</script>
<button class="btn btn-{variant}" {...rest}>
{@render children?.()}
</button>
<!-- Parent.svelte -->
<script lang="ts">
import Button from "./Button.svelte";
</script>
<Button onclick={() => console.log("Clicked!")} onmouseenter={() => console.log("Hovered!")}>
Click me
</Button>
Typed Custom Events (Svelte 4)
Strongly type custom events with generics:
<!-- SearchBox.svelte -->
<script lang="ts">
import { createEventDispatcher } from "svelte";
interface SearchEvents {
search: string;
clear: void;
select: { id: number; label: string };
}
const dispatch = createEventDispatcher<SearchEvents>();
let query: string = "";
function handleSearch(): void {
dispatch("search", query);
}
function handleClear(): void {
query = "";
dispatch("clear");
}
function handleSelect(id: number, label: string): void {
dispatch("select", { id, label });
}
</script>
<input bind:value={query} on:input={handleSearch} />
<button on:click={handleClear}>Clear</button>
<!-- Parent.svelte -->
<script lang="ts">
import SearchBox from "./SearchBox.svelte";
function onSearch(event: CustomEvent<string>): void {
console.log("Searching for:", event.detail);
}
function onClear(): void {
console.log("Search cleared");
}
function onSelect(event: CustomEvent<{ id: number; label: string }>): void {
console.log("Selected:", event.detail.id, event.detail.label);
}
</script>
<SearchBox on:search={onSearch} on:clear={onClear} on:select={onSelect} />
Typed Callback Props (Svelte 5)
In Svelte 5, callbacks are naturally typed:
<!-- SearchBox.svelte -->
<script lang="ts">
interface Props {
onSearch?: (query: string) => void;
onClear?: () => void;
onSelect?: (item: { id: number; label: string }) => void;
}
let { onSearch, onClear, onSelect }: Props = $props();
let query: string = $state("");
function handleInput(): void {
onSearch?.(query);
}
function handleClear(): void {
query = "";
onClear?.();
}
</script>
<input bind:value={query} oninput={handleInput} />
<button onclick={handleClear}>Clear</button>
<!-- Parent.svelte -->
<script lang="ts">
import SearchBox from "./SearchBox.svelte";
function handleSearch(query: string): void {
console.log("Searching for:", query);
}
function handleSelect(item: { id: number; label: string }): void {
console.log("Selected:", item.id, item.label);
}
</script>
<SearchBox onSearch={handleSearch} onClear={() => console.log("Cleared")} onSelect={handleSelect} />
Handling Window and Document Events
Use <svelte:window> and <svelte:document> for global events:
<script lang="ts">
let innerWidth: number = $state(0);
let innerHeight: number = $state(0);
function handleKeyDown(event: KeyboardEvent): void {
if (event.key === "Escape") {
console.log("Escape pressed!");
}
}
function handleVisibilityChange(): void {
console.log("Visibility:", document.visibilityState);
}
</script>
<!-- Window events -->
<svelte:window
bind:innerWidth
bind:innerHeight
onkeydown={handleKeyDown}
onresize={() => console.log("Resized!")}
/>
<!-- Document events -->
<svelte:document onvisibilitychange={handleVisibilityChange} />
<p>Window size: {innerWidth} x {innerHeight}</p>
Handling Body Events
Use <svelte:body> for body-level events:
<script lang="ts">
let mouseX: number = $state(0);
let mouseY: number = $state(0);
function handleMouseMove(event: MouseEvent): void {
mouseX = event.clientX;
mouseY = event.clientY;
}
</script>
<svelte:body onmousemove={handleMouseMove} />
<p>Mouse position: {mouseX}, {mouseY}</p>
Event Syntax Comparison
Feature
Svelte 4
Svelte 5
DOM event
on:click={handler}
onclick={handler}
Inline handler
on:click={() => ...}
onclick={() => ...}
Modifiers
on:click|preventDefault
event.preventDefault()
Event forwarding
on:click (no value)
{...rest} spread
Component events
createEventDispatcher
Callback props
Listen to component
on:customEvent={handler}
onCustomEvent={handler}
Capture phase
on:click|capture
onclickcapture
Svelte Bindings
What Are Bindings?
Bindings create a two-way connection between a variable and an element property or component prop.
When the variable changes, the element updates. When the element changes (e.g., user input), the variable updates.
Svelte uses the bind: directive for bindings.
Basic Input Binding
Bind an input's value to a variable:
<script lang="ts">
let name: string = $state("");
</script>
<input type="text" bind:value={name} />
<p>Hello, {name || "stranger"}!</p>
Typing in the input automatically updates name.
Changing name programmatically updates the input.
Text Input Types
Works with various text input types:
<script lang="ts">
let text: string = $state("");
let email: string = $state("");
let password: string = $state("");
let search: string = $state("");
let url: string = $state("");
let tel: string = $state("");
</script>
<input type="text" bind:value={text} />
<input type="email" bind:value={email} />
<input type="password" bind:value={password} />
<input type="search" bind:value={search} />
<input type="url" bind:value={url} />
<input type="tel" bind:value={tel} />
Numeric Input Binding
For type="number" and type="range", the value is automatically coerced to a number:
<script lang="ts">
let quantity: number = $state(1);
let volume: number = $state(50);
</script>
<input type="number" bind:value={quantity} min="0" max="100" />
<p>Quantity: {quantity} (type: {typeof quantity})</p>
<input type="range" bind:value={volume} min="0" max="100" />
<p>Volume: {volume}%</p>
The bound variable will be a number, not a string.
Checkbox Binding
For checkboxes, bind to checked instead of value:
<script lang="ts">
let agreed: boolean = $state(false);
let subscribed: boolean = $state(true);
</script>
<label>
<input type="checkbox" bind:checked={agreed} />
I agree to the terms
</label>
<label>
<input type="checkbox" bind:checked={subscribed} />
Subscribe to newsletter
</label>
<p>Agreed: {agreed}, Subscribed: {subscribed}</p>
Checkbox Group Binding
Bind multiple checkboxes to an array using bind:group:
<script lang="ts">
let selectedFruits: string[] = $state([]);
const fruits: string[] = ["Apple", "Banana", "Cherry", "Mango"];
</script>
{#each fruits as fruit}
<label>
<input type="checkbox" value={fruit} bind:group={selectedFruits} />
{fruit}
</label>
{/each}
<p>Selected: {selectedFruits.join(", ") || "None"}</p>
Checking a box adds its value to the array.
Unchecking removes it from the array.
Radio Button Binding
Bind radio buttons to a single value using bind:group:
<script lang="ts">
let selectedColor: string = $state("red");
const colors: string[] = ["red", "green", "blue"];
</script>
{#each colors as color}
<label>
<input type="radio" value={color} bind:group={selectedColor} />
{color}
</label>
{/each}
<p>Selected color: {selectedColor}</p>
Only one radio button in a group can be selected at a time.
Radio Button with Objects
Radio buttons can bind to object references:
<script lang="ts">
interface Plan {
id: string;
name: string;
price: number;
}
const plans: Plan[] = [
{ id: "basic", name: "Basic", price: 9.99 },
{ id: "pro", name: "Pro", price: 19.99 },
{ id: "enterprise", name: "Enterprise", price: 49.99 }
];
let selectedPlan: Plan = $state(plans[0]);
</script>
{#each plans as plan}
<label>
<input type="radio" value={plan} bind:group={selectedPlan} />
{plan.name} - ${plan.price}/month
</label>
{/each}
<p>Selected: {selectedPlan.name} at ${selectedPlan.price}</p>
Select Dropdown Binding
Bind a <select> element to a variable:
<script lang="ts">
const selectedCountry: string = $state("");
const countries: string[] = ["USA", "Canada", "UK", "Germany", "Japan"];
</script>
<select bind:value={selectedCountry}>
<option value="">Select a country</option>
{#each countries as country}
<option value={country}>{country}</option>
{/each}
</select>
<p>Selected: {selectedCountry || "None"}</p>
Select with Objects
Select elements can bind to object values:
<script lang="ts">
interface Country {
code: string;
name: string;
population: number;
}
const countries: Country[] = [
{ code: "US", name: "United States", population: 331000000 },
{ code: "JP", name: "Japan", population: 125800000 },
{ code: "DE", name: "Germany", population: 83200000 }
];
const selectedCountry: Country | undefined = $state(undefined);
</script>
<select bind:value={selectedCountry}>
<option value={undefined}>Select a country</option>
{#each countries as country}
<option value={country}>{country.name}</option>
{/each}
</select>
{#if selectedCountry}
<p>{selectedCountry.name}: {selectedCountry.population.toLocaleString()} people</p>
{/if}
Multiple Select Binding
Bind a multi-select to an array:
<script lang="ts">
const selectedSkills: string[] = $state([]);
const skills: string[] = ["JavaScript", "TypeScript", "Python", "Rust", "Go"];
</script>
<select multiple bind:value={selectedSkills}>
{#each skills as skill}
<option value={skill}>{skill}</option>
{/each}
</select>
<p>Selected: {selectedSkills.join(", ") || "None"}</p>
Hold Ctrl/Cmd to select multiple options.
Textarea Binding
Bind a textarea's content:
<script lang="ts">
const bio: string = $state("");
</script>
<textarea bind:value={bio} rows="4" cols="50"></textarea>
<p>Character count: {bio.length}</p>
<p>Preview:</p>
<pre>{bio}</pre>
Contenteditable Binding
Bind to contenteditable elements using textContent or innerHTML:
<script lang="ts">
let plainText: string = $state("Edit this text");
let richText: string = $state("<b>Bold</b> and <i>italic</i>");
</script>
<!-- Plain text binding -->
<div contenteditable="true" bind:textContent={plainText}></div>
<p>Plain: {plainText}</p>
<!-- HTML binding -->
<div contenteditable="true" bind:innerHTML={richText}></div>
<p>HTML: {richText}</p>
bind:textContent — Gets/sets plain text only.
bind:innerHTML — Gets/sets HTML content.
Element Reference Binding (bind:this)
Get a reference to a DOM element:
<script lang="ts">
import { onMount } from "svelte";
let inputElement: HTMLInputElement;
let canvasElement: HTMLCanvasElement;
onMount(() => {
// Focus the input on mount
inputElement.focus();
// Draw on canvas
const ctx = canvasElement.getContext("2d");
if (ctx) {
ctx.fillStyle = "blue";
ctx.fillRect(10, 10, 100, 100);
}
});
</script>
<input bind:this={inputElement} placeholder="Auto-focused" />
<canvas bind:this={canvasElement} width="200" height="150"></canvas>
The element is undefined until the component mounts.
Use onMount or $effect to safely access the element.
Element Reference with Svelte 5
In Svelte 5, use $effect to work with element references:
<script lang="ts">
let inputElement: HTMLInputElement | undefined = $state(undefined);
$effect(() => {
if (inputElement) {
inputElement.focus();
}
});
</script>
<input bind:this={inputElement} placeholder="Auto-focused" />
Dimension Bindings
Bind to an element's dimensions (read-only):
<script lang="ts">
let clientWidth: number = $state(0);
let clientHeight: number = $state(0);
let offsetWidth: number = $state(0);
let offsetHeight: number = $state(0);
</script>
<div
bind:clientWidth
bind:clientHeight
bind:offsetWidth
bind:offsetHeight
style="padding: 20px; border: 5px solid black;"
>
<p>Resize the window to see changes</p>
</div>
<p>Client: {clientWidth} x {clientHeight}</p>
<p>Offset: {offsetWidth} x {offsetHeight}</p>
clientWidth/Height — Content + padding (excluding border and scrollbar).
offsetWidth/Height — Content + padding + border.
Media Element Bindings
Bind to audio and video element properties:
<script lang="ts">
let videoElement: HTMLVideoElement;
// Readonly bindings
let duration: number = $state(0);
let buffered: TimeRanges;
let seekable: TimeRanges;
let played: TimeRanges;
let seeking: boolean = $state(false);
let ended: boolean = $state(false);
let readyState: number = $state(0);
let videoWidth: number = $state(0);
let videoHeight: number = $state(0);
// Two-way bindings
let currentTime: number = $state(0);
let playbackRate: number = $state(1);
let paused: boolean = $state(true);
let volume: number = $state(1);
let muted: boolean = $state(false);
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, "0")}`;
}
</script>
<video
bind:this={videoElement}
bind:duration
bind:currentTime
bind:paused
bind:volume
bind:muted
bind:playbackRate
bind:seeking
bind:ended
src="video.mp4"
width="400"
>
<track kind="captions" />
</video>
<div>
<button onclick={() => paused = !paused}>
{paused ? "Play" : "Pause"}
</button>
<span>{formatTime(currentTime)} / {formatTime(duration)}</span>
</div>
<div>
<label>
Volume:
<input type="range" bind:value={volume} min="0" max="1" step="0.1" />
</label>
<label>
<input type="checkbox" bind:checked={muted} />
Muted
</label>
</div>
<div>
<label>
Speed:
<select bind:value={playbackRate}>
<option value={0.5}>0.5x</option>
<option value={1}>1x</option>
<option value={1.5}>1.5x</option>
<option value={2}>2x</option>
</select>
</label>
</div>
Window Bindings
Bind to window properties using <svelte:window>:
<script lang="ts">
// Readonly
let innerWidth: number = $state(0);
let innerHeight: number = $state(0);
let outerWidth: number = $state(0);
let outerHeight: number = $state(0);
let online: boolean = $state(true);
// Two-way
let scrollX: number = $state(0);
let scrollY: number = $state(0);
</script>
<svelte:window
bind:innerWidth
bind:innerHeight
bind:outerWidth
bind:outerHeight
bind:scrollX
bind:scrollY
bind:online
/>
<div style="position: fixed; top: 10px; right: 10px; background: white; padding: 10px;">
<p>Window: {innerWidth} x {innerHeight}</p>
<p>Scroll: {scrollX}, {scrollY}</p>
<p>Online: {online ? "Yes" : "No"}</p>
</div>
<button onclick={() => scrollY = 0}>Scroll to top</button>
Document Bindings
Bind to document properties using <svelte:document>:
<script lang="ts">
let activeElement: Element | null = $state(null);
let fullscreenElement: Element | null = $state(null);
let pointerLockElement: Element | null = $state(null);
let visibilityState: DocumentVisibilityState = $state("visible");
</script>
<svelte:document
bind:activeElement
bind:fullscreenElement
bind:pointerLockElement
bind:visibilityState
/>
<p>Active element: {activeElement?.tagName || "None"}</p>
<p>Visibility: {visibilityState}</p>
<input placeholder="Focus me" />
<button>Or focus me</button>
Component Bindings
Bind to a child component's exported values (Svelte 4):
<!-- Counter.svelte (Svelte 4) -->
<script lang="ts">
export let count: number = 0;
export function reset(): void {
count = 0;
}
</script>
<button on:click={() => count++}>{count}</button>
<!-- Parent.svelte (Svelte 4) -->
<script lang="ts">
import Counter from "./Counter.svelte";
let counterValue: number;
let counterComponent: Counter;
</script>
<Counter bind:count={counterValue} bind:this={counterComponent} />
<p>Counter value: {counterValue}</p>
<button on:click={() => counterComponent.reset()}>Reset</button>
Component Bindings (Svelte 5)
In Svelte 5, use $bindable for props that can be bound:
<!-- Counter.svelte (Svelte 5) -->
<script lang="ts">
interface Props {
count?: number;
}
let { count = $bindable(0) }: Props = $props();
</script>
<button onclick={() => count++}>{count}</button>
<!-- Parent.svelte (Svelte 5) -->
<script lang="ts">
import Counter from "./Counter.svelte";
let counterValue: number = $state(0);
</script>
<Counter bind:count={counterValue} />
<p>Counter value: {counterValue}</p>
<button onclick={() => counterValue = 0}>Reset</button>
$bindable() marks a prop as bindable with an optional default value.
Changes in either parent or child sync both ways.
Binding Shorthand
When the variable name matches the property name, use the shorthand:
<script lang="ts">
let value: string = $state("");
let checked: boolean = $state(false);
let clientWidth: number = $state(0);
let innerWidth: number = $state(0);
</script>
<!-- These are equivalent: -->
<input bind:value={value} />
<input bind:value />
<input type="checkbox" bind:checked={checked} />
<input type="checkbox" bind:checked />
<div bind:clientWidth={clientWidth}>...</div>
<div bind:clientWidth>...</div>
<svelte:window bind:innerWidth={innerWidth} />
<svelte:window bind:innerWidth />
Binding Summary
Element
Binding
Type
Text input
bind:value
string
Number/Range input
bind:value
number
Checkbox
bind:checked
boolean
Checkbox group
bind:group
Array
Radio group
bind:group
Single value
Select
bind:value
Any
Multi-select
bind:value
Array
Textarea
bind:value
string
Contenteditable
bind:textContent, bind:innerHTML
string
Any element
bind:this
HTMLElement
Any element
bind:clientWidth, etc.
number (readonly)
Audio/Video
bind:currentTime, bind:paused, etc.
Various
<svelte:window>
bind:innerWidth, bind:scrollY, etc.
number
Component (Svelte 5)
bind:prop with $bindable
Any
Svelte Classes
What Are Class Directives?
Svelte provides special syntax for dynamically adding and removing CSS classes.
The class: directive conditionally applies a class based on a boolean expression.
This is cleaner than manually building class strings.
Basic Class Attribute
You can use the standard class attribute:
<script lang="ts">
let className: string = "button primary";
</script>
<button class="button">Static class</button>
<button class={className}>Dynamic class</button>
Conditional Classes with Ternary
Use template expressions to conditionally apply classes:
<script lang="ts">
let isActive: boolean = $state(false);
let isDisabled: boolean = $state(false);
</script>
<button class={isActive ? "active" : ""}>
Toggle
</button>
<button class={isActive ? "btn active" : "btn"}>
With base class
</button>
<button class="btn {isActive ? 'active' : ''} {isDisabled ? 'disabled' : ''}">
Multiple conditions
</button>
This works but can become verbose with multiple conditions.
The class: Directive
The class: directive provides a cleaner syntax:
<script lang="ts">
let isActive: boolean = $state(false);
</script>
<button class:active={isActive}>
Toggle
</button>
<!-- The class "active" is added when isActive is true -->
<style>
.active {
background-color: #4CAF50;
color: white;
}
</style>
Syntax: class:classname={condition}
When condition is truthy, classname is added.
Shorthand class: Directive
When the class name matches the variable name, use the shorthand:
<script lang="ts">
let active: boolean = $state(false);
let disabled: boolean = $state(false);
let hidden: boolean = $state(false);
</script>
<!-- These are equivalent: -->
<button class:active={active}>Long form</button>
<button class:active>Shorthand</button>
<!-- Multiple shorthand classes -->
<button class:active class:disabled class:hidden>
Multiple
</button>
Multiple Class Directives
Apply multiple conditional classes to one element:
<script lang="ts">
let isActive: boolean = $state(true);
let isLarge: boolean = $state(false);
let isPrimary: boolean = $state(true);
let isDisabled: boolean = $state(false);
</script>
<button
class="btn"
class:active={isActive}
class:large={isLarge}
class:primary={isPrimary}
class:disabled={isDisabled}
>
Styled Button
</button>
<style>
.btn {
padding: 10px 20px;
border: none;
cursor: pointer;
}
.active { border: 2px solid blue; }
.large { font-size: 1.5rem; padding: 15px 30px; }
.primary { background-color: #007bff; color: white; }
.disabled { opacity: 0.5; cursor: not-allowed; }
</style>
Combining class and class:
Use both static classes and conditional class directives together:
<script lang="ts">
let variant: string = $state("primary");
let isLoading: boolean = $state(false);
let isFullWidth: boolean = $state(false);
</script>
<button
class="btn btn-{variant}"
class:loading={isLoading}
class:full-width={isFullWidth}
>
{isLoading ? "Loading..." : "Submit"}
</button>
<style>
.btn { padding: 10px 20px; }
.btn-primary { background: blue; color: white; }
.btn-secondary { background: gray; color: white; }
.btn-danger { background: red; color: white; }
.loading { opacity: 0.7; pointer-events: none; }
.full-width { width: 100%; }
</style>
Class Directive with Expressions
The condition can be any JavaScript expression:
<script lang="ts">
let count: number = $state(0);
let status: string = $state("pending");
let items: string[] = $state(["a", "b"]);
</script>
<!-- Comparison expressions -->
<div class:warning={count > 5}>Count warning</div>
<div class:danger={count > 10}>Count danger</div>
<!-- Equality checks -->
<div class:success={status === "completed"}>Status</div>
<div class:pending={status === "pending"}>Status</div>
<!-- Array/Object checks -->
<div class:empty={items.length === 0}>Items</div>
<div class:has-items={items.length > 0}>Items</div>
<!-- Logical expressions -->
<div class:special={count > 5 && status === "completed"}>
Special
</div>
Dynamic Class Names
Build class names dynamically using template literals:
<script lang="ts">
let size: "sm" | "md" | "lg" = $state("md");
let color: "red" | "green" | "blue" = $state("blue");
let theme: "light" | "dark" = $state("light");
</script>
<button class="btn btn-{size} btn-{color}">
Dynamic Size & Color
</button>
<div class="container theme-{theme}">
Themed Container
</div>
<!-- With conditional fallback -->
<div class="icon icon-{status || 'default'}">
Icon
</div>
<style>
.btn-sm { padding: 5px 10px; font-size: 0.8rem; }
.btn-md { padding: 10px 20px; font-size: 1rem; }
.btn-lg { padding: 15px 30px; font-size: 1.2rem; }
.btn-red { background: red; }
.btn-green { background: green; }
.btn-blue { background: blue; }
.theme-light { background: white; color: black; }
.theme-dark { background: #333; color: white; }
</style>
Class Object Pattern
Create a utility function for complex class logic:
<script lang="ts">
// Utility function to build class string from object
function classNames(classes: Record<string, boolean>): string {
return Object.entries(classes)
.filter(([, value]) => value)
.map(([key]) => key)
.join(" ");
}
let isActive: boolean = $state(true);
let isDisabled: boolean = $state(false);
let size: string = $state("large");
let buttonClasses = $derived(classNames({
"btn": true,
"btn-active": isActive,
"btn-disabled": isDisabled,
"btn-large": size === "large",
"btn-small": size === "small"
}));
</script>
<button class={buttonClasses}>
Dynamic Classes
</button>
Using clsx or classnames Library
For complex class logic, use a utility library like clsx:
npm install clsx
<script lang="ts">
import clsx from "clsx";
let isActive: boolean = $state(false);
let isDisabled: boolean = $state(false);
let variant: "primary" | "secondary" = $state("primary");
let classes = $derived(clsx(
"btn",
`btn-${variant}`,
{
"active": isActive,
"disabled": isDisabled
}
));
</script>
<button class={classes}>
Using clsx
</button>
clsx handles strings, objects, arrays, and falsy values gracefully.
Classes in Loops
Apply conditional classes to items in a loop:
<script lang="ts">
interface Item {
id: number;
name: string;
completed: boolean;
priority: "low" | "medium" | "high";
}
let items: Item[] = $state([
{ id: 1, name: "Task 1", completed: false, priority: "high" },
{ id: 2, name: "Task 2", completed: true, priority: "low" },
{ id: 3, name: "Task 3", completed: false, priority: "medium" }
]);
let selectedId: number | null = $state(null);
</script>
<ul>
{#each items as item (item.id)}
<li
class="item priority-{item.priority}"
class:completed={item.completed}
class:selected={selectedId === item.id}
onclick={() => selectedId = item.id}
>
{item.name}
</li>
{/each}
</ul>
<style>
.item { padding: 10px; cursor: pointer; }
.completed { text-decoration: line-through; opacity: 0.6; }
.selected { background-color: #e0e0e0; }
.priority-low { border-left: 3px solid green; }
.priority-medium { border-left: 3px solid orange; }
.priority-high { border-left: 3px solid red; }
</style>
Global Classes vs Scoped Classes
Svelte styles are scoped by default — they only apply to the current component.
Use :global() to apply styles globally:
<script lang="ts">
let isDark: boolean = $state(false);
</script>
<div class="container" class:dark={isDark}>
<p>This is styled locally.</p>
</div>
<style>
/* Scoped to this component */
.container {
padding: 20px;
background: white;
}
.dark {
background: #333;
color: white;
}
/* Global style - affects all elements */
:global(.global-class) {
font-weight: bold;
}
/* Global modifier within scoped context */
.container :global(.highlight) {
background-color: yellow;
}
</style>
Passing Classes to Child Components
Accept a class prop to allow parent customization:
<!-- Button.svelte -->
<script lang="ts">
interface Props {
variant?: "primary" | "secondary";
class?: string;
}
let { variant = "primary", class: className = "" }: Props = $props();
</script>
<button class="btn btn-{variant} {className}">
<slot />
</button>
<style>
.btn { padding: 10px 20px; border: none; cursor: pointer; }
.btn-primary { background: blue; color: white; }
.btn-secondary { background: gray; color: white; }
</style>
<!-- Parent.svelte -->
<script lang="ts">
import Button from "./Button.svelte";
</script>
<Button class="my-custom-class">Click me</Button>
<Button variant="secondary" class="full-width">Submit</Button>
<style>
:global(.my-custom-class) {
margin: 10px;
}
:global(.full-width) {
width: 100%;
}
</style>
Using $$restProps for Class Forwarding (Svelte 4)
Forward all attributes including class:
<!-- Input.svelte (Svelte 4) -->
<script lang="ts">
export let label: string = "";
</script>
<label>
{label}
<input {...$$restProps} />
</label>
<!-- Usage -->
<Input label="Email" class="form-input" type="email" placeholder="Enter email" />
Using Rest Props for Class Forwarding (Svelte 5)
In Svelte 5, use rest props destructuring:
<!-- Input.svelte (Svelte 5) -->
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
interface Props extends HTMLInputAttributes {
label?: string;
}
let { label = "", ...rest }: Props = $props();
</script>
<label>
{label}
<input {...rest} />
</label>
<!-- Usage -->
<Input label="Email" class="form-input" type="email" placeholder="Enter email" />
Animated Class Transitions
Combine class toggling with CSS transitions:
<script lang="ts">
let expanded: boolean = $state(false);
let visible: boolean = $state(true);
</script>
<button onclick={() => expanded = !expanded}>
Toggle Expand
</button>
<div class="box" class:expanded>
<p>Content here</p>
</div>
<button onclick={() => visible = !visible}>
Toggle Visibility
</button>
<div class="fade-box" class:visible>
<p>Fading content</p>
</div>
<style>
.box {
height: 50px;
overflow: hidden;
background: #f0f0f0;
transition: height 0.3s ease;
}
.box.expanded {
height: 200px;
}
.fade-box {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-box.visible {
opacity: 1;
transform: translateY(0);
}
</style>
Class Directive Summary
Syntax
Description
Example
class="name"
Static class
<div class="container">
class={expr}
Dynamic class string
<div class={className}>
class="a {b}"
Mixed static + dynamic
<div class="btn {variant}">
class:name={cond}
Conditional class
<div class:active={isActive}>
class:name
Shorthand (name === var)
<div class:active>
Multiple class:
Multiple conditionals
<div class:a class:b class:c>
:global()
Escape scoping
:global(.class) { }
Svelte Actions
What Are Actions?
Actions are functions that run when an element is mounted to the DOM.
They provide a way to add reusable behavior to elements without creating wrapper components.
Actions are applied using the use: directive.
Basic Action Syntax
An action is a function that receives the element as its first argument:
<script lang="ts">
function greet(node: HTMLElement): void {
console.log("Element mounted:", node);
}
</script>
<div use:greet>
Hello, World!
</div>
The function runs once when the element is added to the DOM.
Action with Cleanup
Return an object with a destroy method to clean up when the element is removed:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
function logger(node: HTMLElement): ActionReturn {
console.log("Mounted:", node);
return {
destroy() {
console.log("Destroyed:", node);
}
};
}
</script>
<div use:logger>
Watch the console
</div>
The destroy function is called when the element is removed from the DOM.
Use it to remove event listeners, clear timers, or clean up resources.
Action with Parameters
Actions can accept parameters as the second argument:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface TooltipParams {
text: string;
position?: "top" | "bottom" | "left" | "right";
}
function tooltip(node: HTMLElement, params: TooltipParams): ActionReturn<TooltipParams> {
const { text, position = "top" } = params;
node.setAttribute("title", text);
node.setAttribute("data-position", position);
console.log(`Tooltip: "${text}" at ${position}`);
return {
destroy() {
node.removeAttribute("title");
node.removeAttribute("data-position");
}
};
}
</script>
<button use:tooltip={{ text: "Click to submit", position: "bottom" }}>
Submit
</button>
<button use:tooltip={{ text: "Cancel operation" }}>
Cancel
</button>
Action with Update
Return an update method to react when parameters change:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface ColorParams {
color: string;
}
function backgroundColor(node: HTMLElement, params: ColorParams): ActionReturn<ColorParams> {
node.style.backgroundColor = params.color;
return {
update(newParams: ColorParams) {
node.style.backgroundColor = newParams.color;
},
destroy() {
node.style.backgroundColor = "";
}
};
}
const color: string = $state("#ff0000");
</script>
<input type="color" bind:value={color} />
<div use:backgroundColor={{ color }} style="padding: 20px;">
Dynamic background color
</div>
The update function is called whenever the parameters change.
Typing Actions with ActionReturn
Use the ActionReturn type for proper TypeScript support:
import type { ActionReturn } from "svelte/action";
// Action without parameters
function simpleAction(node: HTMLElement): ActionReturn {
return {
destroy() {}
};
}
// Action with required parameters
function paramAction(node: HTMLElement, params: string): ActionReturn<string> {
return {
update(newParams: string) {},
destroy() {}
};
}
// Action with optional parameters
function optionalAction(node: HTMLElement, params?: number): ActionReturn<number | undefined> {
return {
update(newParams?: number) {},
destroy() {}
};
}
Using the Action Type
You can also use the Action type to define action signatures:
import type { Action } from "svelte/action";
interface FocusTrapParams {
enabled?: boolean;
}
const focusTrap: Action<HTMLElement, FocusTrapParams> = (node, params) => {
// Implementation
return {
update(newParams) {
// Handle updates
},
destroy() {
// Cleanup
}
};
};
Click Outside Action
A common action to detect clicks outside an element:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface ClickOutsideParams {
onClickOutside: () => void;
}
function clickOutside(node: HTMLElement, params: ClickOutsideParams): ActionReturn<ClickOutsideParams> {
let { onClickOutside } = params;
function handleClick(event: MouseEvent): void {
if (!node.contains(event.target as Node)) {
onClickOutside();
}
}
document.addEventListener("click", handleClick, true);
return {
update(newParams: ClickOutsideParams) {
onClickOutside = newParams.onClickOutside;
},
destroy() {
document.removeEventListener("click", handleClick, true);
}
};
}
let showDropdown: boolean = $state(false);
</script>
<div class="dropdown-container">
<button onclick={() => showDropdown = !showDropdown}>
Toggle Dropdown
</button>
{#if showDropdown}
<div
class="dropdown"
use:clickOutside={{ onClickOutside: () => showDropdown = false }}
>
<p>Dropdown content</p>
<p>Click outside to close</p>
</div>
{/if}
</div>
<style>
.dropdown-container { position: relative; }
.dropdown {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ccc;
padding: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
Auto Focus Action
Automatically focus an element when mounted:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface FocusParams {
delay?: number;
select?: boolean;
}
function autoFocus(node: HTMLElement, params: FocusParams = {}): ActionReturn<FocusParams> {
const { delay = 0, select = false } = params;
const timeoutId = setTimeout(() => {
node.focus();
if (select && node instanceof HTMLInputElement) {
node.select();
}
}, delay);
return {
destroy() {
clearTimeout(timeoutId);
}
};
}
let showModal: boolean = $state(false);
</script>
<button onclick={() => showModal = true}>Open Modal</button>
{#if showModal}
<div class="modal">
<input
use:autoFocus={{ select: true }}
value="Selected text"
/>
<button onclick={() => showModal = false}>Close</button>
</div>
{/if}
Longpress Action
Detect long press gestures:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface LongpressParams {
duration?: number;
onLongpress: () => void;
}
function longpress(node: HTMLElement, params: LongpressParams): ActionReturn<LongpressParams> {
let { duration = 500, onLongpress } = params;
let timeoutId: ReturnType<typeof setTimeout>;
function handleMouseDown(): void {
timeoutId = setTimeout(() => {
onLongpress();
}, duration);
}
function handleMouseUp(): void {
clearTimeout(timeoutId);
}
node.addEventListener("mousedown", handleMouseDown);
node.addEventListener("mouseup", handleMouseUp);
node.addEventListener("mouseleave", handleMouseUp);
return {
update(newParams: LongpressParams) {
duration = newParams.duration ?? 500;
onLongpress = newParams.onLongpress;
},
destroy() {
clearTimeout(timeoutId);
node.removeEventListener("mousedown", handleMouseDown);
node.removeEventListener("mouseup", handleMouseUp);
node.removeEventListener("mouseleave", handleMouseUp);
}
};
}
let message: string = $state("Press and hold the button");
</script>
<button use:longpress={{ duration: 1000, onLongpress: () => message = "Longpress detected!" }}>
Hold me
</button>
<p>{message}</p>
Lazy Load Image Action
Lazy load images using Intersection Observer:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface LazyLoadParams {
src: string;
placeholder?: string;
}
function lazyLoad(node: HTMLImageElement, params: LazyLoadParams): ActionReturn<LazyLoadParams> {
let { src, placeholder = "" } = params;
// Set placeholder initially
if (placeholder) {
node.src = placeholder;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
node.src = src;
observer.unobserve(node);
}
});
},
{ rootMargin: "50px" }
);
observer.observe(node);
return {
update(newParams: LazyLoadParams) {
src = newParams.src;
if (newParams.placeholder) {
placeholder = newParams.placeholder;
}
},
destroy() {
observer.disconnect();
}
};
}
const images: string[] = [
"https://picsum.photos/400/300?1",
"https://picsum.photos/400/300?2",
"https://picsum.photos/400/300?3"
];
</script>
{#each images as src, i}
<img
use:lazyLoad={{ src, placeholder: "placeholder.jpg" }}
alt="Lazy loaded image {i + 1}"
width="400"
height="300"
/>
{/each}
Portal Action
Move an element to a different location in the DOM:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface PortalParams {
target?: string | HTMLElement;
}
function portal(node: HTMLElement, params: PortalParams = {}): ActionReturn<PortalParams> {
let targetEl: HTMLElement;
function update(newParams: PortalParams): void {
const { target = "body" } = newParams;
if (typeof target === "string") {
targetEl = document.querySelector(target) ?? document.body;
} else {
targetEl = target;
}
targetEl.appendChild(node);
}
update(params);
return {
update,
destroy() {
node.remove();
}
};
}
let showModal: boolean = $state(false);
</script>
<button onclick={() => showModal = true}>Open Modal</button>
{#if showModal}
<div class="modal-overlay" use:portal>
<div class="modal-content">
<h2>Modal Title</h2>
<p>This is rendered at the end of body.</p>
<button onclick={() => showModal = false}>Close</button>
</div>
</div>
{/if}
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
Resize Observer Action
Track element size changes:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface ResizeParams {
onResize: (width: number, height: number) => void;
}
function resize(node: HTMLElement, params: ResizeParams): ActionReturn<ResizeParams> {
let { onResize } = params;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
onResize(width, height);
}
});
observer.observe(node);
return {
update(newParams: ResizeParams) {
onResize = newParams.onResize;
},
destroy() {
observer.disconnect();
}
};
}
let width: number = $state(0);
let height: number = $state(0);
</script>
<div
class="resizable"
use:resize={{ onResize: (w, h) => { width = w; height = h; } }}
>
<p>Resize me!</p>
<p>{width.toFixed(0)} x {height.toFixed(0)}</p>
</div>
<style>
.resizable {
resize: both;
overflow: auto;
border: 2px solid #333;
padding: 20px;
min-width: 100px;
min-height: 100px;
}
</style>
Intersection Observer Action
Detect when an element enters or leaves the viewport:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface InViewParams {
onEnter?: () => void;
onLeave?: () => void;
threshold?: number;
once?: boolean;
}
function inView(node: HTMLElement, params: InViewParams = {}): ActionReturn<InViewParams> {
let { onEnter, onLeave, threshold = 0, once = false } = params;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
onEnter?.();
if (once) {
observer.unobserve(node);
}
} else {
onLeave?.();
}
});
},
{ threshold }
);
observer.observe(node);
return {
update(newParams: InViewParams) {
onEnter = newParams.onEnter;
onLeave = newParams.onLeave;
},
destroy() {
observer.disconnect();
}
};
}
let isVisible: boolean = $state(false);
</script>
<div style="height: 150vh;">
<p>Scroll down...</p>
</div>
<div
class="observed"
class:visible={isVisible}
use:inView={{
onEnter: () => isVisible = true,
onLeave: () => isVisible = false,
threshold: 0.5
}}
>
I'm being observed!
</div>
<style>
.observed {
padding: 40px;
background: #eee;
transition: background 0.3s;
}
.observed.visible {
background: #4CAF50;
color: white;
}
</style>
Third-Party Library Integration
Actions are ideal for integrating third-party libraries:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
// Assuming tippy.js is installed
// import tippy, { type Instance, type Props } from "tippy.js";
interface TippyParams {
content: string;
placement?: "top" | "bottom" | "left" | "right";
}
function tippyAction(node: HTMLElement, params: TippyParams): ActionReturn<TippyParams> {
// const instance: Instance = tippy(node, {
// content: params.content,
// placement: params.placement ?? "top"
// });
// Simulated for demonstration
node.title = params.content;
return {
update(newParams: TippyParams) {
// instance.setContent(newParams.content);
// instance.setProps({ placement: newParams.placement });
node.title = newParams.content;
},
destroy() {
// instance.destroy();
node.removeAttribute("title");
}
};
}
</script>
<button use:tippyAction={{ content: "Hello Tooltip!", placement: "bottom" }}>
Hover me
</button>
Copy to Clipboard Action
Copy text to clipboard on click:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface CopyParams {
text: string;
onCopy?: () => void;
onError?: (error: Error) => void;
}
function copy(node: HTMLElement, params: CopyParams): ActionReturn<CopyParams> {
let { text, onCopy, onError } = params;
async function handleClick(): Promise<void> {
try {
await navigator.clipboard.writeText(text);
onCopy?.();
} catch (err) {
onError?.(err as Error);
}
}
node.addEventListener("click", handleClick);
return {
update(newParams: CopyParams) {
text = newParams.text;
onCopy = newParams.onCopy;
onError = newParams.onError;
},
destroy() {
node.removeEventListener("click", handleClick);
}
};
}
let copied: boolean = $state(false);
const textToCopy: string = "Hello, World!";
function handleCopy(): void {
copied = true;
setTimeout(() => copied = false, 2000);
}
</script>
<code>{textToCopy}</code>
<button use:copy={{ text: textToCopy, onCopy: handleCopy }}>
{copied ? "Copied!" : "Copy"}
</button>
Shortcut / Hotkey Action
Add keyboard shortcuts to elements:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
interface ShortcutParams {
key: string;
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
onTrigger: () => void;
}
function shortcut(node: HTMLElement, params: ShortcutParams): ActionReturn<ShortcutParams> {
let { key, ctrl = false, shift = false, alt = false, onTrigger } = params;
function handleKeyDown(event: KeyboardEvent): void {
if (
event.key.toLowerCase() === key.toLowerCase() &&
event.ctrlKey === ctrl &&
event.shiftKey === shift &&
event.altKey === alt
) {
event.preventDefault();
onTrigger();
}
}
window.addEventListener("keydown", handleKeyDown);
return {
update(newParams: ShortcutParams) {
key = newParams.key;
ctrl = newParams.ctrl ?? false;
shift = newParams.shift ?? false;
alt = newParams.alt ?? false;
onTrigger = newParams.onTrigger;
},
destroy() {
window.removeEventListener("keydown", handleKeyDown);
}
};
}
let count: number = $state(0);
</script>
<div use:shortcut={{ key: "k", ctrl: true, onTrigger: () => count++ }}>
<p>Press Ctrl+K to increment: {count}</p>
</div>
Multiple Actions on One Element
Apply multiple actions to a single element:
<script lang="ts">
import type { ActionReturn } from "svelte/action";
function logMount(node: HTMLElement): ActionReturn {
console.log("Mounted");
return { destroy() { console.log("Destroyed"); } };
}
function addBorder(node: HTMLElement, color: string): ActionReturn<string> {
node.style.border = `2px solid ${color}`;
return {
update(newColor: string) {
node.style.border = `2px solid ${newColor}`;
},
destroy() {
node.style.border = "";
}
};
}
function autoFocus(node: HTMLElement): ActionReturn {
node.focus();
return {};
}
</script>
<input
use:logMount
use:addBorder={"blue"}
use:autoFocus
placeholder="Multiple actions"
/>
Actions Summary
Feature
Description
Syntax
Basic action
Function runs on mount
use:action
With parameter
Pass data to action
use:action={params}
destroy
Cleanup on unmount
return { destroy() {} }
update
React to param changes
return { update(p) {} }
Type (function)
Action return type
ActionReturn<Params>
Type (signature)
Full action type
Action<Element, Params>
Multiple actions
Combine on one element
use:a use:b use:c
Svelte Transitions
What Are Transitions?
Transitions are animations that play when elements are added to or removed from the DOM.
Svelte provides built-in transitions and the ability to create custom ones.
Transitions make UI changes feel smooth and polished.
Key concepts:
transition: — Plays on both enter and exit
in: — Plays only on enter
out: — Plays only on exit
Built-in Transitions
Svelte provides several built-in transitions in svelte/transition:
Transition
Description
fade
Fades opacity in/out
blur
Fades with blur effect
fly
Flies in from a direction
slide
Slides vertically
scale
Scales size in/out
draw
Draws SVG paths
crossfade
Morphs between elements
Fade Transition
The simplest transition — fades opacity:
<script lang="ts">
import { fade } from "svelte/transition";
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<p transition:fade>
This fades in and out
</p>
{/if}
<script lang="ts">
import { fade } from "svelte/transition";
let visible: boolean = $state(true);
</script>
{#if visible}
<p transition:fade={{ duration: 500, delay: 100 }}>
Slower fade with delay
</p>
{/if}
Fly Transition
Animates position and opacity:
<script lang="ts">
import { fly } from "svelte/transition";
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<!-- Fly from top -->
<p transition:fly={{ y: -50, duration: 300 }}>
Flies from top
</p>
<!-- Fly from left -->
<p transition:fly={{ x: -200, duration: 300 }}>
Flies from left
</p>
<!-- Fly with opacity control -->
<p transition:fly={{ y: 100, opacity: 0.5, duration: 400 }}>
Partial opacity fly
</p>
{/if}
Parameters: x, y, opacity, duration, delay, easing
Slide Transition
Slides element vertically (collapses height):
<script lang="ts">
import { slide } from "svelte/transition";
let expanded: boolean = $state(false);
</script>
<button onclick={() => expanded = !expanded}>
{expanded ? "Collapse" : "Expand"}
</button>
{#if expanded}
<div transition:slide={{ duration: 300 }}>
<p>This content slides in and out.</p>
<p>It animates the height property.</p>
<p>Great for accordions and dropdowns.</p>
</div>
{/if}
With axis parameter (Svelte 4+):
<script lang="ts">
import { slide } from "svelte/transition";
let visible: boolean = $state(true);
</script>
{#if visible}
<!-- Horizontal slide -->
<div transition:slide={{ axis: "x", duration: 300 }}>
Slides horizontally
</div>
{/if}
Scale Transition
<script lang="ts">
import { scale } from "svelte/transition";
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<div transition:scale={{ duration: 300 }}>
Scales in and out
</div>
<div transition:scale={{ start: 0.5, opacity: 0.5 }}>
Starts at 50% size
</div>
{/if}
Parameters: start (initial scale), opacity, duration, delay, easing
Blur Transition
Fades with a blur effect:
<script lang="ts">
import { blur } from "svelte/transition";
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<p transition:blur={{ amount: 10, duration: 400 }}>
Blurs in and out
</p>
{/if}
Parameters: amount (blur pixels), opacity, duration, delay, easing
Draw Transition (SVG)
Animates SVG path stroke:
<script lang="ts">
import { draw } from "svelte/transition";
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<svg viewBox="0 0 100 100" width="200" height="200">
<path
transition:draw={{ duration: 1000 }}
d="M10,50 Q50,10 90,50 Q50,90 10,50"
fill="none"
stroke="blue"
stroke-width="2"
/>
</svg>
{/if}
Parameters: speed (pixels per second) or duration, delay, easing
Separate In and Out Transitions
Use different transitions for entering and exiting:
<script lang="ts">
import { fly, fade, scale } from "svelte/transition";
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<!-- Different transitions for in/out -->
<p in:fly={{ y: -50 }} out:fade>
Flies in, fades out
</p>
<p in:scale out:fly={{ x: 200 }}>
Scales in, flies out to the right
</p>
{/if}
Transition Parameters
Common parameters available to all transitions:
Parameter
Type
Description
delay
number
Milliseconds before starting
duration
number
Length in milliseconds
easing
function
Easing function
<script lang="ts">
import { fade } from "svelte/transition";
import { cubicOut, elasticOut, bounceOut } from "svelte/easing";
let visible: boolean = $state(true);
</script>
{#if visible}
<p transition:fade={{ duration: 300, delay: 100, easing: cubicOut }}>
Custom easing
</p>
{/if}
Easing Functions
Svelte provides many easing functions in svelte/easing:
import {
// Linear
linear,
// Sine
sineIn, sineOut, sineInOut,
// Quad (power of 2)
quadIn, quadOut, quadInOut,
// Cubic (power of 3)
cubicIn, cubicOut, cubicInOut,
// Quart (power of 4)
quartIn, quartOut, quartInOut,
// Quint (power of 5)
quintIn, quintOut, quintInOut,
// Expo (exponential)
expoIn, expoOut, expoInOut,
// Circ (circular)
circIn, circOut, circInOut,
// Back (overshoots)
backIn, backOut, backInOut,
// Elastic (springy)
elasticIn, elasticOut, elasticInOut,
// Bounce
bounceIn, bounceOut, bounceInOut
} from "svelte/easing";
<script lang="ts">
import { fly } from "svelte/transition";
import { elasticOut, bounceOut } from "svelte/easing";
let visible: boolean = $state(true);
</script>
{#if visible}
<div transition:fly={{ y: -100, easing: elasticOut, duration: 800 }}>
Elastic entrance!
</div>
<div transition:fly={{ y: 100, easing: bounceOut, duration: 600 }}>
Bouncy entrance!
</div>
{/if}
Transition Events
Listen to transition lifecycle events:
<script lang="ts">
import { fade } from "svelte/transition";
let visible: boolean = $state(true);
let status: string = $state("");
function handleIntroStart(): void {
status = "Intro started";
}
function handleIntroEnd(): void {
status = "Intro ended";
}
function handleOutroStart(): void {
status = "Outro started";
}
function handleOutroEnd(): void {
status = "Outro ended";
}
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
<p>Status: {status}</p>
{#if visible}
<div
transition:fade={{ duration: 500 }}
onintrostart={handleIntroStart}
onintroend={handleIntroEnd}
onoutrostart={handleOutroStart}
onoutroend={handleOutroEnd}
>
Watch the status!
</div>
{/if}
Events: introstart, introend, outrostart, outroend
Local Transitions
By default, transitions play when any parent block is added/removed.
Use local modifier to only play when the element's direct block changes:
<script lang="ts">
import { slide, fade } from "svelte/transition";
let showList: boolean = $state(true);
let items: string[] = $state(["Item 1", "Item 2", "Item 3"]);
function addItem(): void {
items = [...items, `Item ${items.length + 1}`];
}
function removeItem(index: number): void {
items = items.filter((_, i) => i !== index);
}
</script>
<button onclick={() => showList = !showList}>
Toggle List
</button>
<button onclick={addItem}>Add Item</button>
{#if showList}
<ul transition:fade>
{#each items as item, i (item)}
<!-- |local prevents animation when parent toggles -->
<li transition:slide|local>
{item}
<button onclick={() => removeItem(i)}>×</button>
</li>
{/each}
</ul>
{/if}
Without |local, items would animate when toggling the entire list.
With |local, items only animate when individually added/removed.
Global Modifier
The |global modifier ensures transitions play even during initial page load:
<script lang="ts">
import { fade } from "svelte/transition";
</script>
<!-- Plays transition on initial render -->
<div transition:fade|global={{ duration: 1000 }}>
Fades in on page load
</div>
Crossfade Transition
Creates a morphing effect between two elements:
<script lang="ts">
import { crossfade } from "svelte/transition";
import { quintOut } from "svelte/easing";
const [send, receive] = crossfade({
duration: 400,
easing: quintOut,
fallback(node) {
// Fallback when no matching element exists
return {
duration: 300,
css: (t: number) => `opacity: ${t}`
};
}
});
interface Todo {
id: number;
text: string;
done: boolean;
}
let todos: Todo[] = $state([
{ id: 1, text: "Learn Svelte", done: false },
{ id: 2, text: "Build an app", done: false },
{ id: 3, text: "Deploy", done: true }
]);
function toggle(id: number): void {
todos = todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
let pending = $derived(todos.filter(t => !t.done));
let completed = $derived(todos.filter(t => t.done));
</script>
<div class="columns">
<div class="column">
<h3>Pending</h3>
{#each pending as todo (todo.id)}
<div
class="todo"
in:receive={{ key: todo.id }}
out:send={{ key: todo.id }}
onclick={() => toggle(todo.id)}
>
{todo.text}
</div>
{/each}
</div>
<div class="column">
<h3>Completed</h3>
{#each completed as todo (todo.id)}
<div
class="todo done"
in:receive={{ key: todo.id }}
out:send={{ key: todo.id }}
onclick={() => toggle(todo.id)}
>
{todo.text}
</div>
{/each}
</div>
</div>
<style>
.columns { display: flex; gap: 20px; }
.column { flex: 1; }
.todo { padding: 10px; margin: 5px 0; background: #eee; cursor: pointer; }
.todo.done { background: #c8e6c9; }
</style>
crossfade returns [send, receive] pair.
Elements with matching key morph into each other.
Custom Transitions
Create custom transitions by returning a configuration object:
<script lang="ts">
import type { TransitionConfig } from "svelte/transition";
interface SpinParams {
duration?: number;
rotations?: number;
}
function spin(node: HTMLElement, params: SpinParams = {}): TransitionConfig {
const { duration = 500, rotations = 1 } = params;
return {
duration,
css: (t: number) => {
const rotation = t * rotations * 360;
const scale = t;
return `
transform: rotate(${rotation}deg) scale(${scale});
opacity: ${t};
`;
}
};
}
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<div transition:spin={{ duration: 800, rotations: 2 }}>
Spinning!
</div>
{/if}
Custom Transition with tick
Use tick for JavaScript-based animations (when CSS isn't enough):
<script lang="ts">
import type { TransitionConfig } from "svelte/transition";
interface TypewriterParams {
speed?: number;
}
function typewriter(node: HTMLElement, params: TypewriterParams = {}): TransitionConfig {
const { speed = 50 } = params;
const text = node.textContent ?? "";
const duration = text.length * speed;
return {
duration,
tick: (t: number) => {
const length = Math.floor(text.length * t);
node.textContent = text.slice(0, length);
}
};
}
let visible: boolean = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<p transition:typewriter={{ speed: 30 }}>
This text appears character by character...
</p>
{/if}
css is preferred for performance (runs on GPU).
tick is for animations that can't be done with CSS.
Transition Return Object
Custom transitions return a TransitionConfig object:
interface TransitionConfig {
delay?: number; // Delay before starting
duration?: number; // Animation length in ms
easing?: (t: number) => number; // Easing function
css?: (t: number, u: number) => string; // CSS generator
tick?: (t: number, u: number) => void; // JS callback
}
// t: 0 to 1 (intro) or 1 to 0 (outro) - the progress
// u: 1 - t (the inverse)
<script lang="ts">
import type { TransitionConfig } from "svelte/transition";
import { cubicOut } from "svelte/easing";
function customFade(node: HTMLElement): TransitionConfig {
return {
delay: 100,
duration: 400,
easing: cubicOut,
css: (t: number, u: number) => `
opacity: ${t};
transform: translateY(${u * 20}px);
`
};
}
let visible: boolean = $state(true);
</script>
{#if visible}
<div transition:customFade>
Custom fade transition
</div>
{/if}
Deferred Transitions
Return a function to defer transition creation:
<script lang="ts">
import type { TransitionConfig } from "svelte/transition";
function deferredSlide(node: HTMLElement): () => TransitionConfig {
// Measure height when transition starts, not when component mounts
return () => {
const height = node.offsetHeight;
return {
duration: 300,
css: (t: number) => `
height: ${t * height}px;
overflow: hidden;
`
};
};
}
let visible: boolean = $state(true);
</script>
{#if visible}
<div transition:deferredSlide>
Content with dynamic height
</div>
{/if}
Staggered List Transitions
Create staggered animations using index-based delays:
<script lang="ts">
import { fly } from "svelte/transition";
let visible: boolean = $state(true);
const items: string[] = [
"First item",
"Second item",
"Third item",
"Fourth item",
"Fifth item"
];
</script>
<button onclick={() => visible = !visible}>
Toggle List
</button>
{#if visible}
<ul>
{#each items as item, i}
<li
in:fly={{ y: 20, delay: i * 100, duration: 300 }}
out:fly={{ y: -20, delay: (items.length - i - 1) * 50, duration: 200 }}
>
{item}
</li>
{/each}
</ul>
{/if}
Combining Transitions with CSS
Transitions work alongside CSS transitions:
<script lang="ts">
import { fade } from "svelte/transition";
let visible: boolean = $state(true);
let hovered: boolean = $state(false);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<div
class="box"
class:hovered
transition:fade={{ duration: 300 }}
onmouseenter={() => hovered = true}
onmouseleave={() => hovered = false}
>
Hover me!
</div>
{/if}
<style>
.box {
padding: 20px;
background: #3498db;
color: white;
transition: transform 0.2s, background 0.2s;
}
.box.hovered {
transform: scale(1.05);
background: #2980b9;
}
</style>
Transition with {#key}
Use {#key} to trigger transitions when a value changes:
<script lang="ts">
import { fade, fly } from "svelte/transition";
let count: number = $state(0);
</script>
<button onclick={() => count++}>
Increment
</button>
{#key count}
<p transition:fade={{ duration: 200 }}>
Count: {count}
</p>
{/key}
{#key count}
<div in:fly={{ y: -20 }} out:fly={{ y: 20 }}>
Value changed!
</div>
{/key}
Transitions Summary
Directive
Description
Example
transition:
In and out
transition:fade
in:
Enter only
in:fly={{ y: -50 }}
out:
Exit only
out:fade
|local
Only direct block changes
transition:slide|local
|global
Play on initial render
transition:fade|global
Events
Lifecycle hooks
onintroend={handler}
Custom
Return TransitionConfig
css: (t) => ...
Svelte Advanced Reactivity
Deep Reactivity with $state
$state creates deeply reactive state — nested objects and arrays are automatically reactive:
<script lang="ts">
interface User {
name: string;
address: {
city: string;
country: string;
};
hobbies: string[];
}
let user: User = $state({
name: "Alice",
address: {
city: "New York",
country: "USA"
},
hobbies: ["reading", "coding"]
});
function updateCity(): void {
// Deep mutation is reactive!
user.address.city = "Los Angeles";
}
function addHobby(): void {
// Array mutations are reactive!
user.hobbies.push("gaming");
}
</script>
<p>{user.name} lives in {user.address.city}</p>
<p>Hobbies: {user.hobbies.join(", ")}</p>
<button onclick={updateCity}>Move to LA</button>
<button onclick={addHobby}>Add Hobby</button>
Unlike Svelte 4, you don't need to reassign to trigger updates.
Shallow Reactivity with $state.raw
Use $state.raw when you don't need deep reactivity:
<script lang="ts">
interface LargeDataset {
id: number;
values: number[];
}
// Only the reference is reactive, not nested properties
let data: LargeDataset = $state.raw({
id: 1,
values: [1, 2, 3, 4, 5]
});
function updateData(): void {
// This does NOT trigger reactivity
data.values.push(6);
// This DOES trigger reactivity (replacing the whole object)
data = { ...data, values: [...data.values, 7] };
}
function replaceData(): void {
// Reassignment triggers update
data = {
id: 2,
values: [10, 20, 30]
};
}
</script>
<p>ID: {data.id}</p>
<p>Values: {data.values.join(", ")}</p>
<button onclick={updateData}>Update</button>
<button onclick={replaceData}>Replace</button>
Use $state.raw for:
Large datasets where deep reactivity is expensive
Immutable data patterns
Data that rarely changes
Getting Plain Values with $state.snapshot
$state.snapshot returns a non-reactive copy of reactive state:
<script lang="ts">
interface FormData {
name: string;
email: string;
}
let form: FormData = $state({
name: "",
email: ""
});
function handleSubmit(): void {
// Get a plain object snapshot for API calls
const snapshot = $state.snapshot(form);
console.log("Submitting:", snapshot);
console.log("Is proxy:", form !== snapshot); // true
// Send to API
fetch("/api/submit", {
method: "POST",
body: JSON.stringify(snapshot)
});
}
function logState(): void {
// Reactive proxy - may show Proxy in console
console.log("Reactive:", form);
// Plain object - cleaner for debugging
console.log("Snapshot:", $state.snapshot(form));
}
</script>
<input bind:value={form.name} placeholder="Name" />
<input bind:value={form.email} placeholder="Email" />
<button onclick={handleSubmit}>Submit</button>
<button onclick={logState}>Log State</button>
Useful when passing state to external libraries or APIs.
Computed Values with $derived
$derived creates values that automatically update when dependencies change:
<script lang="ts">
let firstName: string = $state("John");
let lastName: string = $state("Doe");
let age: number = $state(25);
// Simple derived value
let fullName: string = $derived(firstName + " " + lastName);
// Derived with transformation
let upperName: string = $derived(fullName.toUpperCase());
// Derived boolean
let isAdult: boolean = $derived(age >= 18);
// Chained derivations
let greeting: string = $derived(
`Hello, ${fullName}! You are ${isAdult ? "an adult" : "a minor"}.`
);
</script>
<input bind:value={firstName} placeholder="First name" />
<input bind:value={lastName} placeholder="Last name" />
<input type="number" bind:value={age} />
<p>Full name: {fullName}</p>
<p>Upper: {upperName}</p>
<p>{greeting}</p>
Complex Derivations with $derived.by
Use $derived.by for derivations that need multiple statements:
<script lang="ts">
interface Product {
name: string;
price: number;
quantity: number;
}
let products: Product[] = $state([
{ name: "Apple", price: 1.5, quantity: 3 },
{ name: "Banana", price: 0.75, quantity: 5 },
{ name: "Orange", price: 2.0, quantity: 2 }
]);
let taxRate: number = $state(0.08);
// Complex derivation with multiple statements
let orderSummary = $derived.by(() => {
const subtotal = products.reduce(
(sum, p) => sum + p.price * p.quantity,
0
);
const tax = subtotal * taxRate;
const total = subtotal + tax;
const itemCount = products.reduce((sum, p) => sum + p.quantity, 0);
return {
subtotal: subtotal.toFixed(2),
tax: tax.toFixed(2),
total: total.toFixed(2),
itemCount
};
});
// Derived with conditional logic
let recommendation = $derived.by(() => {
const total = parseFloat(orderSummary.total);
if (total > 50) {
return "You qualify for free shipping!";
} else if (total > 25) {
return `Add $${(50 - total).toFixed(2)} more for free shipping.`;
} else {
return "Keep shopping for great deals!";
}
});
</script>
<ul>
{#each products as product}
<li>{product.name}: ${product.price} × {product.quantity}</li>
{/each}
</ul>
<p>Items: {orderSummary.itemCount}</p>
<p>Subtotal: ${orderSummary.subtotal}</p>
<p>Tax: ${orderSummary.tax}</p>
<p>Total: ${orderSummary.total}</p>
<p>{recommendation}</p>
Side Effects with $effect
$effect runs code when reactive dependencies change:
<script lang="ts">
let count: number = $state(0);
let name: string = $state("World");
// Runs when count changes
$effect(() => {
console.log("Count is now:", count);
});
// Runs when name changes
$effect(() => {
document.title = `Hello, ${name}!`;
});
// Runs when either changes
$effect(() => {
console.log(`${name} has count ${count}`);
});
</script>
<input bind:value={name} />
<button onclick={() => count++}>Count: {count}</button>
Effects automatically track which reactive values are read.
They re-run whenever those values change.
Effect Cleanup
Return a function from $effect to clean up before re-running:
<script lang="ts">
let interval: number = $state(1000);
let count: number = $state(0);
$effect(() => {
// Set up interval with current value
const id = setInterval(() => {
count++;
}, interval);
console.log(`Started interval: ${interval}ms`);
// Cleanup: clear interval before re-running or on unmount
return () => {
clearInterval(id);
console.log("Cleared interval");
};
});
// Event listener example
let mouseX: number = $state(0);
let mouseY: number = $state(0);
$effect(() => {
function handleMouseMove(e: MouseEvent): void {
mouseX = e.clientX;
mouseY = e.clientY;
}
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
});
</script>
<p>Count: {count}</p>
<p>Mouse: {mouseX}, {mouseY}</p>
<label>
Interval (ms):
<input type="number" bind:value={interval} min="100" step="100" />
</label>
Pre-Effects with $effect.pre
$effect.pre runs before the DOM updates:
<script lang="ts">
let messages: string[] = $state(["Hello", "World"]);
let container: HTMLDivElement;
// Runs BEFORE DOM updates - useful for measuring
$effect.pre(() => {
if (container) {
// Capture scroll position before DOM changes
const isAtBottom =
container.scrollHeight - container.scrollTop === container.clientHeight;
console.log("Before update, at bottom:", isAtBottom);
}
});
// Runs AFTER DOM updates
$effect(() => {
if (container) {
// Auto-scroll to bottom after new messages
container.scrollTop = container.scrollHeight;
}
});
function addMessage(): void {
messages.push(`Message ${messages.length + 1}`);
}
</script>
<div bind:this={container} class="chat" style="height: 200px; overflow-y: auto;">
{#each messages as message}
<p>{message}</p>
{/each}
</div>
<button onclick={addMessage}>Add Message</button>
Use $effect.pre when you need to measure or capture state before the DOM changes.
Effect Tracking with $effect.tracking
Check if code is running inside a tracking context:
<script lang="ts">
let count: number = $state(0);
function logWithContext(message: string): void {
if ($effect.tracking()) {
console.log("[Tracked]", message);
} else {
console.log("[Untracked]", message);
}
}
// Inside $effect - tracked
$effect(() => {
logWithContext(`Count is ${count}`);
});
// Inside $derived - tracked
let doubled = $derived.by(() => {
logWithContext("Computing doubled");
return count * 2;
});
function handleClick(): void {
// Event handler - not tracked
logWithContext("Button clicked");
count++;
}
</script>
<p>Count: {count}, Doubled: {doubled}</p>
<button onclick={handleClick}>Increment</button>
Useful for libraries that need to behave differently in reactive vs non-reactive contexts.
Root Effects with $effect.root
$effect.root creates effects outside the component lifecycle:
<script lang="ts">
let count: number = $state(0);
let cleanupRoot: (() => void) | null = null;
function startRootEffect(): void {
// Create an effect that persists outside component lifecycle
cleanupRoot = $effect.root(() => {
$effect(() => {
console.log("[Root Effect] Count:", count);
});
// Return cleanup for the root
return () => {
console.log("[Root Effect] Cleaned up");
};
});
}
function stopRootEffect(): void {
if (cleanupRoot) {
cleanupRoot();
cleanupRoot = null;
}
}
</script>
<p>Count: {count}</p>
<button onclick={() => count++}>Increment</button>
<button onclick={startRootEffect}>Start Root Effect</button>
<button onclick={stopRootEffect}>Stop Root Effect</button>
Use $effect.root for:
Effects in shared modules
Effects with manual lifecycle control
Testing scenarios
Untracking Dependencies with untrack
Use untrack to read reactive values without creating a dependency:
<script lang="ts">
import { untrack } from "svelte";
let count: number = $state(0);
let name: string = $state("Alice");
let effectRuns: number = $state(0);
$effect(() => {
// This creates a dependency on 'count'
console.log("Count:", count);
// This does NOT create a dependency on 'name'
const currentName = untrack(() => name);
console.log("Name (untracked):", currentName);
effectRuns++;
});
// The effect only re-runs when 'count' changes, not 'name'
</script>
<p>Count: {count}</p>
<p>Name: {name}</p>
<p>Effect runs: {effectRuns}</p>
<button onclick={() => count++}>Increment Count (triggers effect)</button>
<button onclick={() => name = name + "!"}>Change Name (no effect)</button>
Useful when you need to read a value without reacting to its changes.
Debugging with $inspect
$inspect logs reactive values whenever they change:
<script lang="ts">
interface User {
name: string;
age: number;
}
let count: number = $state(0);
let user: User = $state({ name: "Alice", age: 25 });
// Basic inspection - logs on every change
$inspect(count);
// Inspect multiple values
$inspect(count, user);
// Custom logging with .with()
$inspect(user).with((type, value) => {
console.log(`[${type}]`, JSON.stringify(value, null, 2));
});
// Useful for debugging: pause on change
$inspect(count).with((type, value) => {
if (value > 5) {
debugger; // Opens browser debugger
}
});
</script>
<button onclick={() => count++}>Count: {count}</button>
<button onclick={() => user.age++}>Age: {user.age}</button>
<input bind:value={user.name} />
$inspect only works in development mode.
The type parameter is either "init" or "update".
Fine-Grained Reactivity
Svelte 5 tracks reactivity at the property level , not just the object level:
<script lang="ts">
interface Coordinates {
x: number;
y: number;
}
let coords: Coordinates = $state({ x: 0, y: 0 });
// This effect only re-runs when coords.x changes
$effect(() => {
console.log("X changed:", coords.x);
});
// This effect only re-runs when coords.y changes
$effect(() => {
console.log("Y changed:", coords.y);
});
function updateX(): void {
coords.x++; // Only triggers first effect
}
function updateY(): void {
coords.y++; // Only triggers second effect
}
</script>
<p>X: {coords.x}, Y: {coords.y}</p>
<button onclick={updateX}>Update X</button>
<button onclick={updateY}>Update Y</button>
This fine-grained tracking improves performance for complex objects.
Reactive Classes
Use $state in class fields for reactive class instances:
<script lang="ts">
class Counter {
count: number = $state(0);
increment(): void {
this.count++;
}
decrement(): void {
this.count--;
}
get doubled(): number {
return this.count * 2;
}
}
class TodoList {
items: string[] = $state([]);
add(item: string): void {
this.items.push(item);
}
remove(index: number): void {
this.items.splice(index, 1);
}
get count(): number {
return this.items.length;
}
}
const counter = new Counter();
const todos = new TodoList();
let newTodo: string = $state("");
</script>
<div>
<h3>Counter</h3>
<p>Count: {counter.count} (doubled: {counter.doubled})</p>
<button onclick={() => counter.decrement()}>-</button>
<button onclick={() => counter.increment()}>+</button>
</div>
<div>
<h3>Todos ({todos.count})</h3>
<input bind:value={newTodo} />
<button onclick={() => { todos.add(newTodo); newTodo = ""; }}>Add</button>
<ul>
{#each todos.items as item, i}
<li>{item} <button onclick={() => todos.remove(i)}>×</button></li>
{/each}
</ul>
</div>
Reactive State in Modules
Create shared reactive state in separate modules:
// stores/counter.svelte.ts
export function createCounter(initial: number = 0) {
let count = $state(initial);
return {
get count() {
return count;
},
increment() {
count++;
},
decrement() {
count--;
},
reset() {
count = initial;
}
};
}
// Singleton instance
export const counter = createCounter(0);
<!-- Component.svelte -->
<script lang="ts">
import { counter } from "./stores/counter.svelte";
</script>
<p>Count: {counter.count}</p>
<button onclick={counter.increment}>+</button>
<button onclick={counter.decrement}>-</button>
<button onclick={counter.reset}>Reset</button>
Use the .svelte.ts or .svelte.js extension for files containing runes.
Store Interoperability
Svelte 5 stores work with both old stores and new runes:
<script lang="ts">
import { writable, type Writable } from "svelte/store";
import { fromStore, toStore } from "svelte/store";
// Traditional store
const countStore: Writable<number> = writable(0);
// Convert store to rune-based state
const countState = fromStore(countStore);
// Access value (automatically subscribes)
$effect(() => {
console.log("Store value:", countState.current);
});
// Or create a store from rune state
let runeCount: number = $state(0);
const runeStore = toStore(() => runeCount, (v) => runeCount = v);
</script>
<p>Store: {$countStore}</p>
<p>State: {countState.current}</p>
<button onclick={() => countStore.update(n => n + 1)}>
Update Store
</button>
<button onclick={() => runeCount++}>
Update Rune
</button>
Avoiding Infinite Loops
Be careful not to create infinite loops in effects:
<script lang="ts">
import { untrack } from "svelte";
let count: number = $state(0);
let history: number[] = $state([]);
// BAD: Infinite loop!
// $effect(() => {
// history.push(count); // Reading 'count' AND modifying 'history'
// history = history; // This creates a loop
// });
// GOOD: Use untrack to prevent loop
$effect(() => {
const current = count; // Track count
untrack(() => {
history.push(current); // Don't track history modification
});
});
// ALTERNATIVE: Use $effect.pre or separate the concerns
let lastCount: number | null = null;
$effect(() => {
if (count !== lastCount) {
lastCount = count;
// Safe to modify other state here
}
});
</script>
<button onclick={() => count++}>Count: {count}</button>
<p>History: {history.join(", ")}</p>
Conditional Effects
Effects can have conditional logic, but dependencies are still tracked:
<script lang="ts">
let enabled: boolean = $state(true);
let count: number = $state(0);
let logs: string[] = $state([]);
$effect(() => {
// 'enabled' is always tracked
if (enabled) {
// 'count' is only tracked when this branch runs
logs.push(`Count is ${count}`);
}
});
// Better pattern: guard the entire effect
$effect(() => {
if (!enabled) return;
logs.push(`Enabled count: ${count}`);
return () => {
logs.push("Effect cleaned up");
};
});
</script>
<label>
<input type="checkbox" bind:checked={enabled} />
Enabled
</label>
<button onclick={() => count++}>Count: {count}</button>
<ul>
{#each logs as log}
<li>{log}</li>
{/each}
</ul>
Batching Updates
Svelte batches synchronous updates automatically:
<script lang="ts">
import { flushSync } from "svelte";
let a: number = $state(0);
let b: number = $state(0);
let effectRuns: number = $state(0);
$effect(() => {
console.log(`a: ${a}, b: ${b}`);
effectRuns++;
});
function batchedUpdate(): void {
// These are batched - effect runs once
a++;
b++;
}
function forcedUpdate(): void {
a++;
// Force synchronous flush
flushSync();
// This triggers a separate effect run
b++;
}
</script>
<p>a: {a}, b: {b}</p>
<p>Effect runs: {effectRuns}</p>
<button onclick={batchedUpdate}>Batched (1 effect run)</button>
<button onclick={forcedUpdate}>Forced (2 effect runs)</button>
Use flushSync sparingly — only when you need immediate DOM updates.
Advanced Reactivity Summary
API
Purpose
Usage
$state
Deep reactive state
let x = $state(value)
$state.raw
Shallow reactive state
let x = $state.raw(value)
$state.snapshot
Get plain object copy
$state.snapshot(state)
$derived
Computed value (expression)
let x = $derived(expr)
$derived.by
Computed value (function)
let x = $derived.by(() => ...)
$effect
Side effect (after DOM)
$effect(() => { ... })
$effect.pre
Side effect (before DOM)
$effect.pre(() => { ... })
$effect.tracking
Check if tracking
if ($effect.tracking()) ...
$effect.root
Manual effect lifecycle
$effect.root(() => ...)
untrack
Read without tracking
untrack(() => value)
$inspect
Debug reactive values
$inspect(value)
flushSync
Force synchronous update
flushSync()
Svelte Reusing Content
Overview
Svelte provides several ways to create reusable content within and across components.
Key concepts:
Slots — Allow parent to inject content into child components (Svelte 4)
Snippets — Reusable template chunks with parameters (Svelte 5)
@render — Render snippets and children (Svelte 5)
Basic Slots (Svelte 4)
Slots allow a parent component to pass content into a child:
<!-- Card.svelte -->
<script lang="ts">
export let title: string;
</script>
<div class="card">
<h2>{title}</h2>
<div class="content">
<slot />
</div>
</div>
<style>
.card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; }
.content { margin-top: 12px; }
</style>
<!-- Parent.svelte -->
<script lang="ts">
import Card from "./Card.svelte";
</script>
<Card title="Welcome">
<p>This content goes into the slot.</p>
<p>Multiple elements are allowed.</p>
</Card>
Default Slot Content (Svelte 4)
Provide fallback content when no slot content is provided:
<!-- Button.svelte -->
<script lang="ts">
export let variant: "primary" | "secondary" = "primary";
</script>
<button class="btn btn-{variant}">
<slot>
<!-- Default content if nothing is passed -->
Click me
</slot>
</button>
<!-- Parent.svelte -->
<script lang="ts">
import Button from "./Button.svelte";
</script>
<Button /> <!-- Shows "Click me" -->
<Button>Submit</Button> <!-- Shows "Submit" -->
<Button>Cancel</Button> <!-- Shows "Cancel" -->
Named Slots (Svelte 4)
Use named slots for multiple content areas:
<!-- Modal.svelte -->
<script lang="ts">
export let open: boolean = false;
</script>
{#if open}
<div class="modal-overlay">
<div class="modal">
<header>
<slot name="header">
<h2>Modal Title</h2>
</slot>
</header>
<main>
<slot>
<p>Modal content goes here.</p>
</slot>
</main>
<footer>
<slot name="footer">
<button on:click={() => open = false}>Close</button>
</slot>
</footer>
</div>
</div>
{/if}
<!-- Parent.svelte -->
<script lang="ts">
import Modal from "./Modal.svelte";
let showModal: boolean = false;
</script>
<button on:click={() => showModal = true}>Open Modal</button>
<Modal bind:open={showModal}>
<h2 slot="header">Confirm Action</h2>
<p>Are you sure you want to proceed?</p>
<div slot="footer">
<button on:click={() => showModal = false}>Cancel</button>
<button on:click={() => { /* confirm */ showModal = false }}>Confirm</button>
</div>
</Modal>
Slot Props (Svelte 4)
Pass data from child to parent through slot props:
<!-- List.svelte -->
<script lang="ts">
interface Item {
id: number;
name: string;
}
export let items: Item[];
</script>
<ul>
{#each items as item, index}
<li>
<slot {item} {index} isFirst={index === 0} isLast={index === items.length - 1}>
{item.name}
</slot>
</li>
{/each}
</ul>
<!-- Parent.svelte -->
<script lang="ts">
import List from "./List.svelte";
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
];
</script>
<List items={users} let:item let:index let:isFirst let:isLast>
<span class:highlight={isFirst || isLast}>
{index + 1}. {item.name}
{#if isFirst}(First){/if}
{#if isLast}(Last){/if}
</span>
</List>
Checking for Slot Content with $$slots (Svelte 4)
Check if slot content was provided:
<!-- Card.svelte -->
<script lang="ts">
export let title: string;
</script>
<div class="card">
<h2>{title}</h2>
<slot />
{#if $$slots.footer}
<hr />
<footer>
<slot name="footer" />
</footer>
{/if}
{#if $$slots.actions}
<div class="actions">
<slot name="actions" />
</div>
{/if}
</div>
<!-- Parent.svelte -->
<script lang="ts">
import Card from "./Card.svelte";
</script>
<!-- No footer - footer section won't render -->
<Card title="Simple Card">
<p>Just some content.</p>
</Card>
<!-- With footer -->
<Card title="Card with Footer">
<p>Content here.</p>
<span slot="footer">Footer text</span>
</Card>
Introduction to Snippets (Svelte 5)
Snippets are Svelte 5's way to create reusable template chunks:
<script lang="ts">
let items: string[] = $state(["Apple", "Banana", "Cherry"]);
</script>
{#snippet listItem(item: string, index: number)}
<li class="item">
<span class="index">{index + 1}.</span>
<span class="name">{item}</span>
</li>
{/snippet}
<ul>
{#each items as item, i}
{@render listItem(item, i)}
{/each}
</ul>
<style>
.item { display: flex; gap: 8px; }
.index { color: #666; }
</style>
{#snippet name(params)} defines a snippet.
{@render name(args)} renders the snippet.
Snippets with Complex Parameters
Snippets can accept any TypeScript types as parameters:
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
avatar: string;
isOnline: boolean;
}
let users: User[] = $state([
{ id: 1, name: "Alice", email: "alice@example.com", avatar: "👩", isOnline: true },
{ id: 2, name: "Bob", email: "bob@example.com", avatar: "👨", isOnline: false },
{ id: 3, name: "Charlie", email: "charlie@example.com", avatar: "🧑", isOnline: true }
]);
</script>
{#snippet userCard(user: User, showEmail: boolean = false)}
<div class="user-card">
<span class="avatar">{user.avatar}</span>
<div class="info">
<strong>{user.name}</strong>
{#if showEmail}
<small>{user.email}</small>
{/if}
</div>
<span class="status" class:online={user.isOnline}>
{user.isOnline ? "●" : "○"}
</span>
</div>
{/snippet}
<h3>Users (compact)</h3>
{#each users as user}
{@render userCard(user)}
{/each}
<h3>Users (with email)</h3>
{#each users as user}
{@render userCard(user, true)}
{/each}
Snippets for Conditional Rendering
Use snippets to avoid repeating conditional templates:
<script lang="ts">
type Status = "loading" | "success" | "error";
let status: Status = $state("loading");
let data: string | null = $state(null);
let error: string | null = $state(null);
function fetchData(): void {
status = "loading";
setTimeout(() => {
if (Math.random() > 0.3) {
data = "Here is your data!";
status = "success";
} else {
error = "Failed to fetch data";
status = "error";
}
}, 1000);
}
</script>
{#snippet loadingState()}
<div class="loading">
<span class="spinner">⏳</span>
<p>Loading...</p>
</div>
{/snippet}
{#snippet errorState(message: string)}
<div class="error">
<span class="icon">❌</span>
<p>{message}</p>
<button onclick={fetchData}>Retry</button>
</div>
{/snippet}
{#snippet successState(content: string)}
<div class="success">
<span class="icon">✅</span>
<p>{content}</p>
</div>
{/snippet}
<button onclick={fetchData}>Fetch Data</button>
{#if status === "loading"}
{@render loadingState()}
{:else if status === "error" && error}
{@render errorState(error)}
{:else if status === "success" && data}
{@render successState(data)}
{/if}
Passing Snippets to Components (Svelte 5)
Snippets can be passed as props to child components:
<!-- List.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
interface Props<T> {
items: T[];
renderItem: Snippet<[T, number]>;
emptyState?: Snippet;
}
let { items, renderItem, emptyState }: Props<unknown> = $props();
</script>
{#if items.length === 0}
{#if emptyState}
{@render emptyState()}
{:else}
<p>No items to display.</p>
{/if}
{:else}
<ul>
{#each items as item, index}
<li>
{@render renderItem(item, index)}
</li>
{/each}
</ul>
{/if}
<!-- Parent.svelte -->
<script lang="ts">
import List from "./List.svelte";
interface Product {
id: number;
name: string;
price: number;
}
let products: Product[] = $state([
{ id: 1, name: "Widget", price: 9.99 },
{ id: 2, name: "Gadget", price: 19.99 }
]);
</script>
{#snippet productItem(product: Product, index: number)}
<div class="product">
<strong>{product.name}</strong>
<span>${product.price.toFixed(2)}</span>
</div>
{/snippet}
{#snippet emptyProducts()}
<div class="empty">
<p>No products available.</p>
<button>Add Product</button>
</div>
{/snippet}
<List items={products} renderItem={productItem} emptyState={emptyProducts} />
The children Snippet (Svelte 5)
Content passed between component tags becomes the children snippet:
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
title: string;
children: Snippet;
footer?: Snippet;
}
let { title, children, footer }: Props = $props();
</script>
<div class="card">
<h2>{title}</h2>
<div class="content">
{@render children()}
</div>
{#if footer}
<div class="footer">
{@render footer()}
</div>
{/if}
</div>
<!-- Parent.svelte -->
<script lang="ts">
import Card from "./Card.svelte";
</script>
{#snippet cardFooter()}
<button>Learn More</button>
{/snippet}
<Card title="Welcome" footer={cardFooter}>
<p>This becomes the children snippet.</p>
<p>All content here is passed automatically.</p>
</Card>
Optional Children
Make children optional with a default fallback:
<!-- Button.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
onclick?: () => void;
children?: Snippet;
}
let { onclick, children }: Props = $props();
</script>
<button onclick={onclick}>
{#if children}
{@render children()}
{:else}
Click me
{/if}
</button>
<!-- Parent.svelte -->
<script lang="ts">
import Button from "./Button.svelte";
</script>
<Button /> <!-- Shows "Click me" -->
<Button>Submit</Button> <!-- Shows "Submit" -->
<Button>🚀 Launch</Button> <!-- Shows "🚀 Launch" -->
Snippets with Data from Parent (Render Props Pattern)
Pass data back to the snippet when rendering:
<!-- DataFetcher.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
interface FetchState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
interface Props<T> {
url: string;
children: Snippet<[FetchState<T>]>;
}
let { url, children }: Props<unknown> = $props();
let state: FetchState<unknown> = $state({
data: null,
loading: true,
error: null
});
$effect(() => {
state.loading = true;
state.error = null;
fetch(url)
.then(res => res.json())
.then(data => {
state.data = data;
state.loading = false;
})
.catch(err => {
state.error = err.message;
state.loading = false;
});
});
</script>
{@render children(state)}
<!-- Parent.svelte -->
<script lang="ts">
import DataFetcher from "./DataFetcher.svelte";
interface User {
id: number;
name: string;
}
</script>
<DataFetcher url="/api/users">
{#snippet children(state: { data: User[] | null; loading: boolean; error: string | null })}
{#if state.loading}
<p>Loading...</p>
{:else if state.error}
<p class="error">Error: {state.error}</p>
{:else if state.data}
<ul>
{#each state.data as user}
<li>{user.name}</li>
{/each}
</ul>
{/if}
{/snippet}
</DataFetcher>
Typing Snippets
Use the Snippet type from Svelte for proper typing:
import type { Snippet } from "svelte";
// Snippet with no parameters
type NoParams = Snippet;
// Snippet with one parameter
type SingleParam = Snippet<[string]>;
// Snippet with multiple parameters
type MultiParams = Snippet<[string, number, boolean]>;
// Snippet with object parameter
interface ItemData {
id: number;
name: string;
}
type ObjectParam = Snippet<[ItemData]>;
// Optional snippet
interface Props {
required: Snippet;
optional?: Snippet;
}
<script lang="ts">
import type { Snippet } from "svelte";
interface TableColumn<T> {
key: keyof T;
header: string;
render?: Snippet<[T[keyof T], T]>;
}
interface Props<T> {
data: T[];
columns: TableColumn<T>[];
}
let { data, columns }: Props<Record<string, unknown>> = $props();
</script>
<table>
<thead>
<tr>
{#each columns as column}
<th>{column.header}</th>
{/each}
</tr>
</thead>
<tbody>
{#each data as row}
<tr>
{#each columns as column}
<td>
{#if column.render}
{@render column.render(row[column.key], row)}
{:else}
{row[column.key]}
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
Recursive Snippets
Snippets can call themselves for recursive structures:
<script lang="ts">
interface TreeNode {
id: number;
name: string;
children?: TreeNode[];
}
let tree: TreeNode = $state({
id: 1,
name: "Root",
children: [
{
id: 2,
name: "Folder A",
children: [
{ id: 4, name: "File 1" },
{ id: 5, name: "File 2" }
]
},
{
id: 3,
name: "Folder B",
children: [
{ id: 6, name: "File 3" }
]
}
]
});
</script>
{#snippet treeNode(node: TreeNode, depth: number = 0)}
<div class="node" style="padding-left: {depth * 20}px">
<span>{node.children ? "📁" : "📄"} {node.name}</span>
</div>
{#if node.children}
{#each node.children as child}
{@render treeNode(child, depth + 1)}
{/each}
{/if}
{/snippet}
<div class="tree">
{@render treeNode(tree)}
</div>
Snippets vs Components
Use Snippets When
Use Components When
Template is only used in one component
Template is reused across multiple files
No need for separate state/lifecycle
Needs its own state management
Simple, presentational markup
Complex logic or side effects
Avoiding component overhead
Need component features (actions, bindings)
Render props / slot replacement
Standalone, testable unit
Migrating from Slots to Snippets
Comparison of Svelte 4 slots vs Svelte 5 snippets:
<!-- Svelte 4: Default slot -->
<div>
<slot />
</div>
<!-- Svelte 5: children snippet -->
<script lang="ts">
import type { Snippet } from "svelte";
let { children }: { children: Snippet } = $props();
</script>
<div>
{@render children()}
</div>
<!-- Svelte 4: Named slot -->
<header>
<slot name="header" />
</header>
<!-- Svelte 5: Named snippet prop -->
<script lang="ts">
import type { Snippet } from "svelte";
let { header }: { header?: Snippet } = $props();
</script>
<header>
{#if header}
{@render header()}
{/if}
</header>
<!-- Svelte 4: Slot props -->
<slot item={item} index={i} />
<!-- Parent usage -->
<Component let:item let:index>
{item.name} at {index}
</Component>
<!-- Svelte 5: Snippet with parameters -->
<script lang="ts">
import type { Snippet } from "svelte";
let { renderItem }: { renderItem: Snippet<[Item, number]> } = $props();
</script>
{@render renderItem(item, i)}
<!-- Parent usage -->
{#snippet itemRenderer(item: Item, index: number)}
{item.name} at {index}
{/snippet}
<Component renderItem={itemRenderer} />
Advanced: Snippet Composition
Compose snippets for flexible layouts:
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
layout?: "horizontal" | "vertical" | "grid";
items: unknown[];
renderItem: Snippet<[unknown, number]>;
header?: Snippet;
footer?: Snippet;
empty?: Snippet;
}
let {
layout = "vertical",
items,
renderItem,
header,
footer,
empty
}: Props = $props();
</script>
{#snippet wrapper(content: Snippet)}
<div class="container layout-{layout}">
{#if header}
<div class="header">
{@render header()}
</div>
{/if}
<div class="content">
{@render content()}
</div>
{#if footer}
<div class="footer">
{@render footer()}
</div>
{/if}
</div>
{/snippet}
{#snippet itemList()}
{#if items.length === 0}
{#if empty}
{@render empty()}
{:else}
<p>No items</p>
{/if}
{:else}
{#each items as item, i}
<div class="item">
{@render renderItem(item, i)}
</div>
{/each}
{/if}
{/snippet}
{@render wrapper(itemList)}
<style>
.layout-horizontal .content { display: flex; flex-direction: row; gap: 8px; }
.layout-vertical .content { display: flex; flex-direction: column; gap: 8px; }
.layout-grid .content { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
</style>
Reusing Content Summary
Feature
Svelte 4
Svelte 5
Default content
<slot />
{@render children()}
Named content
<slot name="x" />
{@render x()} prop
Pass to named
slot="x"
x={snippet}
Data to parent
<slot {data} /> + let:data
Snippet<[Data]>
Check existence
$$slots.name
{#if snippetProp}
Default fallback
Content inside <slot>
{:else} block
Local reuse
N/A
{#snippet}
Type safety
Limited
Full with Snippet<T>
Svelte Advanced Bindings
Bindable Props with $bindable (Svelte 5)
Use $bindable to make component props support two-way binding:
<!-- Counter.svelte -->
<script lang="ts">
interface Props {
count?: number;
}
let { count = $bindable(0) }: Props = $props();
function increment(): void {
count++;
}
function decrement(): void {
count--;
}
</script>
<div class="counter">
<button onclick={decrement}>-</button>
<span>{count}</span>
<button onclick={increment}>+</button>
</div>
<!-- Parent.svelte -->
<script lang="ts">
import Counter from "./Counter.svelte";
let value: number = $state(10);
</script>
<Counter bind:count={value} />
<p>Parent value: {value}</p>
<button onclick={() => value = 0}>Reset from parent</button>
Changes in either parent or child sync both ways.
The default value in $bindable(0) is used when no binding is provided.
Multiple Bindable Props
Components can have multiple bindable props:
<!-- DateTimePicker.svelte -->
<script lang="ts">
interface Props {
date?: string;
time?: string;
timezone?: string;
}
let {
date = $bindable(""),
time = $bindable(""),
timezone = $bindable("UTC")
}: Props = $props();
const timezones: string[] = ["UTC", "EST", "PST", "CET", "JST"];
</script>
<div class="datetime-picker">
<input type="date" bind:value={date} />
<input type="time" bind:value={time} />
<select bind:value={timezone}>
{#each timezones as tz}
<option value={tz}>{tz}</option>
{/each}
</select>
</div>
<!-- Parent.svelte -->
<script lang="ts">
import DateTimePicker from "./DateTimePicker.svelte";
let selectedDate: string = $state("2024-01-15");
let selectedTime: string = $state("14:30");
let selectedTimezone: string = $state("EST");
let combined = $derived(
`${selectedDate} ${selectedTime} ${selectedTimezone}`
);
</script>
<DateTimePicker
bind:date={selectedDate}
bind:time={selectedTime}
bind:timezone={selectedTimezone}
/>
<p>Selected: {combined}</p>
Bindable with Validation
Add validation logic while maintaining two-way binding:
<!-- ValidatedInput.svelte -->
<script lang="ts">
interface Props {
value?: string;
min?: number;
max?: number;
pattern?: RegExp;
}
let {
value = $bindable(""),
min = 0,
max = Infinity,
pattern
}: Props = $props();
let error: string = $state("");
function validate(newValue: string): void {
error = "";
if (newValue.length < min) {
error = `Minimum ${min} characters required`;
} else if (newValue.length > max) {
error = `Maximum ${max} characters allowed`;
} else if (pattern && !pattern.test(newValue)) {
error = "Invalid format";
}
}
// Validate on change
$effect(() => {
validate(value);
});
let isValid = $derived(error === "");
</script>
<div class="validated-input">
<input
type="text"
bind:value
class:invalid={!isValid}
/>
{#if error}
<span class="error">{error}</span>
{/if}
</div>
<style>
.invalid { border-color: red; }
.error { color: red; font-size: 0.8em; }
</style>
<!-- Parent.svelte -->
<script lang="ts">
import ValidatedInput from "./ValidatedInput.svelte";
let email: string = $state("");
let username: string = $state("");
</script>
<ValidatedInput
bind:value={email}
pattern={/^[^\s@]+@[^\s@]+\.[^\s@]+$/}
/>
<ValidatedInput
bind:value={username}
min={3}
max={20}
/>
File Input Binding
Bind to file inputs to access selected files:
<script lang="ts">
let files: FileList | null = $state(null);
let previews: string[] = $state([]);
$effect(() => {
if (!files) {
previews = [];
return;
}
const newPreviews: string[] = [];
for (const file of files) {
if (file.type.startsWith("image/")) {
const url = URL.createObjectURL(file);
newPreviews.push(url);
}
}
previews = newPreviews;
// Cleanup URLs on change
return () => {
previews.forEach(url => URL.revokeObjectURL(url));
};
});
function formatSize(bytes: number): string {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
}
</script>
<input type="file" bind:files multiple accept="image/*" />
{#if files && files.length > 0}
<h4>Selected Files:</h4>
<ul>
{#each Array.from(files) as file, i}
<li>
{file.name} ({formatSize(file.size)})
</li>
{/each}
</ul>
<div class="previews">
{#each previews as src}
<img {src} alt="Preview" width="100" />
{/each}
</div>
{/if}
File Input Component with Bindable
Create a reusable file input component:
<!-- FileUploader.svelte -->
<script lang="ts">
interface Props {
files?: FileList | null;
accept?: string;
multiple?: boolean;
maxSize?: number; // bytes
}
let {
files = $bindable(null),
accept = "*/*",
multiple = false,
maxSize = 10 * 1024 * 1024 // 10MB default
}: Props = $props();
let inputElement: HTMLInputElement;
let dragOver: boolean = $state(false);
let error: string = $state("");
function validateFiles(fileList: FileList): boolean {
error = "";
for (const file of fileList) {
if (file.size > maxSize) {
error = `File "${file.name}" exceeds maximum size`;
return false;
}
}
return true;
}
function handleDrop(event: DragEvent): void {
event.preventDefault();
dragOver = false;
const droppedFiles = event.dataTransfer?.files;
if (droppedFiles && validateFiles(droppedFiles)) {
files = droppedFiles;
}
}
function handleChange(event: Event): void {
const target = event.target as HTMLInputElement;
if (target.files && validateFiles(target.files)) {
files = target.files;
}
}
</script>
<div
class="dropzone"
class:drag-over={dragOver}
ondragover={(e) => { e.preventDefault(); dragOver = true; }}
ondragleave={() => dragOver = false}
ondrop={handleDrop}
onclick={() => inputElement.click()}
>
<input
bind:this={inputElement}
type="file"
{accept}
{multiple}
onchange={handleChange}
hidden
/>
<p>Drop files here or click to browse</p>
{#if error}
<p class="error">{error}</p>
{/if}
</div>
<style>
.dropzone {
border: 2px dashed #ccc;
padding: 40px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.drag-over { border-color: #007bff; background: #f0f8ff; }
.error { color: red; }
</style>
<!-- Parent.svelte -->
<script lang="ts">
import FileUploader from "./FileUploader.svelte";
let uploadedFiles: FileList | null = $state(null);
</script>
<FileUploader
bind:files={uploadedFiles}
accept="image/*,application/pdf"
multiple
maxSize={5 * 1024 * 1024}
/>
{#if uploadedFiles}
<p>{uploadedFiles.length} file(s) selected</p>
{/if}
Details Element Binding
Bind to the open state of <details> elements:
<script lang="ts">
let detailsOpen: boolean = $state(false);
interface FAQ {
question: string;
answer: string;
open: boolean;
}
let faqs: FAQ[] = $state([
{ question: "What is Svelte?", answer: "A compiler-based framework.", open: false },
{ question: "Is it fast?", answer: "Yes, very fast!", open: false },
{ question: "Should I learn it?", answer: "Absolutely!", open: false }
]);
function closeAll(): void {
faqs = faqs.map(faq => ({ ...faq, open: false }));
}
function openAll(): void {
faqs = faqs.map(faq => ({ ...faq, open: true }));
}
</script>
<h3>Single Details</h3>
<details bind:open={detailsOpen}>
<summary>Click to {detailsOpen ? "close" : "open"}</summary>
<p>Hidden content revealed!</p>
</details>
<p>Details is {detailsOpen ? "open" : "closed"}</p>
<button onclick={() => detailsOpen = !detailsOpen}>
Toggle programmatically
</button>
<h3>FAQ Accordion</h3>
<button onclick={openAll}>Open All</button>
<button onclick={closeAll}>Close All</button>
{#each faqs as faq, i}
<details bind:open={faq.open}>
<summary>{faq.question}</summary>
<p>{faq.answer}</p>
</details>
{/each}
Dialog Element Binding
Bind to the open attribute of <dialog> elements:
<script lang="ts">
let dialogElement: HTMLDialogElement;
let isOpen: boolean = $state(false);
let returnValue: string = $state("");
function openModal(): void {
dialogElement.showModal();
isOpen = true;
}
function closeModal(value: string): void {
dialogElement.close(value);
isOpen = false;
returnValue = value;
}
function handleClose(): void {
isOpen = false;
returnValue = dialogElement.returnValue;
}
</script>
<button onclick={openModal}>Open Dialog</button>
<p>Last result: {returnValue || "None"}</p>
<dialog bind:this={dialogElement} onclose={handleClose}>
<h2>Confirm Action</h2>
<p>Are you sure you want to proceed?</p>
<div class="actions">
<button onclick={() => closeModal("cancel")}>Cancel</button>
<button onclick={() => closeModal("confirm")}>Confirm</button>
</div>
</dialog>
<style>
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 16px;
}
</style>
Binding to Component Instances
Get a reference to a component instance to call its methods:
<!-- VideoPlayer.svelte -->
<script lang="ts">
interface Props {
src: string;
}
let { src }: Props = $props();
let videoElement: HTMLVideoElement;
let currentTime: number = $state(0);
let duration: number = $state(0);
let paused: boolean = $state(true);
// Expose methods for parent to call
export function play(): void {
videoElement.play();
}
export function pause(): void {
videoElement.pause();
}
export function seek(time: number): void {
videoElement.currentTime = time;
}
export function togglePlay(): void {
if (paused) {
play();
} else {
pause();
}
}
</script>
<div class="video-player">
<video
bind:this={videoElement}
bind:currentTime
bind:duration
bind:paused
{src}
>
<track kind="captions" />
</video>
<div class="controls">
<button onclick={togglePlay}>
{paused ? "▶️" : "⏸️"}
</button>
<span>{currentTime.toFixed(1)} / {duration.toFixed(1)}</span>
</div>
</div>
<!-- Parent.svelte -->
<script lang="ts">
import VideoPlayer from "./VideoPlayer.svelte";
let player: VideoPlayer;
</script>
<VideoPlayer bind:this={player} src="video.mp4" />
<div class="external-controls">
<button onclick={() => player.play()}>Play</button>
<button onclick={() => player.pause()}>Pause</button>
<button onclick={() => player.seek(0)}>Restart</button>
<button onclick={() => player.seek(30)}>Skip to 30s</button>
</div>
Binding to Inline Styles
Bind CSS custom properties (CSS variables) directly:
<script lang="ts">
let color: string = $state("#007bff");
let size: number = $state(16);
let opacity: number = $state(1);
let rotation: number = $state(0);
</script>
<div class="controls">
<label>
Color: <input type="color" bind:value={color} />
</label>
<label>
Size: <input type="range" bind:value={size} min="8" max="48" />
</label>
<label>
Opacity: <input type="range" bind:value={opacity} min="0" max="1" step="0.1" />
</label>
<label>
Rotation: <input type="range" bind:value={rotation} min="0" max="360" />
</label>
</div>
<div
class="styled-box"
style:--color={color}
style:--size="{size}px"
style:--opacity={opacity}
style:--rotation="{rotation}deg"
>
Styled Box
</div>
<style>
.styled-box {
color: var(--color);
font-size: var(--size);
opacity: var(--opacity);
transform: rotate(var(--rotation));
padding: 20px;
border: 2px solid var(--color);
display: inline-block;
transition: all 0.2s;
}
</style>
Indeterminate Checkbox Binding
Bind to the indeterminate state of checkboxes:
<script lang="ts">
interface Task {
id: number;
name: string;
completed: boolean;
}
let tasks: Task[] = $state([
{ id: 1, name: "Task 1", completed: true },
{ id: 2, name: "Task 2", completed: false },
{ id: 3, name: "Task 3", completed: true }
]);
let allChecked = $derived(tasks.every(t => t.completed));
let noneChecked = $derived(tasks.every(t => !t.completed));
let indeterminate = $derived(!allChecked && !noneChecked);
function toggleAll(): void {
const newState = !allChecked;
tasks = tasks.map(t => ({ ...t, completed: newState }));
}
</script>
<label>
<input
type="checkbox"
checked={allChecked}
{indeterminate}
onchange={toggleAll}
/>
Select All
</label>
<ul>
{#each tasks as task}
<li>
<label>
<input type="checkbox" bind:checked={task.completed} />
{task.name}
</label>
</li>
{/each}
</ul>
<p>
Status:
{#if allChecked}
All selected
{:else if noneChecked}
None selected
{:else}
Partial selection
{/if}
</p>
Binding to Canvas
Bind to canvas and create interactive drawings:
<script lang="ts">
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D | null = null;
let isDrawing: boolean = $state(false);
let color: string = $state("#000000");
let lineWidth: number = $state(2);
$effect(() => {
if (canvas) {
ctx = canvas.getContext("2d");
if (ctx) {
ctx.lineCap = "round";
ctx.lineJoin = "round";
}
}
});
function startDrawing(e: MouseEvent): void {
if (!ctx) return;
isDrawing = true;
ctx.beginPath();
ctx.moveTo(e.offsetX, e.offsetY);
}
function draw(e: MouseEvent): void {
if (!isDrawing || !ctx) return;
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
function stopDrawing(): void {
isDrawing = false;
}
function clearCanvas(): void {
if (ctx && canvas) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
</script>
<div class="controls">
<input type="color" bind:value={color} />
<input type="range" bind:value={lineWidth} min="1" max="20" />
<button onclick={clearCanvas}>Clear</button>
</div>
<canvas
bind:this={canvas}
width="400"
height="300"
onmousedown={startDrawing}
onmousemove={draw}
onmouseup={stopDrawing}
onmouseleave={stopDrawing}
style="border: 1px solid #ccc; cursor: crosshair;"
></canvas>
Binding to Audio Element
Create a full audio player with comprehensive bindings:
<script lang="ts">
interface Track {
title: string;
artist: string;
src: string;
}
let tracks: Track[] = [
{ title: "Song 1", artist: "Artist A", src: "song1.mp3" },
{ title: "Song 2", artist: "Artist B", src: "song2.mp3" }
];
let currentTrackIndex: number = $state(0);
let audioElement: HTMLAudioElement;
// Bindable audio properties
let currentTime: number = $state(0);
let duration: number = $state(0);
let paused: boolean = $state(true);
let volume: number = $state(1);
let muted: boolean = $state(false);
let playbackRate: number = $state(1);
let ended: boolean = $state(false);
let seeking: boolean = $state(false);
let currentTrack = $derived(tracks[currentTrackIndex]);
let progress = $derived(duration ? (currentTime / duration) * 100 : 0);
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, "0")}`;
}
function nextTrack(): void {
currentTrackIndex = (currentTrackIndex + 1) % tracks.length;
}
function prevTrack(): void {
currentTrackIndex = (currentTrackIndex - 1 + tracks.length) % tracks.length;
}
$effect(() => {
if (ended) {
nextTrack();
}
});
</script>
<audio
bind:this={audioElement}
bind:currentTime
bind:duration
bind:paused
bind:volume
bind:muted
bind:playbackRate
bind:ended
bind:seeking
src={currentTrack.src}
></audio>
<div class="audio-player">
<div class="track-info">
<strong>{currentTrack.title}</strong>
<span>{currentTrack.artist}</span>
</div>
<div class="progress-bar">
<input
type="range"
min="0"
max={duration || 0}
bind:value={currentTime}
/>
<span>{formatTime(currentTime)} / {formatTime(duration)}</span>
</div>
<div class="controls">
<button onclick={prevTrack}>⏮️</button>
<button onclick={() => paused = !paused}>
{paused ? "▶️" : "⏸️"}
</button>
<button onclick={nextTrack}>⏭️</button>
</div>
<div class="volume">
<button onclick={() => muted = !muted}>
{muted ? "🔇" : "🔊"}
</button>
<input type="range" min="0" max="1" step="0.1" bind:value={volume} />
</div>
<div class="speed">
<label>
Speed:
<select bind:value={playbackRate}>
<option value={0.5}>0.5x</option>
<option value={1}>1x</option>
<option value={1.5}>1.5x</option>
<option value={2}>2x</option>
</select>
</label>
</div>
</div>
Binding with Getter/Setter Pattern
Create computed bindings using getters and setters:
<script lang="ts">
// Store value in cents, display in dollars
let cents: number = $state(1000);
// Create a "virtual" binding with getter/setter
let dollars = {
get value(): number {
return cents / 100;
},
set value(v: number) {
cents = Math.round(v * 100);
}
};
// Temperature conversion
let celsius: number = $state(0);
let fahrenheit = {
get value(): number {
return (celsius * 9/5) + 32;
},
set value(f: number) {
celsius = (f - 32) * 5/9;
}
};
</script>
<div>
<h4>Currency (stored as cents: {cents})</h4>
<label>
Dollars: $
<input
type="number"
step="0.01"
value={dollars.value}
oninput={(e) => dollars.value = parseFloat(e.currentTarget.value) || 0}
/>
</label>
</div>
<div>
<h4>Temperature</h4>
<label>
Celsius:
<input type="number" bind:value={celsius} />
</label>
<label>
Fahrenheit:
<input
type="number"
value={fahrenheit.value}
oninput={(e) => fahrenheit.value = parseFloat(e.currentTarget.value) || 0}
/>
</label>
</div>
Binding to Custom Form Components
Create fully custom form components with binding support:
<!-- StarRating.svelte -->
<script lang="ts">
interface Props {
rating?: number;
max?: number;
readonly?: boolean;
}
let {
rating = $bindable(0),
max = 5,
readonly = false
}: Props = $props();
let hoverRating: number | null = $state(null);
function setRating(value: number): void {
if (!readonly) {
rating = value;
}
}
function handleHover(value: number | null): void {
if (!readonly) {
hoverRating = value;
}
}
let displayRating = $derived(hoverRating ?? rating);
</script>
<div class="star-rating" class:readonly>
{#each Array(max) as _, i}
{@const value = i + 1}
<button
type="button"
class="star"
class:filled={value <= displayRating}
onclick={() => setRating(value)}
onmouseenter={() => handleHover(value)}
onmouseleave={() => handleHover(null)}
disabled={readonly}
>
{value <= displayRating ? "★" : "☆"}
</button>
{/each}
</div>
<style>
.star-rating { display: flex; gap: 4px; }
.star {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #ffc107;
}
.readonly .star { cursor: default; }
</style>
<!-- RangeSlider.svelte -->
<script lang="ts">
interface Props {
min?: number;
max?: number;
step?: number;
value?: [number, number];
}
let {
min = 0,
max = 100,
step = 1,
value = $bindable([25, 75] as [number, number])
}: Props = $props();
function handleMinChange(e: Event): void {
const target = e.target as HTMLInputElement;
const newMin = parseFloat(target.value);
if (newMin <= value[1]) {
value = [newMin, value[1]];
}
}
function handleMaxChange(e: Event): void {
const target = e.target as HTMLInputElement;
const newMax = parseFloat(target.value);
if (newMax >= value[0]) {
value = [value[0], newMax];
}
}
</script>
<div class="range-slider">
<input
type="range"
{min}
{max}
{step}
value={value[0]}
oninput={handleMinChange}
/>
<input
type="range"
{min}
{max}
{step}
value={value[1]}
oninput={handleMaxChange}
/>
<div class="values">
{value[0]} - {value[1]}
</div>
</div>
<!-- Parent.svelte -->
<script lang="ts">
import StarRating from "./StarRating.svelte";
import RangeSlider from "./RangeSlider.svelte";
let movieRating: number = $state(3);
let priceRange: [number, number] = $state([20, 80]);
</script>
<h4>Rate this movie:</h4>
<StarRating bind:rating={movieRating} />
<p>You rated: {movieRating} stars</p>
<h4>Price range:</h4>
<RangeSlider bind:value={priceRange} min={0} max={100} />
<p>Selected: ${priceRange[0]} - ${priceRange[1]}</p>
Debounced Binding
Create bindings with debouncing for performance:
<!-- DebouncedInput.svelte -->
<script lang="ts">
interface Props {
value?: string;
delay?: number;
placeholder?: string;
}
let {
value = $bindable(""),
delay = 300,
placeholder = ""
}: Props = $props();
let internalValue: string = $state(value);
let timeoutId: ReturnType<typeof setTimeout>;
// Sync internal value when external value changes
$effect(() => {
internalValue = value;
});
function handleInput(e: Event): void {
const target = e.target as HTMLInputElement;
internalValue = target.value;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
value = internalValue;
}, delay);
}
// Cleanup on unmount
$effect(() => {
return () => clearTimeout(timeoutId);
});
</script>
<input
type="text"
value={internalValue}
oninput={handleInput}
{placeholder}
/>
<!-- Parent.svelte -->
<script lang="ts">
import DebouncedInput from "./DebouncedInput.svelte";
let searchQuery: string = $state("");
let searchCount: number = $state(0);
$effect(() => {
if (searchQuery) {
searchCount++;
console.log(`Searching for: ${searchQuery}`);
}
});
</script>
<DebouncedInput
bind:value={searchQuery}
delay={500}
placeholder="Search..."
/>
<p>Query: {searchQuery}</p>
<p>Search triggered: {searchCount} times</p>
Binding to Scroll Position
Create scroll-bound animations and effects:
<script lang="ts">
let scrollY: number = $state(0);
let innerHeight: number = $state(0);
let scrollContainer: HTMLDivElement;
let containerScrollTop: number = $state(0);
let scrollProgress = $derived(
scrollY / (document.body.scrollHeight - innerHeight) * 100 || 0
);
let headerOpacity = $derived(
Math.max(0, 1 - scrollY / 200)
);
let showBackToTop = $derived(scrollY > 300);
function scrollToTop(): void {
window.scrollTo({ top: 0, behavior: "smooth" });
}
</script>
<svelte:window bind:scrollY bind:innerHeight />
<!-- Progress bar -->
<div class="scroll-progress" style="width: {scrollProgress}%"></div>
<!-- Header with fade effect -->
<header style="opacity: {headerOpacity}">
<h1>Scroll Demo</h1>
</header>
<!-- Content -->
<main>
<p>Scroll Y: {scrollY.toFixed(0)}px</p>
<p>Progress: {scrollProgress.toFixed(1)}%</p>
<!-- Lots of content to enable scrolling -->
{#each Array(50) as _, i}
<p>Paragraph {i + 1}</p>
{/each}
</main>
<!-- Back to top button -->
{#if showBackToTop}
<button class="back-to-top" onclick={scrollToTop}>
↑ Top
</button>
{/if}
<style>
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: #007bff;
z-index: 100;
transition: width 0.1s;
}
header {
position: fixed;
top: 3px;
width: 100%;
background: white;
padding: 10px;
z-index: 99;
}
main { margin-top: 80px; }
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
}
</style>
Advanced Bindings Summary
Binding Type
Syntax
Use Case
Bindable prop
$bindable(default)
Two-way component props
File input
bind:files
File uploads
Details open
bind:open
Accordions, dropdowns
Component instance
bind:this
Call component methods
CSS custom property
style:--prop
Dynamic theming
Indeterminate
indeterminate
Partial checkbox state
Canvas reference
bind:this
Drawing, graphics
Audio/Video
bind:currentTime, etc.
Media players
Window scroll
bind:scrollY
Scroll effects
Debounced
Custom component
Search, performance
Svelte Context API
What Is Context?
Context allows you to pass data between components without prop drilling.
Data flows from a parent component to any descendant , regardless of depth.
Key characteristics:
Context is not reactive by default (but can hold reactive values)
Context is available only during component initialization
Each component tree can have its own context values
Common use cases:
Theme/styling configuration
Authentication state
Internationalization (i18n)
Feature flags
Shared services
Basic Context: setContext and getContext
Use setContext to provide a value and getContext to consume it:
<!-- Parent.svelte -->
<script lang="ts">
import { setContext } from "svelte";
import Child from "./Child.svelte";
// Set a context value with a key
setContext("username", "Alice");
setContext("theme", "dark");
</script>
<div>
<h2>Parent Component</h2>
<Child />
</div>
<!-- Child.svelte -->
<script lang="ts">
import { getContext } from "svelte";
import GrandChild from "./GrandChild.svelte";
// Get context values
const username = getContext<string>("username");
const theme = getContext<string>("theme");
</script>
<div>
<p>Child: User is {username}, theme is {theme}</p>
<GrandChild />
</div>
<!-- GrandChild.svelte -->
<script lang="ts">
import { getContext } from "svelte";
// Context is available at any depth
const username = getContext<string>("username");
</script>
<p>GrandChild: Hello, {username}!</p>
Context must be set during component initialization (not in event handlers or effects).
Type-Safe Context Keys
Use symbols or typed keys for better type safety:
// context-keys.ts
import type { Writable } from "svelte/store";
// Define context keys as symbols for uniqueness
export const THEME_KEY = Symbol("theme");
export const USER_KEY = Symbol("user");
export const CONFIG_KEY = Symbol("config");
// Define types for context values
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "user" | "guest";
}
export interface ThemeConfig {
mode: "light" | "dark";
primaryColor: string;
fontSize: number;
}
export interface AppConfig {
apiUrl: string;
features: {
darkMode: boolean;
notifications: boolean;
};
}
<!-- App.svelte -->
<script lang="ts">
import { setContext } from "svelte";
import { THEME_KEY, USER_KEY, CONFIG_KEY } from "./context-keys";
import type { User, ThemeConfig, AppConfig } from "./context-keys";
import Dashboard from "./Dashboard.svelte";
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
role: "admin"
};
const theme: ThemeConfig = {
mode: "dark",
primaryColor: "#007bff",
fontSize: 16
};
const config: AppConfig = {
apiUrl: "https://api.example.com",
features: {
darkMode: true,
notifications: true
}
};
setContext(THEME_KEY, theme);
setContext(USER_KEY, user);
setContext(CONFIG_KEY, config);
</script>
<Dashboard />
<!-- Dashboard.svelte -->
<script lang="ts">
import { getContext } from "svelte";
import { THEME_KEY, USER_KEY } from "./context-keys";
import type { User, ThemeConfig } from "./context-keys";
// Type-safe context retrieval
const user = getContext<User>(USER_KEY);
const theme = getContext<ThemeConfig>(THEME_KEY);
</script>
<div style="background: {theme.mode === 'dark' ? '#333' : '#fff'}">
<h1>Welcome, {user.name}!</h1>
<p>Role: {user.role}</p>
</div>
Checking for Context with hasContext
Use hasContext to check if a context exists before accessing it:
<script lang="ts">
import { getContext, hasContext } from "svelte";
interface Theme {
mode: "light" | "dark";
}
const THEME_KEY = Symbol("theme");
// Check if context exists
const hasTheme = hasContext(THEME_KEY);
// Provide fallback if context doesn't exist
const theme: Theme = hasContext(THEME_KEY)
? getContext<Theme>(THEME_KEY)
: { mode: "light" }; // default fallback
// Or use a helper function
function getContextWithDefault<T>(key: symbol, defaultValue: T): T {
return hasContext(key) ? getContext<T>(key) : defaultValue;
}
const safeTheme = getContextWithDefault<Theme>(THEME_KEY, { mode: "light" });
</script>
<p>Theme context exists: {hasTheme}</p>
<p>Current mode: {theme.mode}</p>
Getting All Contexts with getAllContexts
Retrieve all available contexts as a Map:
<!-- Parent.svelte -->
<script lang="ts">
import { setContext } from "svelte";
import Child from "./Child.svelte";
setContext("theme", "dark");
setContext("language", "en");
setContext("userId", 123);
</script>
<Child />
<!-- Child.svelte -->
<script lang="ts">
import { getAllContexts, setContext } from "svelte";
import GrandChild from "./GrandChild.svelte";
// Add more context at this level
setContext("role", "admin");
// Get all contexts (including from ancestors)
const allContexts = getAllContexts();
// Convert to array for display
const contextEntries = Array.from(allContexts.entries());
</script>
<div>
<h3>Available Contexts:</h3>
<ul>
{#each contextEntries as [key, value]}
<li>{String(key)}: {JSON.stringify(value)}</li>
{/each}
</ul>
<GrandChild />
</div>
getAllContexts is useful for:
Debugging context availability
Forwarding all contexts to dynamically created components
Building context-aware wrapper components
Reactive Context with Svelte 5
Context values are not reactive by default, but you can pass reactive state:
<!-- ThemeProvider.svelte -->
<script lang="ts">
import { setContext } from "svelte";
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
// Create reactive state
let mode: "light" | "dark" = $state("light");
let primaryColor: string = $state("#007bff");
// Create a context object with getters and methods
const themeContext = {
get mode() { return mode; },
get primaryColor() { return primaryColor; },
toggle() {
mode = mode === "light" ? "dark" : "light";
},
setColor(color: string) {
primaryColor = color;
}
};
setContext("theme", themeContext);
</script>
{@render children()}
<!-- App.svelte -->
<script lang="ts">
import ThemeProvider from "./ThemeProvider.svelte";
import ThemedContent from "./ThemedContent.svelte";
</script>
<ThemeProvider>
<ThemedContent />
</ThemeProvider>
<!-- ThemedContent.svelte -->
<script lang="ts">
import { getContext } from "svelte";
interface ThemeContext {
readonly mode: "light" | "dark";
readonly primaryColor: string;
toggle: () => void;
setColor: (color: string) => void;
}
const theme = getContext<ThemeContext>("theme");
</script>
<div
class="content"
style="
background: {theme.mode === 'dark' ? '#1a1a1a' : '#ffffff'};
color: {theme.mode === 'dark' ? '#ffffff' : '#1a1a1a'};
"
>
<p>Current theme: {theme.mode}</p>
<p>Primary color: {theme.primaryColor}</p>
<button onclick={theme.toggle}>
Toggle Theme
</button>
<input
type="color"
value={theme.primaryColor}
oninput={(e) => theme.setColor(e.currentTarget.value)}
/>
</div>
Context Factory Pattern
Create reusable context with factory functions:
// auth-context.svelte.ts
import { setContext, getContext, hasContext } from "svelte";
const AUTH_KEY = Symbol("auth");
export interface User {
id: number;
name: string;
email: string;
}
export interface AuthContext {
readonly user: User | null;
readonly isAuthenticated: boolean;
readonly isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
export function createAuthContext(): AuthContext {
let user: User | null = $state(null);
let isLoading: boolean = $state(false);
const context: AuthContext = {
get user() { return user; },
get isAuthenticated() { return user !== null; },
get isLoading() { return isLoading; },
async login(email: string, password: string) {
isLoading = true;
try {
// Simulate API call
await new Promise(r => setTimeout(r, 1000));
user = { id: 1, name: "Alice", email };
} finally {
isLoading = false;
}
},
logout() {
user = null;
}
};
setContext(AUTH_KEY, context);
return context;
}
export function getAuthContext(): AuthContext {
if (!hasContext(AUTH_KEY)) {
throw new Error("Auth context not found. Did you forget to wrap with AuthProvider?");
}
return getContext<AuthContext>(AUTH_KEY);
}
<!-- AuthProvider.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
import { createAuthContext } from "./auth-context.svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
// Initialize auth context
createAuthContext();
</script>
{@render children()}
<!-- LoginForm.svelte -->
<script lang="ts">
import { getAuthContext } from "./auth-context.svelte";
const auth = getAuthContext();
let email: string = $state("");
let password: string = $state("");
async function handleSubmit(): Promise<void> {
await auth.login(email, password);
}
</script>
{#if auth.isAuthenticated}
<div>
<p>Welcome, {auth.user?.name}!</p>
<button onclick={auth.logout}>Logout</button>
</div>
{:else}
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input bind:value={email} placeholder="Email" disabled={auth.isLoading} />
<input bind:value={password} type="password" placeholder="Password" disabled={auth.isLoading} />
<button type="submit" disabled={auth.isLoading}>
{auth.isLoading ? "Loading..." : "Login"}
</button>
</form>
{/if}
Internationalization (i18n) Context
Build a complete i18n solution with context:
// i18n-context.svelte.ts
import { setContext, getContext } from "svelte";
const I18N_KEY = Symbol("i18n");
type Locale = "en" | "es" | "fr" | "de";
interface Translations {
[key: string]: string;
}
const translations: Record<Locale, Translations> = {
en: {
greeting: "Hello",
farewell: "Goodbye",
welcome: "Welcome, {name}!",
items: "{count} item(s)"
},
es: {
greeting: "Hola",
farewell: "Adiós",
welcome: "¡Bienvenido, {name}!",
items: "{count} artículo(s)"
},
fr: {
greeting: "Bonjour",
farewell: "Au revoir",
welcome: "Bienvenue, {name}!",
items: "{count} article(s)"
},
de: {
greeting: "Hallo",
farewell: "Auf Wiedersehen",
welcome: "Willkommen, {name}!",
items: "{count} Artikel"
}
};
export interface I18nContext {
readonly locale: Locale;
readonly availableLocales: Locale[];
setLocale: (locale: Locale) => void;
t: (key: string, params?: Record<string, string | number>) => string;
}
export function createI18nContext(initialLocale: Locale = "en"): I18nContext {
let locale: Locale = $state(initialLocale);
function t(key: string, params?: Record<string, string | number>): string {
let text = translations[locale][key] || key;
if (params) {
Object.entries(params).forEach(([k, v]) => {
text = text.replace(`{${k}}`, String(v));
});
}
return text;
}
const context: I18nContext = {
get locale() { return locale; },
get availableLocales() { return ["en", "es", "fr", "de"] as Locale[]; },
setLocale(newLocale: Locale) {
locale = newLocale;
},
t
};
setContext(I18N_KEY, context);
return context;
}
export function getI18n(): I18nContext {
return getContext<I18nContext>(I18N_KEY);
}
<!-- I18nProvider.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
import { createI18nContext, type I18nContext } from "./i18n-context.svelte";
interface Props {
locale?: "en" | "es" | "fr" | "de";
children: Snippet;
}
let { locale = "en", children }: Props = $props();
createI18nContext(locale);
</script>
{@render children()}
<!-- TranslatedContent.svelte -->
<script lang="ts">
import { getI18n } from "./i18n-context.svelte";
const i18n = getI18n();
let itemCount: number = $state(5);
</script>
<div>
<select
value={i18n.locale}
onchange={(e) => i18n.setLocale(e.currentTarget.value as "en" | "es" | "fr" | "de")}
>
{#each i18n.availableLocales as loc}
<option value={loc}>{loc.toUpperCase()}</option>
{/each}
</select>
<h1>{i18n.t("greeting")}</h1>
<p>{i18n.t("welcome", { name: "Alice" })}</p>
<input type="number" bind:value={itemCount} min="0" />
<p>{i18n.t("items", { count: itemCount })}</p>
<p>{i18n.t("farewell")}</p>
</div>
Toast/Notification Context
Create a global toast notification system:
// toast-context.svelte.ts
import { setContext, getContext } from "svelte";
const TOAST_KEY = Symbol("toast");
export interface Toast {
id: number;
message: string;
type: "info" | "success" | "warning" | "error";
duration: number;
}
export interface ToastContext {
readonly toasts: Toast[];
show: (message: string, type?: Toast["type"], duration?: number) => void;
success: (message: string) => void;
error: (message: string) => void;
warning: (message: string) => void;
info: (message: string) => void;
dismiss: (id: number) => void;
clear: () => void;
}
export function createToastContext(): ToastContext {
let toasts: Toast[] = $state([]);
let nextId = 0;
function show(message: string, type: Toast["type"] = "info", duration = 3000): void {
const id = nextId++;
const toast: Toast = { id, message, type, duration };
toasts = [...toasts, toast];
if (duration > 0) {
setTimeout(() => dismiss(id), duration);
}
}
function dismiss(id: number): void {
toasts = toasts.filter(t => t.id !== id);
}
function clear(): void {
toasts = [];
}
const context: ToastContext = {
get toasts() { return toasts; },
show,
success: (msg) => show(msg, "success"),
error: (msg) => show(msg, "error", 5000),
warning: (msg) => show(msg, "warning"),
info: (msg) => show(msg, "info"),
dismiss,
clear
};
setContext(TOAST_KEY, context);
return context;
}
export function useToast(): ToastContext {
return getContext<ToastContext>(TOAST_KEY);
}
<!-- ToastProvider.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
import { createToastContext, type Toast } from "./toast-context.svelte";
import { fly, fade } from "svelte/transition";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
const toast = createToastContext();
const typeStyles: Record<Toast["type"], string> = {
info: "background: #3498db; color: white;",
success: "background: #27ae60; color: white;",
warning: "background: #f39c12; color: white;",
error: "background: #e74c3c; color: white;"
};
</script>
{@render children()}
<div class="toast-container">
{#each toast.toasts as t (t.id)}
<div
class="toast"
style={typeStyles[t.type]}
in:fly={{ y: 50, duration: 200 }}
out:fade={{ duration: 150 }}
>
<span>{t.message}</span>
<button onclick={() => toast.dismiss(t.id)}>×</button>
</div>
{/each}
</div>
<style>
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 1000;
}
.toast {
padding: 12px 16px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 12px;
min-width: 200px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.toast button {
background: none;
border: none;
color: inherit;
font-size: 18px;
cursor: pointer;
margin-left: auto;
}
</style>
<!-- SomeComponent.svelte -->
<script lang="ts">
import { useToast } from "./toast-context.svelte";
const toast = useToast();
function handleSave(): void {
toast.success("Saved successfully!");
}
function handleError(): void {
toast.error("Something went wrong!");
}
</script>
<button onclick={handleSave}>Save</button>
<button onclick={handleError}>Trigger Error</button>
<button onclick={() => toast.info("Just FYI...")}>Info</button>
<button onclick={() => toast.warning("Be careful!")}>Warning</button>
Modal/Dialog Context
Manage modals globally through context:
// modal-context.svelte.ts
import { setContext, getContext } from "svelte";
import type { Snippet } from "svelte";
const MODAL_KEY = Symbol("modal");
export interface ModalConfig {
title?: string;
content: Snippet;
onClose?: () => void;
closeOnBackdrop?: boolean;
}
export interface ModalContext {
readonly isOpen: boolean;
readonly config: ModalConfig | null;
open: (config: ModalConfig) => void;
close: () => void;
confirm: (title: string, message: string) => Promise<boolean>;
}
export function createModalContext(): ModalContext {
let isOpen: boolean = $state(false);
let config: ModalConfig | null = $state(null);
let confirmResolver: ((value: boolean) => void) | null = null;
const context: ModalContext = {
get isOpen() { return isOpen; },
get config() { return config; },
open(newConfig: ModalConfig) {
config = newConfig;
isOpen = true;
},
close() {
config?.onClose?.();
isOpen = false;
config = null;
if (confirmResolver) {
confirmResolver(false);
confirmResolver = null;
}
},
confirm(title: string, message: string): Promise<boolean> {
return new Promise((resolve) => {
confirmResolver = resolve;
// Note: content would be set by the ModalProvider
isOpen = true;
});
}
};
setContext(MODAL_KEY, context);
return context;
}
export function useModal(): ModalContext {
return getContext<ModalContext>(MODAL_KEY);
}
Feature Flags Context
Control feature visibility across your app:
// feature-flags-context.svelte.ts
import { setContext, getContext } from "svelte";
const FEATURE_FLAGS_KEY = Symbol("featureFlags");
export interface FeatureFlags {
darkMode: boolean;
newDashboard: boolean;
betaFeatures: boolean;
experimentalApi: boolean;
}
export interface FeatureFlagsContext {
readonly flags: FeatureFlags;
isEnabled: (flag: keyof FeatureFlags) => boolean;
enable: (flag: keyof FeatureFlags) => void;
disable: (flag: keyof FeatureFlags) => void;
toggle: (flag: keyof FeatureFlags) => void;
}
export function createFeatureFlagsContext(initial: Partial<FeatureFlags> = {}): FeatureFlagsContext {
let flags: FeatureFlags = $state({
darkMode: false,
newDashboard: false,
betaFeatures: false,
experimentalApi: false,
...initial
});
const context: FeatureFlagsContext = {
get flags() { return flags; },
isEnabled(flag: keyof FeatureFlags): boolean {
return flags[flag];
},
enable(flag: keyof FeatureFlags): void {
flags[flag] = true;
},
disable(flag: keyof FeatureFlags): void {
flags[flag] = false;
},
toggle(flag: keyof FeatureFlags): void {
flags[flag] = !flags[flag];
}
};
setContext(FEATURE_FLAGS_KEY, context);
return context;
}
export function useFeatureFlags(): FeatureFlagsContext {
return getContext<FeatureFlagsContext>(FEATURE_FLAGS_KEY);
}
<!-- FeatureFlaggedComponent.svelte -->
<script lang="ts">
import { useFeatureFlags } from "./feature-flags-context.svelte";
const features = useFeatureFlags();
</script>
{#if features.isEnabled("newDashboard")}
<div class="new-dashboard">
<h2>New Dashboard (Beta)</h2>
<!-- New dashboard content -->
</div>
{:else}
<div class="old-dashboard">
<h2>Dashboard</h2>
<!-- Old dashboard content -->
</div>
{/if}
{#if features.isEnabled("betaFeatures")}
<aside class="beta-panel">
<h3>Beta Features</h3>
<!-- Beta content -->
</aside>
{/if}
Nested Context Override
Child components can override parent context:
<!-- ThemeProvider.svelte -->
<script lang="ts">
import { setContext } from "svelte";
import type { Snippet } from "svelte";
interface Props {
theme: "light" | "dark";
children: Snippet;
}
let { theme, children }: Props = $props();
// This will override any parent theme context
setContext("theme", theme);
</script>
<div class="theme-{theme}">
{@render children()}
</div>
<!-- App.svelte -->
<script lang="ts">
import ThemeProvider from "./ThemeProvider.svelte";
import ThemedBox from "./ThemedBox.svelte";
</script>
<ThemeProvider theme="light">
<!-- This section uses light theme -->
<ThemedBox>Light themed content</ThemedBox>
<ThemeProvider theme="dark">
<!-- This nested section overrides to dark theme -->
<ThemedBox>Dark themed content (nested override)</ThemedBox>
</ThemeProvider>
<!-- Back to light theme -->
<ThemedBox>Light themed again</ThemedBox>
</ThemeProvider>
<!-- ThemedBox.svelte -->
<script lang="ts">
import { getContext } from "svelte";
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
const theme = getContext<"light" | "dark">("theme");
</script>
<div
class="box"
style="
background: {theme === 'dark' ? '#333' : '#fff'};
color: {theme === 'dark' ? '#fff' : '#333'};
"
>
<p>Theme: {theme}</p>
{@render children()}
</div>
Context vs Props vs Stores
Choosing the right state sharing mechanism:
Feature
Props
Context
Stores / Runes in Modules
Direction
Parent → Child (direct)
Parent → Any Descendant
Any → Any
Reactivity
Automatic
Manual (via runes)
Automatic
Scope
Single component
Component subtree
Global / Module
Multiple instances
Yes
Yes (per tree)
No (singleton)
Best for
Direct parent-child data
Avoiding prop drilling
Global app state
Debugging
Easy
Moderate
Easy
Common Pitfalls
Setting context outside initialization:
<script lang="ts">
import { setContext, getContext } from "svelte";
// ✅ CORRECT: During initialization
setContext("key", "value");
// ❌ WRONG: In event handler
function handleClick(): void {
// This will throw an error!
// setContext("key", "newValue");
}
// ❌ WRONG: In $effect
// $effect(() => {
// setContext("key", someValue);
// });
</script>
Expecting automatic reactivity:
<script lang="ts">
import { setContext } from "svelte";
let count = $state(0);
// ❌ WRONG: Primitive won't be reactive in consumers
// setContext("count", count);
// ✅ CORRECT: Pass an object with getter
setContext("counter", {
get count() { return count; },
increment() { count++; }
});
</script>
Using string keys (collision risk):
// ❌ WRONG: String keys can collide
setContext("theme", myTheme);
// ✅ CORRECT: Use symbols for uniqueness
const THEME_KEY = Symbol("theme");
setContext(THEME_KEY, myTheme);
Context API Summary
Function
Purpose
Usage
setContext
Provide a value
setContext(key, value)
getContext
Consume a value
getContext<T>(key)
hasContext
Check if exists
hasContext(key)
getAllContexts
Get all as Map
getAllContexts()
Symbol keys
Prevent collisions
Symbol("name")
Factory pattern
Reusable contexts
createXContext()
Reactive context
Use getters + runes
get value() { return state }
Svelte Special Elements
Overview
Svelte provides special elements that offer functionality beyond regular HTML.
These elements are prefixed with svelte: and handle specific use cases:
<svelte:self> — Recursive component rendering
<svelte:component> — Dynamic component rendering
<svelte:element> — Dynamic HTML element rendering
<svelte:window> — Window event listeners and bindings
<svelte:document> — Document event listeners and bindings
<svelte:body> — Body event listeners
<svelte:head> — Insert content into <head>
<svelte:options> — Compiler options
<svelte:fragment> — Group elements without wrapper (Svelte 4)
<svelte:boundary> — Error boundaries (Svelte 5)
<svelte:self> — Recursive Components
Allows a component to include itself recursively:
<!-- TreeView.svelte -->
<script lang="ts">
interface TreeNode {
name: string;
children?: TreeNode[];
expanded?: boolean;
}
interface Props {
node: TreeNode;
depth?: number;
}
let { node, depth = 0 }: Props = $props();
let expanded: boolean = $state(node.expanded ?? false);
function toggle(): void {
expanded = !expanded;
}
</script>
<div class="tree-node" style="padding-left: {depth * 20}px">
{#if node.children && node.children.length > 0}
<button class="toggle" onclick={toggle}>
{expanded ? "▼" : "▶"}
</button>
{:else}
<span class="leaf">•</span>
{/if}
<span class="name">{node.name}</span>
{#if expanded && node.children}
{#each node.children as child}
<!-- Recursive call to self -->
<svelte:self node={child} depth={depth + 1} />
{/each}
{/if}
</div>
<style>
.tree-node { font-family: monospace; }
.toggle {
background: none;
border: none;
cursor: pointer;
width: 20px;
}
.leaf { width: 20px; display: inline-block; text-align: center; }
</style>
<!-- App.svelte -->
<script lang="ts">
import TreeView from "./TreeView.svelte";
const fileSystem = {
name: "root",
expanded: true,
children: [
{
name: "src",
children: [
{ name: "App.svelte" },
{ name: "main.ts" },
{
name: "components",
children: [
{ name: "Header.svelte" },
{ name: "Footer.svelte" }
]
}
]
},
{
name: "public",
children: [
{ name: "index.html" },
{ name: "favicon.ico" }
]
},
{ name: "package.json" }
]
};
</script>
<TreeView node={fileSystem} />
<svelte:self> — Comment Thread Example
Nested comment threads are a perfect use case:
<!-- Comment.svelte -->
<script lang="ts">
interface CommentData {
id: number;
author: string;
text: string;
timestamp: string;
replies?: CommentData[];
}
interface Props {
comment: CommentData;
depth?: number;
}
let { comment, depth = 0 }: Props = $props();
let showReplies: boolean = $state(true);
let maxDepth = 5;
</script>
<div class="comment" style="margin-left: {depth * 24}px">
<div class="comment-header">
<strong>{comment.author}</strong>
<span class="timestamp">{comment.timestamp}</span>
</div>
<p class="comment-text">{comment.text}</p>
{#if comment.replies && comment.replies.length > 0}
<button onclick={() => showReplies = !showReplies}>
{showReplies ? "Hide" : "Show"} {comment.replies.length} replies
</button>
{#if showReplies && depth < maxDepth}
{#each comment.replies as reply}
<svelte:self comment={reply} depth={depth + 1} />
{/each}
{:else if depth >= maxDepth}
<p class="max-depth">Continue thread →</p>
{/if}
{/if}
</div>
<style>
.comment {
border-left: 2px solid #ddd;
padding: 8px 12px;
margin: 8px 0;
}
.timestamp { color: #666; font-size: 0.8em; margin-left: 8px; }
.max-depth { color: #007bff; cursor: pointer; }
</style>
<svelte:component> — Dynamic Components
Render different components dynamically based on a variable:
<!-- Alert.svelte -->
<script lang="ts">
interface Props {
message: string;
}
let { message }: Props = $props();
</script>
<div class="alert">⚠️ {message}</div>
<!-- Success.svelte -->
<script lang="ts">
interface Props {
message: string;
}
let { message }: Props = $props();
</script>
<div class="success">✅ {message}</div>
<!-- Error.svelte -->
<script lang="ts">
interface Props {
message: string;
}
let { message }: Props = $props();
</script>
<div class="error">❌ {message}</div>
<!-- App.svelte -->
<script lang="ts">
import Alert from "./Alert.svelte";
import Success from "./Success.svelte";
import Error from "./Error.svelte";
import type { Component } from "svelte";
type NotificationType = "alert" | "success" | "error";
const components: Record<NotificationType, Component<{ message: string }>> = {
alert: Alert,
success: Success,
error: Error
};
let currentType: NotificationType = $state("alert");
let message: string = $state("This is a notification");
let CurrentComponent = $derived(components[currentType]);
</script>
<select bind:value={currentType}>
<option value="alert">Alert</option>
<option value="success">Success</option>
<option value="error">Error</option>
</select>
<input bind:value={message} />
<!-- Dynamic component rendering -->
<svelte:component this={CurrentComponent} {message} />
If this is falsy, nothing is rendered.
<svelte:component> — Tab System
Build a dynamic tab system:
<script lang="ts">
import type { Component } from "svelte";
import HomeTab from "./tabs/HomeTab.svelte";
import ProfileTab from "./tabs/ProfileTab.svelte";
import SettingsTab from "./tabs/SettingsTab.svelte";
interface Tab {
id: string;
label: string;
component: Component;
icon: string;
}
const tabs: Tab[] = [
{ id: "home", label: "Home", component: HomeTab, icon: "🏠" },
{ id: "profile", label: "Profile", component: ProfileTab, icon: "👤" },
{ id: "settings", label: "Settings", component: SettingsTab, icon: "⚙️" }
];
let activeTabId: string = $state("home");
let activeTab = $derived(tabs.find(t => t.id === activeTabId));
</script>
<div class="tabs">
<nav class="tab-list">
{#each tabs as tab}
<button
class="tab-button"
class:active={activeTabId === tab.id}
onclick={() => activeTabId = tab.id}
>
{tab.icon} {tab.label}
</button>
{/each}
</nav>
<div class="tab-content">
{#if activeTab}
<svelte:component this={activeTab.component} />
{/if}
</div>
</div>
<style>
.tab-list { display: flex; gap: 4px; border-bottom: 1px solid #ddd; }
.tab-button {
padding: 8px 16px;
border: none;
background: none;
cursor: pointer;
}
.tab-button.active {
border-bottom: 2px solid #007bff;
color: #007bff;
}
.tab-content { padding: 16px; }
</style>
<svelte:element> — Dynamic HTML Elements
Render different HTML elements based on a variable:
<script lang="ts">
interface Props {
level?: 1 | 2 | 3 | 4 | 5 | 6;
children: import("svelte").Snippet;
}
let { level = 1, children }: Props = $props();
// Dynamically determine the heading tag
let tag = $derived(`h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6");
</script>
<svelte:element this={tag}>
{@render children()}
</svelte:element>
<!-- Usage -->
<script lang="ts">
import Heading from "./Heading.svelte";
</script>
<Heading level={1}>Main Title</Heading>
<Heading level={2}>Subtitle</Heading>
<Heading level={3}>Section</Heading>
<svelte:element> — Polymorphic Components
Create components that can render as different elements:
<!-- Button.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
as?: "button" | "a" | "div";
href?: string;
disabled?: boolean;
variant?: "primary" | "secondary" | "ghost";
onclick?: () => void;
children: Snippet;
}
let {
as = "button",
href,
disabled = false,
variant = "primary",
onclick,
children
}: Props = $props();
// If href is provided, render as anchor
let tag = $derived(href ? "a" : as);
</script>
<svelte:element
this={tag}
class="btn btn-{variant}"
class:disabled
{href}
{disabled}
{onclick}
>
{@render children()}
</svelte:element>
<style>
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.btn-primary { background: #007bff; color: white; border: none; }
.btn-secondary { background: #6c757d; color: white; border: none; }
.btn-ghost { background: transparent; border: 1px solid #007bff; color: #007bff; }
.disabled { opacity: 0.5; pointer-events: none; }
</style>
<!-- Usage -->
<script lang="ts">
import Button from "./Button.svelte";
</script>
<!-- Renders as <button> -->
<Button onclick={() => console.log("clicked")}>Click Me</Button>
<!-- Renders as <a> -->
<Button href="/about">Go to About</Button>
<!-- Renders as <div> -->
<Button as="div" variant="ghost">Div Button</Button>
<svelte:window> — Window Events
Add event listeners to the window object:
<script lang="ts">
let innerWidth: number = $state(0);
let innerHeight: number = $state(0);
let scrollY: number = $state(0);
let online: boolean = $state(true);
function handleKeydown(event: KeyboardEvent): void {
if (event.key === "Escape") {
console.log("Escape pressed!");
}
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
console.log("Save shortcut!");
}
}
function handleResize(): void {
console.log(`Window resized: ${innerWidth}x${innerHeight}`);
}
function handleOnline(): void {
online = true;
console.log("Back online!");
}
function handleOffline(): void {
online = false;
console.log("Gone offline!");
}
</script>
<!-- Window event listeners and bindings -->
<svelte:window
onkeydown={handleKeydown}
onresize={handleResize}
ononline={handleOnline}
onoffline={handleOffline}
bind:innerWidth
bind:innerHeight
bind:scrollY
/>
<div class="status-bar" class:offline={!online}>
<p>Window: {innerWidth} × {innerHeight}</p>
<p>Scroll Y: {scrollY}px</p>
<p>Status: {online ? "🟢 Online" : "🔴 Offline"}</p>
</div>
<div style="height: 200vh; padding: 20px;">
<p>Scroll down to see scrollY update</p>
<p>Press Escape or Ctrl+S to test keyboard events</p>
</div>
<style>
.status-bar {
position: fixed;
top: 0;
right: 0;
background: white;
padding: 10px;
border: 1px solid #ddd;
font-size: 12px;
}
.offline { background: #ffebee; }
</style>
<svelte:window> — Available Bindings
All bindable window properties:
<script lang="ts">
// Readonly bindings
let innerWidth: number = $state(0);
let innerHeight: number = $state(0);
let outerWidth: number = $state(0);
let outerHeight: number = $state(0);
let online: boolean = $state(true);
let devicePixelRatio: number = $state(1);
// Two-way bindings (can be set)
let scrollX: number = $state(0);
let scrollY: number = $state(0);
function scrollToTop(): void {
scrollY = 0; // This will scroll the window
}
function scrollToPosition(x: number, y: number): void {
scrollX = x;
scrollY = y;
}
</script>
<svelte:window
bind:innerWidth
bind:innerHeight
bind:outerWidth
bind:outerHeight
bind:scrollX
bind:scrollY
bind:online
bind:devicePixelRatio
/>
<div class="info">
<h3>Window Properties</h3>
<table>
<tr><td>Inner Size</td><td>{innerWidth} × {innerHeight}</td></tr>
<tr><td>Outer Size</td><td>{outerWidth} × {outerHeight}</td></tr>
<tr><td>Scroll Position</td><td>{scrollX}, {scrollY}</td></tr>
<tr><td>Online</td><td>{online}</td></tr>
<tr><td>Pixel Ratio</td><td>{devicePixelRatio}</td></tr>
</table>
<button onclick={scrollToTop}>Scroll to Top</button>
</div>
Binding
Type
Writable
innerWidth
number
No
innerHeight
number
No
outerWidth
number
No
outerHeight
number
No
scrollX
number
Yes
scrollY
number
Yes
online
boolean
No
devicePixelRatio
number
No
<svelte:document> — Document Events
Add event listeners to the document object:
<script lang="ts">
let activeElement: Element | null = $state(null);
let visibilityState: DocumentVisibilityState = $state("visible");
let fullscreenElement: Element | null = $state(null);
function handleVisibilityChange(): void {
visibilityState = document.visibilityState;
console.log("Visibility:", visibilityState);
}
function handleSelectionChange(): void {
const selection = document.getSelection();
if (selection && selection.toString()) {
console.log("Selected:", selection.toString());
}
}
function handleFullscreenChange(): void {
fullscreenElement = document.fullscreenElement;
}
</script>
<svelte:document
onvisibilitychange={handleVisibilityChange}
onselectionchange={handleSelectionChange}
onfullscreenchange={handleFullscreenChange}
bind:activeElement
bind:fullscreenElement
/>
<div>
<p>Active Element: {activeElement?.tagName ?? "None"}</p>
<p>Visibility: {visibilityState}</p>
<p>Fullscreen: {fullscreenElement ? "Yes" : "No"}</p>
<input placeholder="Focus me to see activeElement change" />
<textarea>Select this text to trigger selectionchange</textarea>
</div>
Available document bindings:
activeElement — Currently focused element
fullscreenElement — Element in fullscreen mode
pointerLockElement — Element with pointer lock
visibilityState — Document visibility state
<svelte:body> — Body Events
Add event listeners to the document body:
<script lang="ts">
let mouseX: number = $state(0);
let mouseY: number = $state(0);
let isDragging: boolean = $state(false);
function handleMouseMove(event: MouseEvent): void {
mouseX = event.clientX;
mouseY = event.clientY;
}
function handleDragStart(): void {
isDragging = true;
}
function handleDragEnd(): void {
isDragging = false;
}
// Useful for modals - prevent body scroll
let preventScroll: boolean = $state(false);
$effect(() => {
if (preventScroll) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
});
</script>
<svelte:body
onmousemove={handleMouseMove}
ondragstart={handleDragStart}
ondragend={handleDragEnd}
/>
<div>
<p>Mouse Position: {mouseX}, {mouseY}</p>
<p>Dragging: {isDragging}</p>
<label>
<input type="checkbox" bind:checked={preventScroll} />
Prevent body scroll (for modals)
</label>
</div>
<!-- Custom cursor that follows mouse -->
<div
class="cursor-follower"
style="left: {mouseX}px; top: {mouseY}px"
></div>
<style>
.cursor-follower {
position: fixed;
width: 20px;
height: 20px;
background: rgba(0, 123, 255, 0.3);
border-radius: 50%;
pointer-events: none;
transform: translate(-50%, -50%);
}
</style>
<svelte:head> — Document Head
Insert elements into the document's <head>:
<script lang="ts">
interface Props {
title?: string;
description?: string;
}
let {
title = "My App",
description = "A Svelte application"
}: Props = $props();
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={description} />
</svelte:head>
<h1>{title}</h1>
<svelte:head> — SEO Component
Create a reusable SEO component:
<!-- SEO.svelte -->
<script lang="ts">
interface Props {
title: string;
description?: string;
keywords?: string[];
image?: string;
url?: string;
type?: "website" | "article" | "profile";
author?: string;
publishedTime?: string;
noindex?: boolean;
}
let {
title,
description = "",
keywords = [],
image = "",
url = "",
type = "website",
author = "",
publishedTime = "",
noindex = false
}: Props = $props();
let fullTitle = $derived(`${title} | My Site`);
</script>
<svelte:head>
<!-- Basic Meta -->
<title>{fullTitle}</title>
<meta name="description" content={description} />
{#if keywords.length > 0}
<meta name="keywords" content={keywords.join(", ")} />
{/if}
{#if noindex}
<meta name="robots" content="noindex, nofollow" />
{/if}
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content={type} />
{#if url}
<meta property="og:url" content={url} />
{/if}
{#if image}
<meta property="og:image" content={image} />
{/if}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{#if image}
<meta name="twitter:image" content={image} />
{/if}
<!-- Article specific -->
{#if type === "article"}
{#if author}
<meta property="article:author" content={author} />
{/if}
{#if publishedTime}
<meta property="article:published_time" content={publishedTime} />
{/if}
{/if}
<!-- Canonical URL -->
{#if url}
<link rel="canonical" href={url} />
{/if}
</svelte:head>
<!-- BlogPost.svelte -->
<script lang="ts">
import SEO from "./SEO.svelte";
interface Props {
post: {
title: string;
excerpt: string;
image: string;
author: string;
date: string;
content: string;
};
}
let { post }: Props = $props();
</script>
<SEO
title={post.title}
description={post.excerpt}
image={post.image}
type="article"
author={post.author}
publishedTime={post.date}
/>
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
<svelte:head> — Dynamic Styles and Scripts
Load external resources dynamically:
<script lang="ts">
let theme: "light" | "dark" = $state("light");
let loadHighlighting: boolean = $state(false);
</script>
<svelte:head>
<!-- Dynamic theme stylesheet -->
<link
rel="stylesheet"
href="/themes/{theme}.css"
/>
<!-- Conditionally load external library -->
{#if loadHighlighting}
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
{/if}
<!-- Custom fonts -->
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
rel="stylesheet"
/>
</svelte:head>
<select bind:value={theme}>
<option value="light">Light Theme</option>
<option value="dark">Dark Theme</option>
</select>
<label>
<input type="checkbox" bind:checked={loadHighlighting} />
Load syntax highlighting
</label>
<svelte:options> — Compiler Options
Configure compiler behavior for a component:
<!-- Must be at the top of the component -->
<svelte:options
immutable={true}
accessors={true}
namespace="svg"
customElement="my-component"
/>
<script lang="ts">
// Component code...
</script>
Option
Type
Description
immutable
boolean
Assume data never mutates (optimize comparisons)
accessors
boolean
Generate getters/setters for props
namespace
string
Namespace for the component (e.g., "svg", "mathml")
customElement
string
Compile as custom element with given tag name
runes
boolean
Force runes mode (Svelte 5)
<svelte:options> — Custom Elements
Compile a Svelte component as a Web Component:
<svelte:options customElement="my-counter" />
<script lang="ts">
interface Props {
count?: number;
step?: number;
}
let { count = $bindable(0), step = 1 }: Props = $props();
function increment(): void {
count += step;
}
function decrement(): void {
count -= step;
}
</script>
<div class="counter">
<button onclick={decrement}>-</button>
<span>{count}</span>
<button onclick={increment}>+</button>
</div>
<style>
/* Styles are encapsulated in Shadow DOM */
.counter {
display: flex;
gap: 8px;
align-items: center;
}
button {
width: 32px;
height: 32px;
}
</style>
<!-- Use in any HTML page -->
<my-counter count="10" step="5"></my-counter>
<svelte:fragment> — Slot Grouping (Svelte 4)
Group content for named slots without a wrapper element:
<!-- Layout.svelte (Svelte 4) -->
<script lang="ts">
export let title: string;
</script>
<div class="layout">
<header>
<slot name="header">
<h1>{title}</h1>
</slot>
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
<!-- Parent.svelte (Svelte 4) -->
<script lang="ts">
import Layout from "./Layout.svelte";
</script>
<Layout title="My Page">
<!-- Use svelte:fragment to pass multiple elements to a named slot -->
<svelte:fragment slot="header">
<h1>Custom Header</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</svelte:fragment>
<!-- Default slot content -->
<p>Main content goes here.</p>
<svelte:fragment slot="footer">
<p>Copyright 2024</p>
<p>All rights reserved</p>
</svelte:fragment>
</Layout>
In Svelte 5 with snippets, <svelte:fragment> is less commonly needed.
<svelte:boundary> — Error Boundaries (Svelte 5)
Catch and handle errors in child components:
<script lang="ts">
import BuggyComponent from "./BuggyComponent.svelte";
let errorMessage: string = $state("");
let hasError: boolean = $state(false);
function handleError(error: Error): void {
hasError = true;
errorMessage = error.message;
console.error("Caught error:", error);
}
function reset(): void {
hasError = false;
errorMessage = "";
}
</script>
<svelte:boundary onerror={handleError}>
{#if hasError}
<div class="error-fallback">
<h2>Something went wrong</h2>
<p>{errorMessage}</p>
<button onclick={reset}>Try Again</button>
</div>
{:else}
<BuggyComponent />
{/if}
</svelte:boundary>
<style>
.error-fallback {
padding: 20px;
background: #ffebee;
border: 1px solid #f44336;
border-radius: 4px;
}
</style>
<svelte:boundary> — With Failed Snippet
Use the failed snippet for error UI:
<script lang="ts">
import DataDisplay from "./DataDisplay.svelte";
</script>
<svelte:boundary>
<DataDisplay />
{#snippet failed(error, reset)}
<div class="error-boundary">
<h3>⚠️ Error Loading Data</h3>
<p>{error.message}</p>
<details>
<summary>Stack trace</summary>
<pre>{error.stack}</pre>
</details>
<button onclick={reset}>Retry</button>
</div>
{/snippet}
</svelte:boundary>
<style>
.error-boundary {
padding: 16px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
}
details { margin: 10px 0; }
pre {
background: #f5f5f5;
padding: 10px;
overflow-x: auto;
font-size: 12px;
}
</style>
<svelte:boundary> — Nested Boundaries
Create granular error handling with nested boundaries:
<script lang="ts">
import Header from "./Header.svelte";
import Sidebar from "./Sidebar.svelte";
import MainContent from "./MainContent.svelte";
</script>
<div class="app">
<!-- Header errors won't crash the whole app -->
<svelte:boundary>
<Header />
{#snippet failed(error, reset)}
<header class="fallback">
<span>Header unavailable</span>
<button onclick={reset}>↻</button>
</header>
{/snippet}
</svelte:boundary>
<div class="content">
<!-- Sidebar errors are isolated -->
<svelte:boundary>
<Sidebar />
{#snippet failed()}
<aside class="fallback">Menu unavailable</aside>
{/snippet}
</svelte:boundary>
<!-- Main content has its own boundary -->
<svelte:boundary>
<MainContent />
{#snippet failed(error, reset)}
<main class="fallback">
<p>Content failed to load</p>
<button onclick={reset}>Reload</button>
</main>
{/snippet}
</svelte:boundary>
</div>
</div>
<style>
.app { display: flex; flex-direction: column; min-height: 100vh; }
.content { display: flex; flex: 1; }
.fallback {
background: #f5f5f5;
padding: 10px;
color: #666;
}
</style>
Special Elements Summary
Element
Purpose
Key Usage
<svelte:self>
Recursive rendering
Trees, nested comments
<svelte:component>
Dynamic components
this={Component}
<svelte:element>
Dynamic HTML elements
this={"div"}
<svelte:window>
Window events/bindings
bind:scrollY
<svelte:document>
Document events/bindings
bind:activeElement
<svelte:body>
Body events
onmousemove
<svelte:head>
Document head content
<title>, <meta>
<svelte:options>
Compiler options
customElement
<svelte:fragment>
Slot grouping (Svelte 4)
slot="name"
<svelte:boundary>
Error boundaries (Svelte 5)
onerror, failed
SvelteKit Introduction
What Is SvelteKit?
SvelteKit is the official application framework for Svelte.
While Svelte is a component framework , SvelteKit is a full-stack framework that provides:
File-based routing
Server-side rendering (SSR)
Static site generation (SSG)
API routes
Data loading
Form handling
Code splitting
Hot module replacement (HMR)
SvelteKit is to Svelte what Next.js is to React or Nuxt is to Vue.
Creating a SvelteKit Project
Use the official create command:
# Create a new project
npx sv create my-app
# Navigate to the project
cd my-app
# Install dependencies
npm install
# Start the development server
npm run dev
The CLI will prompt you to choose:
Template (Skeleton, Demo, Library)
TypeScript support
Additional options (ESLint, Prettier, Playwright, Vitest)
Project Structure
A typical SvelteKit project structure:
my-app/
├── src/
│ ├── lib/ # Library code (alias: $lib)
│ │ ├── components/ # Reusable components
│ │ ├── server/ # Server-only code ($lib/server)
│ │ └── utils.ts # Utility functions
│ ├── routes/ # File-based routing
│ │ ├── +page.svelte # Home page (/)
│ │ ├── +page.ts # Page load function
│ │ ├── +layout.svelte # Root layout
│ │ ├── +error.svelte # Error page
│ │ ├── about/
│ │ │ └── +page.svelte # About page (/about)
│ │ └── api/
│ │ └── +server.ts # API endpoint (/api)
│ ├── app.html # HTML template
│ ├── app.css # Global styles
│ └── app.d.ts # Type declarations
├── static/ # Static assets (favicon, images)
├── svelte.config.js # Svelte/SvelteKit config
├── vite.config.ts # Vite config
├── tsconfig.json # TypeScript config
└── package.json
The src/routes Directory
Routes are defined by the directory structure inside src/routes:
File Path
URL
src/routes/+page.svelte
/
src/routes/about/+page.svelte
/about
src/routes/blog/+page.svelte
/blog
src/routes/blog/[slug]/+page.svelte
/blog/:slug
src/routes/api/users/+server.ts
/api/users
Route Files Overview
Special files in each route directory:
File
Purpose
+page.svelte
The page component (UI)
+page.ts
Load data for the page (runs on server & client)
+page.server.ts
Load data (server-only), form actions
+layout.svelte
Shared layout wrapper
+layout.ts
Load data for layout
+layout.server.ts
Load data for layout (server-only)
+error.svelte
Error page for this route
+server.ts
API endpoint (GET, POST, etc.)
Basic Page Component
<!-- src/routes/+page.svelte -->
<script lang="ts">
let count: number = $state(0);
</script>
<svelte:head>
<title>Home | My App</title>
<meta name="description" content="Welcome to my SvelteKit app" />
</svelte:head>
<main>
<h1>Welcome to SvelteKit</h1>
<p>This is the home page.</p>
<button onclick={() => count++}>
Clicks: {count}
</button>
</main>
<style>
main {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
</style>
Layouts
Layouts wrap pages and provide shared UI:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="app">
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main>
{@render children()}
</main>
<footer>
<p>© 2024 My App</p>
</footer>
</div>
<style>
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
background: #333;
color: white;
padding: 1rem;
}
nav {
display: flex;
gap: 1rem;
}
nav a {
color: white;
text-decoration: none;
}
main {
flex: 1;
padding: 1rem;
}
footer {
background: #333;
color: white;
padding: 1rem;
text-align: center;
}
</style>
Nested Layouts
Layouts can be nested for different sections:
src/routes/
├── +layout.svelte # Root layout (applies to all pages)
├── +page.svelte # Home page
├── blog/
│ ├── +layout.svelte # Blog layout (nested inside root)
│ ├── +page.svelte # Blog index (/blog)
│ └── [slug]/
│ └── +page.svelte # Blog post (/blog/my-post)
└── dashboard/
├── +layout.svelte # Dashboard layout
├── +page.svelte # Dashboard home (/dashboard)
└── settings/
└── +page.svelte # Settings (/dashboard/settings)
<!-- src/routes/blog/+layout.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="blog-layout">
<aside>
<h3>Blog Categories</h3>
<ul>
<li><a href="/blog?category=tech">Technology</a></li>
<li><a href="/blog?category=life">Lifestyle</a></li>
<li><a href="/blog?category=news">News</a></li>
</ul>
</aside>
<article>
{@render children()}
</article>
</div>
<style>
.blog-layout {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2rem;
}
aside {
background: #f5f5f5;
padding: 1rem;
}
</style>
Loading Data with +page.ts
Load data before the page renders:
// src/routes/+page.ts
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch("/api/posts");
const posts = await response.json();
return {
posts,
title: "Latest Posts"
};
};
<!-- src/routes/+page.svelte -->
<script lang="ts">
import type { PageData } from "./$types";
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<h1>{data.title}</h1>
<ul>
{#each data.posts as post}
<li>
<a href="/blog/{post.slug}">{post.title}</a>
</li>
{/each}
</ul>
Server-Only Loading with +page.server.ts
For sensitive data or database access:
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from "./$types";
import { redirect } from "@sveltejs/kit";
import { db } from "$lib/server/database";
export const load: PageServerLoad = async ({ locals, cookies }) => {
// Check authentication
const sessionId = cookies.get("session");
if (!sessionId) {
throw redirect(303, "/login");
}
// Access database (server-only)
const user = await db.user.findUnique({
where: { sessionId }
});
if (!user) {
throw redirect(303, "/login");
}
const stats = await db.stats.findMany({
where: { userId: user.id }
});
return {
user: {
name: user.name,
email: user.email
},
stats
};
};
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
import type { PageData } from "./$types";
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<h1>Welcome back, {data.user.name}!</h1>
<div class="stats">
{#each data.stats as stat}
<div class="stat-card">
<h3>{stat.label}</h3>
<p>{stat.value}</p>
</div>
{/each}
</div>
Dynamic Routes
Use square brackets for dynamic parameters:
src/routes/
├── blog/
│ └── [slug]/ # Dynamic: /blog/my-post
│ └── +page.svelte
├── users/
│ └── [id]/ # Dynamic: /users/123
│ ├── +page.svelte
│ └── +page.ts
└── products/
└── [category]/
└── [id]/ # Multiple params: /products/electronics/456
└── +page.svelte
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from "./$types";
import { error } from "@sveltejs/kit";
export const load: PageLoad = async ({ params, fetch }) => {
// params.slug contains the dynamic value
const response = await fetch(`/api/posts/${params.slug}`);
if (!response.ok) {
throw error(404, {
message: "Post not found"
});
}
const post = await response.json();
return { post };
};
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from "./$types";
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<svelte:head>
<title>{data.post.title} | Blog</title>
</svelte:head>
<article>
<h1>{data.post.title}</h1>
<time>{data.post.date}</time>
<div class="content">
{@html data.post.content}
</div>
</article>
Rest Parameters
Catch multiple path segments with [...rest]:
src/routes/
└── docs/
└── [...path]/ # Matches /docs/a, /docs/a/b, /docs/a/b/c
└── +page.svelte
// src/routes/docs/[...path]/+page.ts
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ params }) => {
// params.path = "a/b/c" for /docs/a/b/c
const pathParts = params.path?.split("/") ?? [];
return {
path: params.path,
parts: pathParts,
depth: pathParts.length
};
};
Optional Parameters
Make parameters optional with double brackets:
src/routes/
└── lang/
└── [[locale]]/ # Matches /lang and /lang/en
└── +page.svelte
// src/routes/lang/[[locale]]/+page.ts
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ params }) => {
// params.locale is undefined for /lang
// params.locale is "en" for /lang/en
const locale = params.locale ?? "en"; // Default to English
return {
locale,
translations: await loadTranslations(locale)
};
};
async function loadTranslations(locale: string) {
// Load translations...
return {};
}
API Routes with +server.ts
// src/routes/api/posts/+server.ts
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit";
// GET /api/posts
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get("limit")) || 10;
const page = Number(url.searchParams.get("page")) || 1;
// Fetch posts from database
const posts = await fetchPosts({ limit, page });
return json({
posts,
page,
limit
});
};
// POST /api/posts
export const POST: RequestHandler = async ({ request, cookies }) => {
// Check authentication
const session = cookies.get("session");
if (!session) {
throw error(401, "Unauthorized");
}
const body = await request.json();
// Validate
if (!body.title || !body.content) {
throw error(400, "Title and content required");
}
// Create post
const post = await createPost(body);
return json(post, { status: 201 });
};
async function fetchPosts(options: { limit: number; page: number }) {
// Database query...
return [];
}
async function createPost(data: { title: string; content: string }) {
// Database insert...
return { id: 1, ...data };
}
// src/routes/api/posts/[id]/+server.ts
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit";
// GET /api/posts/123
export const GET: RequestHandler = async ({ params }) => {
const post = await findPost(params.id);
if (!post) {
throw error(404, "Post not found");
}
return json(post);
};
// PUT /api/posts/123
export const PUT: RequestHandler = async ({ params, request }) => {
const body = await request.json();
const post = await updatePost(params.id, body);
return json(post);
};
// DELETE /api/posts/123
export const DELETE: RequestHandler = async ({ params }) => {
await deletePost(params.id);
return new Response(null, { status: 204 });
};
async function findPost(id: string) { return null; }
async function updatePost(id: string, data: unknown) { return {}; }
async function deletePost(id: string) {}
Navigation with Links
Use standard anchor tags for client-side navigation:
<script lang="ts">
import { page } from "$app/stores";
// Check if link is active
let isActive = $derived($page.url.pathname === "/about");
</script>
<nav>
<!-- Standard links get client-side navigation -->
<a href="/">Home</a>
<a href="/about" class:active={isActive}>About</a>
<a href="/blog">Blog</a>
<!-- External links open normally -->
<a href="https://svelte.dev" target="_blank" rel="noopener">
Svelte Docs
</a>
<!-- Disable client-side navigation -->
<a href="/api/download" data-sveltekit-reload>
Download (full page reload)
</a>
<!-- Preload on hover -->
<a href="/blog" data-sveltekit-preload-data="hover">
Blog (preloads on hover)
</a>
</nav>
<style>
.active { font-weight: bold; color: #007bff; }
</style>
Programmatic Navigation
Navigate using the goto function:
<script lang="ts">
import { goto, invalidate, invalidateAll } from "$app/navigation";
import { page } from "$app/stores";
async function navigateToHome(): Promise<void> {
await goto("/");
}
async function navigateWithState(): Promise<void> {
await goto("/dashboard", {
replaceState: true, // Don't add to history
keepFocus: true, // Keep focus on current element
noScroll: true // Don't scroll to top
});
}
async function navigateBack(): Promise<void> {
history.back();
}
async function refreshData(): Promise<void> {
// Re-run load functions for current page
await invalidateAll();
}
async function refreshSpecificData(): Promise<void> {
// Re-run load functions that depend on this URL
await invalidate("/api/posts");
}
</script>
<button onclick={navigateToHome}>Go Home</button>
<button onclick={navigateBack}>Go Back</button>
<button onclick={refreshData}>Refresh Data</button>
<p>Current path: {$page.url.pathname}</p>
The $lib Alias
Import from src/lib using the $lib alias:
// src/lib/utils.ts
export function formatDate(date: Date): string {
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^\w-]+/g, "");
}
<!-- src/routes/+page.svelte -->
<script lang="ts">
// Import from $lib
import { formatDate, slugify } from "$lib/utils";
import Button from "$lib/components/Button.svelte";
import Header from "$lib/components/Header.svelte";
const date = formatDate(new Date());
</script>
<Header />
<p>Today: {date}</p>
<Button>Click me</Button>
Server-Only Code with $lib/server
Code in $lib/server can only be imported in server files:
// src/lib/server/database.ts
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
export async function getUserById(id: string) {
return db.user.findUnique({ where: { id } });
}
// src/lib/server/auth.ts
import { db } from "./database";
import bcrypt from "bcrypt";
export async function verifyPassword(email: string, password: string) {
const user = await db.user.findUnique({ where: { email } });
if (!user) return null;
const valid = await bcrypt.compare(password, user.passwordHash);
return valid ? user : null;
}
// src/routes/login/+page.server.ts
import { verifyPassword } from "$lib/server/auth"; // ✅ Allowed
import type { Actions } from "./$types";
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get("email") as string;
const password = data.get("password") as string;
const user = await verifyPassword(email, password);
if (user) {
cookies.set("session", user.id, { path: "/" });
return { success: true };
}
return { success: false, error: "Invalid credentials" };
}
};
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
// ❌ This would cause an error at build time:
// import { verifyPassword } from "$lib/server/auth";
// ✅ Use form actions instead
</script>
<form method="POST">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</form>
Error Handling
Create custom error pages:
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from "$app/stores";
</script>
<svelte:head>
<title>Error {$page.status}</title>
</svelte:head>
<div class="error">
<h1>{$page.status}</h1>
<p>{$page.error?.message ?? "Something went wrong"}</p>
{#if $page.status === 404}
<p>The page you're looking for doesn't exist.</p>
<a href="/">Go home</a>
{:else}
<button onclick={() => location.reload()}>
Try again
</button>
{/if}
</div>
<style>
.error {
text-align: center;
padding: 4rem;
}
h1 {
font-size: 4rem;
color: #e74c3c;
}
</style>
// Throwing errors in load functions
import type { PageLoad } from "./$types";
import { error } from "@sveltejs/kit";
export const load: PageLoad = async ({ params }) => {
const post = await getPost(params.id);
if (!post) {
throw error(404, {
message: "Post not found",
code: "POST_NOT_FOUND"
});
}
return { post };
};
Environment Variables
Access environment variables safely:
# .env
PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://localhost/mydb
SECRET_KEY=super-secret-key
// Server-side: Access all env vars
// src/routes/api/data/+server.ts
import { DATABASE_URL, SECRET_KEY } from "$env/static/private";
import { PUBLIC_API_URL } from "$env/static/public";
console.log(DATABASE_URL); // ✅ Server only
console.log(SECRET_KEY); // ✅ Server only
console.log(PUBLIC_API_URL); // ✅ Available everywhere
<!-- Client-side: Only PUBLIC_ vars -->
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { PUBLIC_API_URL } from "$env/static/public";
// ❌ These would fail:
// import { DATABASE_URL } from "$env/static/private";
// import { SECRET_KEY } from "$env/static/private";
</script>
<p>API URL: {PUBLIC_API_URL}</p>
Page Options
Configure page behavior with exports:
// src/routes/blog/+page.ts
import type { PageLoad } from "./$types";
// Prerender this page at build time
export const prerender = true;
// Enable/disable SSR
export const ssr = true;
// Enable/disable client-side rendering
export const csr = true;
// Trailing slash behavior: 'never', 'always', 'ignore'
export const trailingSlash = "never";
export const load: PageLoad = async () => {
return { /* ... */ };
};
// src/routes/app/+layout.ts
// Disable SSR for entire app section (SPA mode)
export const ssr = false;
// All child routes will inherit this setting
SvelteKit Stores
Built-in stores for app state:
<script lang="ts">
import { page, navigating, updated } from "$app/stores";
// $page - current page info
// $page.url - current URL
// $page.params - route parameters
// $page.data - data from load functions
// $page.status - HTTP status code
// $page.error - error object (if any)
// $navigating - navigation state (null when not navigating)
// $navigating.from - source URL
// $navigating.to - destination URL
// $updated - true when new version is available
</script>
<!-- Show loading indicator during navigation -->
{#if $navigating}
<div class="loading-bar"></div>
{/if}
<p>Current path: {$page.url.pathname}</p>
<p>Search params: {$page.url.searchParams.toString()}</p>
{#if $page.params.slug}
<p>Slug: {$page.params.slug}</p>
{/if}
{#if $updated}
<div class="update-banner">
<p>New version available!</p>
<button onclick={() => location.reload()}>
Refresh
</button>
</div>
{/if}
Configuration (svelte.config.js)
Configure SvelteKit behavior:
// svelte.config.js
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Preprocessors (TypeScript, SCSS, etc.)
preprocess: vitePreprocess(),
kit: {
// Adapter for deployment target
adapter: adapter(),
// Path aliases
alias: {
$components: "src/lib/components",
$stores: "src/lib/stores",
$utils: "src/lib/utils"
},
// Content Security Policy
csp: {
mode: "auto",
directives: {
"script-src": ["self"]
}
},
// Prerender options
prerender: {
entries: ["*"],
handleMissingId: "warn"
}
}
};
export default config;
SvelteKit vs Svelte Summary
Feature
Svelte
SvelteKit
Type
Component framework
Application framework
Routing
Manual / third-party
File-based (built-in)
SSR
Not included
Built-in
API Routes
Not included
Built-in
Data Loading
Manual
Load functions
Build Tool
Any (Vite, Rollup)
Vite (integrated)
Deployment
Static files
Adapters (Node, Vercel, etc.)
Use Case
Widgets, components
Full applications