Build & Development Commands
Testing Guidelines
Code Style & Guidelines
Project Guidelines
- Write code in Svelte 5 with runes
- Use TypeScript with types.
- Use Vite for building and development.
- Use Tailwind and DaisyUI for styling.
- Add newly created files to git.
Documentation Guidelines
Svelte 5 Runes Overview
$State
<!-- ================================== -->
<!-- Example 1: Basic Usage of $state -->
<!-- ================================== -->
<!-- Demonstrates how to create and update a reactive state variable using $state. -->
<!-- Filename: Counter.svelte -->
<script>
let count = $state(0); // count is reactive
</script>
<button onclick={() => count++}>
clicks: {count} <!-- UI updates automatically when count changes -->
</button>
<!-- Notes: -->
<!-- - $state makes `count` reactive. -->
<!-- - You can modify `count` like a normal variable, no need for a special setter. -->
<!-- ================================== -->
<!-- Example 2: Deep Reactivity with Arrays -->
<!-- ================================== -->
<!-- Demonstrates how $state makes arrays deeply reactive. -->
<!-- Filename: TodoList.svelte -->
<script>
let todos = $state([
{ done: false, text: 'Learn Svelte' },
{ done: false, text: 'Build an app' }
]);
function toggle(index) {
todos[index].done = !todos[index].done; // Reactivity applies to individual items
}
</script>
<ul>
{#each todos as todo, index}
<li>
<input type="checkbox" bind:checked={todo.done} onclick={() => toggle(index)} />
{todo.text}
</li>
{/each}
</ul>
<button onclick={() => todos.push({ done: false, text: 'New task' })}>
Add Todo
</button>
<!-- Notes: -->
<!-- - `todos` is a reactive proxy, meaning updates to properties trigger UI updates. -->
<!-- - `todos.push()` automatically makes new objects reactive. -->
<!-- ================================== -->
<!-- Example 3: Reactivity in Classes -->
<!-- ================================== -->
<!-- Demonstrates using $state inside a class to create reactive properties. -->
<!-- Filename: TodoClassExample.svelte -->
<script>
class Todo {
done = $state(false);
text = $state('');
constructor(text) {
this.text = text;
}
reset = () => {
this.text = '';
this.done = false;
}
}
let todo = new Todo('Buy groceries');
</script>
<p>{todo.text} - {todo.done ? 'Done' : 'Not done'}</p>
<button onclick={() => todo.done = !todo.done}>Toggle</button>
<button onclick={() => todo.reset()}>Reset</button>
<!-- Notes: -->
<!-- - Using an arrow function for `reset` ensures `this` remains correct. -->
<!-- - Class properties become reactive with $state. -->
<!-- ================================== -->
<!-- Example 4: Using $state.raw -->
<!-- ================================== -->
<!-- Demonstrates when to use $state.raw to avoid deep reactivity. -->
<!-- Filename: RawStateExample.svelte -->
<script>
let person = $state.raw({
name: 'Alice',
age: 30
});
function updatePerson() {
// This won't work because `person` is raw (not reactive)
person.age += 1;
// Instead, we must reassign the whole object
person = { ...person, age: person.age + 1 };
}
</script>
<p>{person.name} is {person.age} years old.</p>
<button onclick={updatePerson}>Increase Age</button>
<!-- Notes: -->
<!-- - `person.age += 1` does nothing because $state.raw objects are immutable. -->
<!-- - To update, replace the entire object. -->
<!-- ================================== -->
<!-- Example 5: Taking a Snapshot of Reactive State -->
<!-- ================================== -->
<!-- Demonstrates how to extract a non-proxied snapshot of a reactive object. -->
<!-- Filename: SnapshotExample.svelte -->
<script>
let counter = $state({ count: 0 });
function logSnapshot() {
console.log($state.snapshot(counter)); // Logs a plain object, not a Proxy
}
</script>
<p>Count: {counter.count}</p>
<button onclick={() => counter.count++}>Increment</button>
<button onclick={logSnapshot}>Log Snapshot</button>
<!-- Notes: -->
<!-- - `snapshot` removes reactivity, making it safe to pass to external libraries. -->
<!-- ================================== -->
<!-- Example 6: Passing Reactive State to Functions -->
<!-- ================================== -->
<!-- Demonstrates passing a reactive state into a function and ensuring it remains reactive. -->
<!-- Filename: FunctionStateExample.svelte -->
<script>
function add(getA, getB) {
return () => getA() + getB();
}
let a = $state(1);
let b = $state(2);
let total = add(() => a, () => b); // Pass getters to maintain reactivity
</script>
<p>{a} + {b} = {total()}</p>
<button onclick={() => a++}>Increment A</button>
<button onclick={() => b++}>Increment B</button>
<!-- Notes: -->
<!-- - Directly passing `a` and `b` would not be reactive. -->
<!-- - Using getter functions ensures the function always retrieves the latest values. -->
<!-- ================================== -->
<!-- Extra: Optional Getter/Setter Binding -->
<!-- ================================== -->
<!-- Shows how to bind an input to a reactive value with a custom getter/setter. -->
<script>
let name = $state('Alice');
</script>
<input bind:value={
() => name,
(v) => name = v.trimStart()
} />
<p>Hello, {name}!</p>
<!-- Notes:
- This uses the new bind: syntax (Svelte 5.9+) with a getter and setter.
- Useful for applying validation or transformation automatically.
-->
$Derived
<!-- ================================== -->
<!-- Example 1: Basic Derived State -->
<!-- ================================== -->
<!-- Demonstrates how `$derived` updates a value based on `$state`. -->
<!-- Filename: App.svelte -->
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2); // `doubled` automatically updates when `count` changes
</script>
<button onclick={() => count++}>
Increment
</button>
<p>{count} doubled is {doubled}</p>
<!-- Notes: -->
$derived(count * 2)
ensures doubled
always reflects the latest count
.
- You cannot modify state inside
$derived
, e.g., count++
inside the expression.
<!-- ================================== -->
<!-- Example 2: Using $derived.by for Complex Computation -->
<!-- ================================== -->
<!-- Demonstrates how `$derived.by` allows more complex calculations. -->
<!-- Filename: App.svelte -->
<script lang="ts">
let numbers = $state([1, 2, 3]);
// `total` is automatically recalculated when `numbers` change
let total = $derived.by(() => {
let sum = 0;
for (const n of numbers) {
sum += n;
}
return sum;
});
</script>
<button onclick={() => numbers.push(numbers.length + 1)}>
Add Number
</button>
<p>{numbers.join(' + ')} = {total}</p>
<!-- Notes: -->
$derived.by(() => { ... })
is useful when the derived state requires loops or multiple steps.
- The derived state (
total
) automatically updates when numbers
change.
<!-- ================================== -->
<!-- Example 3: Understanding Dependencies -->
<!-- ================================== -->
<!-- Shows how derived state tracks dependencies and updates accordingly. -->
<!-- Filename: App.svelte -->
<script lang="ts">
let count = $state(0);
let isEven = $derived(count % 2 === 0);
</script>
<button onclick={() => count++}>
Increment
</button>
<p>{count} is {isEven ? 'Even' : 'Odd'}</p>
<!-- Notes: -->
$derived
tracks all synchronously accessed state inside its expression.
- If
count
changes, isEven
will automatically update.
<!-- ================================== -->
<!-- Example 4: Avoiding Unnecessary Updates -->
<!-- ================================== -->
<!-- Demonstrates how Svelte skips updates when derived values remain the same. -->
<!-- Filename: App.svelte -->
<script lang="ts">
let count = $state(0);
let large = $derived(count > 10);
</script>
<button onclick={() => count++}>
Increment
</button>
<p>Is count large? {large}</p>
<!-- Notes: -->
- Svelte only updates when
large
changes (false
β true
or vice versa).
- If
count
changes within the same threshold (e.g., 5 β 6
), no update occurs.
$Effect
<!-- ================================== -->
<!-- Example 1: Basic Usage of $effect -->
<!-- ================================== -->
<!-- Demonstrates how to use `$effect` to reactively update a canvas when state changes. -->
<!-- Filename: Canvas.svelte -->
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// This effect runs whenever `color` or `size` changes
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
<input type="color" bind:value={color} />
<input type="range" bind:value={size} min="10" max="100" />
<!-- Notes:
- `$effect` automatically tracks dependencies (`size`, `color`).
- Runs only in the browser, not during SSR.
- Avoid modifying state inside an effect to prevent infinite loops.
-->
<!-- ================================== -->
<!-- Example 2: Using Teardown in $effect -->
<!-- ================================== -->
<!-- Demonstrates how to return a cleanup function inside `$effect` to clear intervals. -->
<!-- Filename: Counter.svelte -->
<script>
let count = $state(0);
let milliseconds = $state(1000);
$effect(() => {
const interval = setInterval(() => {
count += 1;
}, milliseconds);
// Cleanup function runs before effect re-runs or on component destroy
return () => {
clearInterval(interval);
};
});
</script>
<h1>{count}</h1>
<button onclick={() => (milliseconds *= 2)}>Slower</button>
<button onclick={() => (milliseconds /= 2)}>Faster</button>
<!-- Notes:
- The interval is cleared before the next effect run or when the component is destroyed.
- If `milliseconds` changes, the old interval is removed before a new one is created.
-->
<!-- ================================== -->
<!-- Example 3: Dependency Tracking in $effect -->
<!-- ================================== -->
<!-- Demonstrates how `$effect` only tracks dependencies accessed synchronously. -->
<!-- Filename: AsyncCanvas.svelte -->
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// This effect re-runs when `color` changes but not when `size` changes
context.fillStyle = color;
setTimeout(() => {
context.fillRect(0, 0, size, size);
}, 0);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
<input type="color" bind:value={color} />
<input type="range" bind:value={size} min="10" max="100" />
<!-- Notes:
- `size` is accessed inside `setTimeout`, so it is NOT a tracked dependency.
- The effect runs when `color` changes, but `size` updates do not trigger a re-run.
-->
<!-- ================================== -->
<!-- Example 4: $effect.pre for Pre-DOM Updates -->
<!-- ================================== -->
<!-- Demonstrates how `$effect.pre` can be used to run logic before the DOM updates. -->
<!-- Filename: AutoScroll.svelte -->
<script>
import { tick } from 'svelte';
let div = $state();
let messages = $state([]);
$effect.pre(() => {
if (!div) return;
// Tracks `messages.length` so the effect re-runs when messages change
messages.length;
// Auto-scroll logic before the DOM updates
if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) {
tick().then(() => {
div.scrollTo(0, div.scrollHeight);
});
}
});
</script>
<div bind:this={div}>
{#each messages as message}
<p>{message}</p>
{/each}
</div>
<button onclick={() => messages = [...messages, 'New message']}>Add Message</button>
<!-- Notes:
- `$effect.pre` runs before the DOM updates.
- Useful for cases like auto-scrolling where timing is important.
-->
<!-- ================================== -->
<!-- Example 5: Avoiding Infinite Loops in $effect -->
<!-- ================================== -->
<!-- Demonstrates the correct way to derive state instead of modifying it within `$effect`. -->
<!-- Filename: DerivedState.svelte -->
<script>
let count = $state(0);
let doubled = $derived(count * 2); // Correct approach
// Incorrect approach (causes infinite loops):
// $effect(() => {
// doubled = count * 2;
// });
</script>
<h1>{count} doubled is {doubled}</h1>
<button onclick={() => count += 1}>Increment</button>
<!-- Notes:
- Use `$derived` instead of modifying state inside `$effect`.
- Prevents unnecessary re-renders and infinite loops.
-->
$Bindable
<!-- ================================== -->
<!-- Example 1: Basic Two-Way Binding with $bindable -->
<!-- ================================== -->
<!-- Demonstrates how to use `$bindable` to create a prop that can be bound to a parent component. -->
<!-- Filename: FancyInput.svelte -->
<script lang="ts">
let { value = $bindable(), ...props } = $props(); // `value` is bindable, allowing two-way data flow
</script>
<input bind:value={value} {...props} />
<!-- Filename: App.svelte -->
<script lang="ts">
import FancyInput from './FancyInput.svelte';
let message = $state('hello'); // Using `$state` to create a reactive state variable
</script>
<FancyInput bind:value={message} />
<p>{message}</p> <!-- This will update as the user types in the input -->
<!-- Notes: -->
<!-- - The `bind:value` directive ensures that `message` in the parent updates when the input changes. -->
<!-- - This is useful for creating reusable, controlled input components. -->
<!-- - The parent is not required to bind to `value`; it can simply pass a normal prop. -->
<!-- ================================== -->
<!-- Example 2: Default Fallback for Bindable Props -->
<!-- ================================== -->
<!-- Shows how to provide a default value when the parent does not pass a bindable prop. -->
<!-- Filename: FancyInput.svelte -->
<script lang="ts">
let { value = $bindable('fallback'), ...props } = $props(); // Default value set to 'fallback'
</script>
<input bind:value={value} {...props} />
<!-- Filename: App.svelte -->
<script lang="ts">
import FancyInput from './FancyInput.svelte';
</script>
<FancyInput /> <!-- No `bind:value`, so it defaults to 'fallback' -->
<!-- Notes: -->
<!-- - If `value` is not provided by the parent, it starts with 'fallback'. -->
<!-- - This is useful for ensuring a component always has a meaningful initial state. -->
<!-- ================================== -->
<!-- Example 3: Using $bindable with Multiple Props -->
<!-- ================================== -->
<!-- Demonstrates how to use multiple `$bindable` props in a single component. -->
<!-- Filename: FancyInput.svelte -->
<script lang="ts">
let { value = $bindable('default text'), disabled = $bindable(false), ...props } = $props();
</script>
<input bind:value={value} bind:disabled={disabled} {...props} />
<!-- Filename: App.svelte -->
<script lang="ts">
import FancyInput from './FancyInput.svelte';
let message = $state('Editable text');
let isDisabled = $state(false);
</script>
<FancyInput bind:value={message} bind:disabled={isDisabled} />
<button onclick={() => isDisabled = !isDisabled}>
Toggle Disabled
</button>
<!-- Notes: -->
- The input's
value
and disabled
states can both be bound to the parent.
- The button toggles the
disabled
state dynamically.
- Useful for forms and UI elements requiring controlled states.
$Props
<!-- ================================== -->
<!-- Example 1: Basic Prop Usage with $props -->
<!-- ================================== -->
<!-- Demonstrates how to receive props in a Svelte component using $props -->
<!-- Filename: App.svelte -->
<script lang="ts">
import MyComponent from './MyComponent.svelte';
</script>
<MyComponent adjective="cool" />
<!-- Filename: MyComponent.svelte -->
<script lang="ts">
let props = $props();
</script>
<p>This component is {props.adjective}</p>
<!-- Explanation:
- `MyComponent` receives a prop `adjective`.
- The `$props` rune allows access to the props object inside `MyComponent`.
- `props.adjective` displays the value passed from `App.svelte`.
-->
<!-- ================================== -->
<!-- Example 2: Destructuring Props -->
<!-- ================================== -->
<!-- Demonstrates how to destructure props for cleaner code -->
<!-- Filename: MyComponent.svelte -->
<script lang="ts">
let { adjective } = $props();
</script>
<p>This component is {adjective}</p>
<!-- Explanation:
- Instead of accessing `props.adjective`, we destructure `adjective` directly from `$props()`.
- This makes the code more concise and readable.
-->
<!-- ================================== -->
<!-- Example 3: Fallback Values for Props -->
<!-- ================================== -->
<!-- Demonstrates how to provide default values when a prop is not passed -->
<!-- Filename: MyComponent.svelte -->
<script lang="ts">
let { adjective = 'happy' } = $props();
</script>
<p>This component is {adjective}</p>
<!-- Explanation:
- If `adjective` is not provided by the parent, it defaults to `'happy'`.
- The fallback value does **not** become reactive.
-->
<!-- ================================== -->
<!-- Example 4: Renaming Props -->
<!-- ================================== -->
<!-- Demonstrates how to rename props when destructuring -->
<!-- Filename: MyComponent.svelte -->
<script lang="ts">
let { super: trouper = 'lights are gonna find me' } = $props();
</script>
<p>Song lyric: {trouper}</p>
<!-- Explanation:
- `super` is a reserved keyword in JavaScript.
- We rename `super` to `trouper` while destructuring.
- It also includes a default fallback value.
-->
<!-- ================================== -->
<!-- Example 5: Using Rest Props -->
<!-- ================================== -->
<!-- Demonstrates how to collect all remaining props using the rest syntax -->
<!-- Filename: MyComponent.svelte -->
<script lang="ts">
let { a, b, c, ...others } = $props();
</script>
<p>a: {a}, b: {b}, c: {c}</p>
<p>Other props: {JSON.stringify(others)}</p>
<!-- Explanation:
- `a`, `b`, and `c` are extracted explicitly.
- The rest of the props are collected into the `others` object.
- This can be useful for passing arbitrary props to a child component.
-->
<!-- ================================== -->
<!-- Example 6: Updating Props (Ephemeral State) -->
<!-- ================================== -->
<!-- Demonstrates how prop values update when the parent changes, but can be overridden temporarily -->
<!-- Filename: App.svelte -->
<script lang="ts">
import Child from './Child.svelte';
let count = $state(0);
</script>
<button onclick={() => count += 1}>
clicks (parent): {count}
</button>
<Child {count} />
<!-- Filename: Child.svelte -->
<script lang="ts">
let { count } = $props();
</script>
<button onclick={() => count += 1}>
clicks (child): {count}
</button>
<!-- Explanation:
- The `count` prop updates when changed in `App.svelte`.
- However, inside `Child.svelte`, `count` can be modified temporarily.
- This is useful for unsaved, ephemeral state.
-->
<!-- ================================== -->
<!-- Example 7: Avoiding Object Prop Mutation -->
<!-- ================================== -->
<!-- Demonstrates how object mutations do not work if props are plain objects -->
<!-- Filename: App.svelte -->
<script lang="ts">
import Child from './Child.svelte';
</script>
<Child object={{ count: 0 }} />
<!-- Filename: Child.svelte -->
<script lang="ts">
let { object } = $props();
</script>
<button onclick={() => object.count += 1}>
clicks: {object.count}
</button>
<!-- Explanation:
- The object prop `{ count: 0 }` is passed to `Child.svelte`.
- Mutating `object.count` **does not update the parent**.
- This is because plain objects are not reactive.
-->
<!-- ================================== -->
<!-- Example 8: Mutating a Reactive Prop (Warning) -->
<!-- ================================== -->
<!-- Demonstrates the warning when mutating a reactive state prop -->
<!-- Filename: App.svelte -->
<script lang="ts">
import Child from './Child.svelte';
let object = $state({ count: 0 });
</script>
<Child {object} />
<!-- Filename: Child.svelte -->
<script lang="ts">
let { object } = $props();
</script>
<button onclick={() => object.count += 1}>
clicks: {object.count}
</button>
<!-- Explanation:
- `object` is a `$state` reactive proxy.
- Mutating `object.count` inside `Child.svelte` **does work**, but shows a warning.
- This warns against mutating state that does not "belong" to the child.
-->
<!-- ================================== -->
<!-- Example 9: Using $props.id() for Unique IDs -->
<!-- ================================== -->
<!-- Demonstrates how to generate unique component-scoped IDs -->
<!-- Filename: MyComponent.svelte -->
<script lang="ts">
const uid = $props.id();
</script>
<form>
<label for="{uid}-firstname">First Name: </label>
<input id="{uid}-firstname" type="text" />
<label for="{uid}-lastname">Last Name: </label>
<input id="{uid}-lastname" type="text" />
</form>
<!-- Explanation:
- `$props.id()` generates a unique ID for this component instance.
- This is useful for accessibility (`for` and `aria-labelledby` attributes).
- Ensures consistency between server and client during hydration.
-->
$Host
<!-- ================================== -->
<!-- Example 1: Using $host to Dispatch Events -->
<!-- ================================== -->
<!-- Demonstrates how to use $host inside a custom element to dispatch events. -->
<!-- Filename: Stepper.svelte -->
<svelte:options customElement="my-stepper" />
<script lang="ts">
function dispatch(type) {
$host().dispatchEvent(new CustomEvent(type));
// $host() gives access to the custom element itself.
// Dispatches 'increment' or 'decrement' events.
}
</script>
<button onclick={() => dispatch('decrement')}>decrement</button>
<button onclick={() => dispatch('increment')}>increment</button>
<!-- Filename: App.svelte -->
<script lang="ts">
import './Stepper.svelte';
let count = $state(0); // State variable to track count
</script>
<my-stepper
ondecrement={() => count -= 1}
onincrement={() => count += 1}
</my-stepper>
<p>count: {count}</p> <!-- Displays updated count -->
<!-- Notes -->
$host()
only works in components compiled as custom elements (<svelte:options customElement="..."/>
).
- This approach allows dispatching custom events from a shadow DOM encapsulated component.
- Ensure the parent component listens to the dispatched events (
ondecrement
, onincrement
).
$Inspect
<!-- ================================== -->
<!-- Example 1: Basic $inspect Usage -->
<!-- ================================== -->
<!-- Demonstrates how `$inspect` logs changes to reactive state in development mode. -->
<!-- Filename: App.svelte -->
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // Logs `count` and `message` whenever they change
</script>
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
<!-- Notes:
- `$inspect` will log changes to `count` and `message` automatically.
- It only works during development and is ignored in production.
- Useful for debugging reactive state.
-->
<!-- ================================== -->
<!-- Example 2: Custom Logging with .with -->
<!-- ================================== -->
<!-- Demonstrates how `$inspect(...).with` replaces default logging with a custom function. -->
<!-- Filename: App.svelte -->
<script>
let count = $state(0);
$inspect(count).with((type, count) => {
if (type === 'update') {
console.log(`Count updated to: ${count}`);
}
});
</script>
<button onclick={() => count++}>Increment</button>
<!-- Notes:
- Instead of logging automatically, `$inspect(count).with(...)` calls a custom function.
- The callback receives `type` (`"init"` or `"update"`) and the updated values.
- Useful for debugging or conditional logging.
-->
<!-- ================================== -->
<!-- Example 3: Tracing Changes with console.trace -->
<!-- ================================== -->
<!-- Demonstrates how to trace reactive state changes using `$inspect(...).with(console.trace)`. -->
<!-- Filename: App.svelte -->
<script>
let message = $state('Hello');
$inspect(message).with(console.trace);
</script>
<input bind:value={message} />
<!-- Notes:
- Using `console.trace` with `$inspect.with()` helps track where state changes originate.
- Best used when debugging complex reactive flows.
-->
<!-- ================================== -->
<!-- Example 4: Tracing Effects with $inspect.trace -->
<!-- ================================== -->
<!-- Demonstrates how `$inspect.trace()` tracks function re-execution due to reactive state changes. -->
<!-- Filename: App.svelte -->
<script>
import { doSomeWork } from './utils';
$effect(() => {
$inspect.trace(); // Logs which state changes caused this effect to run
doSomeWork();
});
</script>
<!-- Notes:
- `$inspect.trace()` is useful inside `$effect()` to track reactivity.
- When the effect re-runs, it logs which state triggered the change.
- Added in Svelte 5.14.
-->
Svelte 5 Event Handling
<!-- ================================== -->
<!-- Example 1: Basic Event Handling -->
<!-- ================================== -->
Svelte 5 replaces on:
directives with direct property bindings for event handlers.
<!-- Filename: App.svelte -->
<script>
let count = $state(0);
function onclick() {
count++;
}
</script>
<button {onclick}>
clicks: {count}
</button>
Key changes:
- Removed
on:click
, replacing it with {onclick}
as a property.
- Uses a named function (
onclick
) instead of inline arrow functions.
<!-- ================================== -->
<!-- Example 2: Component Event Callbacks -->
<!-- ================================== -->
In Svelte 5, components no longer use createEventDispatcher()
. Instead, they accept callback props.
<!-- Filename: Pump.svelte -->
<script lang="ts">
let { inflate, deflate } = $props(); // Props passed from parent
let power = $state(5);
</script>
<button onclick={() => inflate(power)}>inflate</button>
<button onclick={() => deflate(power)}>deflate</button>
<button onclick={() => power--}>-</button>
Pump power: {power}
<button onclick={() => power++}>+</button>
<!-- Filename: App.svelte -->
<script lang="ts">
import Pump from './Pump.svelte';
let size = $state(15);
let burst = $state(false);
function inflateHandler(power) {
size += power;
if (size > 75) burst = true;
}
function deflateHandler(power) {
if (size > 0) size -= power;
}
function reset() {
size = 15;
burst = false;
}
</script>
<Pump inflate={inflateHandler} deflate={deflateHandler} />
{#if burst}
<button onclick={reset}>new balloon</button>
<span class="boom">π₯</span>
{:else}
<span class="balloon" style="scale: {0.01 * size}">
π
</span>
{/if}
Key changes:
- Removed
createEventDispatcher()
, instead using callback props (inflate
, deflate
).
- Events are now just function calls inside
Pump.svelte
.
- Parent (App.svelte) passes handlers like
inflateHandler
to Pump
.
<!-- ================================== -->
<!-- Example 3: Bubbling Events with Callback Props -->
<!-- ================================== -->
To forward an event from an element to a component, use callback props instead of on:
.
<!-- Filename: Clickable.svelte -->
<script>
let { onclick } = $props(); // Accepts an `onclick` function from parent
</script>
<button {onclick}>
click me
</button>
Key changes:
- No need to manually forward events like
on:click
β {onclick}
does the job.
<!-- ================================== -->
<!-- Example 4: Spreading Props Including Event Handlers -->
<!-- ================================== -->
Instead of manually listing every event, just spread props.
<!-- Filename: Button.svelte -->
<script>
let props = $props();
</script>
<button {...props}>
click me
</button>
Key changes:
{...props}
automatically applies all attributes, including event handlers like onclick
.
<!-- ================================== -->
<!-- Example 5: Multiple Event Handlers on the Same Element -->
<!-- ================================== -->
Instead of duplicate event bindings, combine handlers manually.
<!-- Filename: App.svelte -->
<script>
function one(event) {
console.log('First handler', event);
}
function two(event) {
console.log('Second handler', event);
}
</script>
<button
onclick={(e) => {
one(e);
two(e);
}}
>
...
</button>
Key changes:
- Instead of multiple
on:click={one} on:click={two}
, handlers are manually merged.
<!-- ================================== -->
<!-- Example 6: Preventing Local Handlers from Being Overwritten -->
<!-- ================================== -->
Ensure that when spreading props, local event handlers still execute.
<!-- Filename: App.svelte -->
<script>
let props = $props();
function doStuff(event) {
console.log('Handled locally', event);
}
</script>
<button
{...props}
onclick={(e) => {
doStuff(e);
props.onclick?.(e);
}}
>
...
</button>
Key changes:
- The local
onclick
executes before the props.onclick
, preventing overwrites.
<!-- ================================== -->
<!-- Example 7: Event Modifiers as Functions -->
<!-- ================================== -->
Modifiers like once
and preventDefault
should now be implemented manually.
<!-- Filename: App.svelte -->
<script>
function once(fn) {
return function (event) {
if (fn) fn.call(this, event);
fn = null; // Ensures it only runs once
};
}
function preventDefault(fn) {
return function (event) {
event.preventDefault();
fn.call(this, event);
};
}
function handler(event) {
console.log('Button clicked');
}
</script>
<button onclick={once(preventDefault(handler))}>
...
</button>
Key changes:
- Used higher-order functions to replace
on:click|once|preventDefault={handler}
.
// svelte/events - on
function
// on
attaches an event handler and returns a cleanup function.
// It ensures correct handler order compared to addEventListener
.
import { on } from 'svelte/events';
// Attach an event to window
function on<Type extends keyof WindowEventMap>(
window: Window,
type: Type,
handler: (this: Window, event: WindowEventMap[Type]) => any,
options?: AddEventListenerOptions
): () => void;
// Attach an event to document
function on<Type extends keyof DocumentEventMap>(
document: Document,
type: Type,
handler: (this: Document, event: DocumentEventMap[Type]) => any,
options?: AddEventListenerOptions
): () => void;
// Attach an event to an HTMLElement
function on<Element extends HTMLElement, Type extends keyof HTMLElementEventMap>(
element: Element,
type: Type,
handler: (this: Element, event: HTMLElementEventMap[Type]) => any,
options?: AddEventListenerOptions
): () => void;
// Attach an event to a MediaQueryList
function on<Element extends MediaQueryList, Type extends keyof MediaQueryListEventMap>(
element: Element,
type: Type,
handler: (this: Element, event: MediaQueryListEventMap[Type]) => any,
options?: AddEventListenerOptions
): () => void;
// Generic event attachment
function on(
element: EventTarget,
type: string,
handler: EventListener,
options?: AddEventListenerOptions
): () => void;
Use on:eventname={handler}
to attach handlers (e.g., onclick={() => ...}
).
- Handlers can mutate
$state
directly; UI updates automatically.
- Shorthand
{eventname}
works if the handler is a variable or function.
- Events like
click
, input
, etc., are delegated; assume bubbling unless stopped.
- Events fire after bindings (e.g.,
oninput
after bind:value
).
- Case-sensitive:
onclick
β onClick
(custom events may use uppercase).
<script>
let count = $state(0); // Reactive state
let text = $state(''); // Input value
const logClick = () => console.log('Clicked!'); // Named handler
// $effect to react to state changes
$effect(() => console.log(`Count is now ${count}`));
</script>
<!-- Inline handler with $state -->
<button onclick={() => count++}>
Increment: {count}
</button>
<!-- Shorthand with named handler -->
<button {logClick}>
Log Click
</button>
<!-- Input event with binding -->
<input
oninput={(e) => text = e.target.value}
bind:value={text}
placeholder="Type here"
/>
<p>You typed: {text}</p>
<!-- Keydown event with condition -->
<input
onkeydown={(e) => e.key === 'Enter' && count++}
placeholder="Press Enter to increment"
/>
Note: For other events (e.g., onmouseover
, onfocus
), follow the same pattern:
on:eventname={() => ...}
or {eventname}
. Adjust the handler logic based on the eventβs purpose.
Svelte 5 Snippets
<!-- ================================== -->
<!-- Example 1: Basic Snippet Usage -->
<!-- ================================== -->
<!-- Demonstrates how to create and use a snippet to avoid repetitive markup -->
<!-- Filename: ImageList.svelte -->
<script>
let images = [
{ src: "image1.jpg", caption: "Image 1", href: "https://example.com" },
{ src: "image2.jpg", caption: "Image 2" }
];
</script>
{#snippet figure(image)}
<figure>
<img src={image.src} alt={image.caption} />
<figcaption>{image.caption}</figcaption>
</figure>
{/snippet}
{#each images as image}
{#if image.href}
<a href={image.href}>
{@render figure(image)}
</a>
{:else}
{@render figure(image)}
{/if}
{/each}
<!-- This example shows how snippets reduce duplication in templates -->
<!-- ================================== -->
<!-- Example 2: Snippet Scope and Visibility -->
<!-- ================================== -->
<!-- Demonstrates snippet scoping rules and visibility -->
<!-- Filename: ScopedSnippets.svelte -->
<div>
{#snippet x()}
{#snippet y()}
<p>Inside y</p>
{/snippet}
<!-- Rendering `y` inside `x` is allowed -->
{@render y()}
{/snippet}
<!-- This will cause an error because `y` is out of scope -->
{@render y()}
</div>
<!-- This will also cause an error because `x` is out of scope -->
{@render x()}
<!-- Notes: Snippets are only accessible within their lexical scope -->
<!-- ================================== -->
<!-- Example 3: Recursive Snippets -->
<!-- ================================== -->
<!-- Demonstrates how a snippet can reference itself -->
<!-- Filename: Countdown.svelte -->
{#snippet blastoff()}
<span>π</span>
{/snippet}
{#snippet countdown(n)}
{#if n > 0}
<span>{n}...</span>
{@render countdown(n - 1)}
{:else}
{@render blastoff()}
{/if}
{/snippet}
{@render countdown(5)}
<!-- Recursive snippets are useful for countdowns, trees, and nested structures -->
<!-- ================================== -->
<!-- Example 4: Passing Snippets to Components -->
<!-- ================================== -->
<!-- Demonstrates passing snippets as props to a component -->
<!-- Filename: Table.svelte -->
<script>
let { data, header, row } = $props();
</script>
<table>
<thead>
<tr>{@render header()}</tr>
</thead>
<tbody>
{#each data as item}
<tr>{@render row(item)}</tr>
{/each}
</tbody>
</table>
<!-- Filename: App.svelte -->
<script>
import Table from "./Table.svelte";
const fruits = [
{ name: "Apples", qty: 5, price: 2 },
{ name: "Bananas", qty: 10, price: 1 },
{ name: "Cherries", qty: 20, price: 0.5 }
];
</script>
{#snippet header()}
<th>Fruit</th>
<th>Qty</th>
<th>Price</th>
<th>Total</th>
{/snippet}
{#snippet row(d)}
<td>{d.name}</td>
<td>{d.qty}</td>
<td>{d.price}</td>
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<!-- Snippets allow passing reusable markup structures into components -->
<!-- ================================== -->
<!-- Example 5: Default Slot with Snippets -->
<!-- ================================== -->
<!-- Demonstrates how snippets replace slots in Svelte 5 -->
<!-- Filename: Button.svelte -->
<script>
let { children } = $props();
</script>
<button>{@render children()}</button>
<!-- Filename: App.svelte -->
<Button>Click Me</Button>
<!-- Any non-snippet content passed to a component becomes the `children` snippet -->
<!-- ================================== -->
<!-- Example 6: Optional Snippet Props -->
<!-- ================================== -->
<!-- Demonstrates handling optional snippet props -->
<!-- Filename: OptionalSnippet.svelte -->
<script>
let { children } = $props();
</script>
{#if children}
{@render children()}
{:else}
<p>Fallback content</p>
{/if}
<!-- Filename: App.svelte -->
<OptionalSnippet />
<OptionalSnippet>
{#snippet children()}
<p>Custom content</p>
{/snippet}
</OptionalSnippet>
<!-- Use `optional chaining` or `#if` blocks to handle missing snippets -->
<!-- ================================== -->
<!-- Example 7: Exporting Snippets -->
<!-- ================================== -->
<!-- Demonstrates exporting a snippet for use in other components -->
<!-- Filename: math.svelte -->
<script module>
export { add };
</script>
{#snippet add(a, b)}
{a} + {b} = {a + b}
{/snippet}
<!-- Filename: App.svelte -->
<script>
import { add } from "./math.svelte";
</script>
{@render add(2, 3)}
<!-- Note: You can pass snippets to components that use slots, but not vice versa -->
<!-- ================================== -->
<!-- Example 8: Multiple Content Placeholders -->
<!-- ================================== -->
<!-- Demonstrates using multiple snippets instead of named slots -->
<!-- Filename: Layout.svelte -->
<script>
let { header, main, footer } = $props();
</script>
<header>
{@render header?.()}
</header>
<main>
{@render main?.()}
</main>
<footer>
{@render footer?.()}
</footer>
<!-- Filename: App.svelte -->
<script>
import Layout from "./Layout.svelte";
</script>
<Layout>
{#snippet header()}
<h1>My Website</h1>
{/snippet}
{#snippet main()}
<p>Welcome to my website!</p>
{/snippet}
{#snippet footer()}
<p>Β© 2023 My Website</p>
{/snippet}
</Layout>
<!-- ================================== -->
<!-- Example 9: Passing Data to Parent with Snippets -->
<!-- ================================== -->
<!-- Demonstrates how snippets allow passing data from child to parent -->
<!-- Filename: List.svelte -->
<script>
let { items, item, empty } = $props();
</script>
{#if items.length}
<ul>
{#each items as entry}
<li>
{@render item(entry)}
</li>
{/each}
</ul>
{:else}
{@render empty?.()}
{/if}
<!-- Filename: App.svelte -->
<script>
import List from "./List.svelte";
const fruits = ['Apple', 'Banana', 'Cherry'];
</script>
<List items={fruits}>
{#snippet item(fruit)}
<span>{fruit}</span>
{/snippet}
{#snippet empty()}
<span>No fruits available</span>
{/snippet}
</List>
<!--
The child component passes data as parameters to the snippet function,
which the parent can then access and use within the snippet definition.
-->
Svelte 5 + TypeScript
<!-- ================================== -->
<!-- Example 1: Using TypeScript in Svelte Components -->
<!-- ================================== -->
<!-- Demonstrates basic TypeScript setup inside a Svelte component. -->
<!-- Filename: App.svelte -->
<script lang="ts">
let name: string = 'world';
function greet(name: string) {
alert(`Hello, ${name}!`);
}
</script>
<button onclick={() => greet(name)}>Greet</button>
Notes:
- Adding
lang="ts"
enables TypeScript.
- Type annotations (
string
) help with static checking.
- No runtime overhead since TypeScript removes type annotations at compile time.
<!-- ================================== -->
<!-- Example 2: Typing Component Props -->
<!-- ================================== -->
<!-- Shows how to type component props in Svelte. -->
<!-- Filename: Greeting.svelte -->
<script lang="ts">
interface Props {
name: string;
}
let { name }: Props = $props();
</script>
<p>Hello, {name}!</p>
<!-- Filename: App.svelte -->
<script lang="ts">
import Greeting from './Greeting.svelte';
</script>
<Greeting name="Alice" />
Notes:
Props
interface ensures name
is always a string.
$props()
extracts the componentβs props with proper typing.
<!-- ================================== -->
<!-- Example 3: Generic Component Props -->
<!-- ================================== -->
<!-- Demonstrates generic props for flexible typing. -->
<!-- Filename: List.svelte -->
<script lang="ts" generics="Item">
interface Props {
items: Item[];
select: (item: Item) => void;
}
let { items, select }: Props = $props();
</script>
{#each items as item}
<button onclick={() => select(item)}>{item}</button>
{/each}
<!-- Filename: App.svelte -->
<script lang="ts">
import List from './List.svelte';
const names = ['Alice', 'Bob', 'Charlie'];
function handleSelect(name: string) {
console.log('Selected:', name);
}
</script>
<List items={names} select={handleSelect} />
Notes:
generics="Item"
allows the component to accept any item type.
- Ensures
items
and select
function operate on the same type.
<!-- ================================== -->
<!-- Example 4: Typing $state -->
<!-- ================================== -->
<!-- Shows how to type state variables in Svelte. -->
<!-- Filename: Counter.svelte -->
<script lang="ts">
let count: number = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>Count: {count}</button>
Notes:
$state(0)
initializes a reactive variable with a type.
- Without an initial value, TypeScript infers
number | undefined
.
<!-- ================================== -->
<!-- Example 5: Typing Wrapper Components -->
<!-- ================================== -->
<!-- Demonstrates how to type wrapper components that extend native HTML elements. -->
<!-- Filename: Button.svelte -->
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
let { children, ...rest }: HTMLButtonAttributes = $props();
</script>
<button {...rest}>{@render children?.()}</button>
<!-- Filename: App.svelte -->
<script lang="ts">
import Button from './Button.svelte';
</script>
<Button onclick={() => alert('Clicked!')}>Click Me</Button>
Notes:
HTMLButtonAttributes
ensures Button.svelte
supports all standard button attributes.
...rest
spreads remaining props onto the <button>
.
<!-- ================================== -->
<!-- Example 6: Typing Custom Events and Attributes -->
<!-- ================================== -->
<!-- Demonstrates how to extend built-in DOM types for custom attributes and events. -->
<!-- Filename: additional-svelte-typings.d.ts -->
declare namespace svelteHTML {
interface IntrinsicElements {
'custom-element': { customProp: string; 'on:customEvent': (e: CustomEvent<any>) => void };
}
}
<!-- Filename: App.svelte -->
<script lang="ts">
function handleEvent(e: CustomEvent) {
console.log('Custom event:', e.detail);
}
</script>
<custom-element customProp="Hello" on:customEvent={handleEvent}></custom-element>
Notes:
- Extending
IntrinsicElements
allows TypeScript to recognize <custom-element>
and its props.
- Avoids TypeScript errors when using experimental or third-party web components.
Svelte 4 β 5 Migration Summary
Concise Summary (Svelte 4 β 5)
- New Runes API
- Reactive variables use
let count = $state(0);
- Derived values use
const double = $derived(count * 2);
- Side effects use
$effect(() => {
if (count > 5) alert('Count is too high!');
});
- Declaring Props
- All props come from
let {...} = $props();
- Example:
<script>
let { optional = 'unset', required } = $props();
</script>
- Renaming or forwarding props:
<script>
let { class: klass, ...rest } = $props();
</script>
<button class={klass} {...rest}>click me</button>
Below is a concise, mostly bullet-pointed summary of the changes, focusing on code snippets:
Event Changes
- Svelte 4 used
on:
directives.
- Svelte 5: Use regular properties like
onclick
instead (no :
).
Example:
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
clicks: {count}
</button>
Shorthand Example:
<script>
let count = $state(0);
function onclick() {
count++;
}
</script>
<button {onclick}>
clicks: {count}
</button>
Component Events
- Svelte 4:
createEventDispatcher
for emitting events.
- Svelte 5: Use callback props instead.
Example (App.svelte):
<script lang="ts">
import Pump from './Pump.svelte';
let size = $state(15);
let burst = $state(false);
function reset() {
size = 15;
burst = false;
}
</script>
<Pump
inflate={(power) => {
size += power;
if (size > 75) burst = true;
}}
deflate={(power) => {
if (size > 0) size -= power;
}}
/>
{#if burst}
<button onclick={reset}>new balloon</button>
<span class="boom">π₯</span>
{:else}
<span class="balloon" style="scale: {0.01 * size}">
π
</span>
{/if}
Example (Pump.svelte):
<script lang="ts">
let { inflate, deflate } = $props();
let power = $state(5);
</script>
<button onclick={() => inflate(power)}>inflate</button>
<button onclick={() => deflate(power)}>deflate</button>
<button onclick={() => power--}>-</button>
Pump power: {power}
<button onclick={() => power++}>+</button>
Bubbling Events
- No more
<button on:click>
to forward.
- Accept a callback prop like
onclick
:
<script>
let { onclick } = $props();
</script>
<button {onclick}>
click me
</button>
- Can βspreadβ handlers with other props:
<script>
let props = $props();
</script>
<button {...props}>
click me
</button>
Event Modifiers
- Svelte 4 example:
<button on:click|once|preventDefault={handler}>...
- Svelte 5: No built-in modifiers. Handle
event.preventDefault()
in the function or wrap manually.
Example Wrappers:
<script>
function once(fn) {
return function (event) {
if (fn) fn.call(this, event);
fn = null;
};
}
function preventDefault(fn) {
return function (event) {
event.preventDefault();
fn.call(this, event);
};
}
</script>
<button onclick={once(preventDefault(handler))}>...</button>
- For
capture
, do: onclickcapture={...}
- For
passive
or nonpassive
, use an action to attach the event.
Multiple Event Handlers
- Svelte 4 allowed
<button on:click={one} on:click={two}>
.
- Svelte 5: Only one
onclick
property. Combine them:
<button
onclick={(e) => {
one(e);
two(e);
}}
>
...
</button>
- When spreading props, place local handlers afterward to avoid overwriting:
<button
{...props}
onclick={(e) => {
doStuff(e);
props.onclick?.(e);
}}
>
...
</button>
Svelte 5 Changes - Condensed Bullet Points
Component Changes
- No More Class Components β Components are now functions.
- Instantiation β Use
mount
or hydrate
instead of new Component()
.
- Mount vs. Hydrate β
hydrate
picks up server-rendered HTML, otherwise identical to mount
.
- No
$on
, $set
, $destroy
$on
β Use events
property instead (though callbacks are preferred).
$set
β Use $state
for reactivity.
$destroy
β Use unmount
.
Code Examples
import { mount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {
target: document.getElementById("app"),
events: { event: callback } // Replacement for $on
});
const props = $state({ foo: 'bar' }); // Replacement for $set
props.foo = 'baz';
import { unmount } from 'svelte';
unmount(app); // Replacement for $destroy
Legacy Compatibility
- Use
createClassComponent
from svelte/legacy
if needed.
compatibility.componentApi
option enables auto-backwards compatibility.
export default {
compilerOptions: {
compatibility: {
componentApi: 4
}
}
};
Asynchronous Behavior
mount
and hydrate
are not synchronous.
- Use
flushSync()
if onMount must be guaranteed to run.
Server-Side Rendering (SSR)
- No
render()
method in components.
- Instead, use
render()
from svelte/server
.
import { render } from 'svelte/server';
import App from './App.svelte';
const { html, head } = render(App, { props: { message: 'hello' }});
- CSS not included by default β Use
css: 'injected'
to include styles.
Typing Changes
SvelteComponent
deprecated β Use Component
type.
ComponentEvents
& ComponentType
deprecated.
import type { Component } from 'svelte';
export declare const MyComponent: Component<{ foo: string }>;
bind:this Changes
- No longer returns
$set
, $on
, or $destroy
methods.
- Now only returns instance exports & property accessors (if enabled).
Dynamic Components
<svelte:component>
no longer needed.
- Components update dynamically when reassigned.
<script>
import A from './A.svelte';
import B from './B.svelte';
let Thing = $state();
</script>
<select bind:value={Thing}>
<option value={A}>A</option>
<option value={B}>B</option>
</select>
<!-- Both are now equivalent -->
<Thing />
<svelte:component this={Thing} />
Other Changes
- Dot Notation (
<foo.bar>
) now treated as a component, not an HTML tag.
- Whitespace Handling Simplified β Predictable behavior.
- Reserved
children
Prop β Cannot use children
as a prop name; it's reserved.