<script> let count = $state(0); // count is reactive </script>
<button onclick={() => count++}> clicks: {count}
<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>
-
{#each todos as todo, index}
- toggle(index)} /> {todo.text} {/each}
<button onclick={() => todos.push({ done: false, text: 'New task' })}> Add Todo
<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>
{todo.text} - {todo.done ? 'Done' : 'Not done'}
todo.done = !todo.done}>Toggle todo.reset()}>Reset<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>
{person.name} is {person.age} years old.
Increase Age<script> let counter = $state({ count: 0 }); function logSnapshot() { console.log($state.snapshot(counter)); // Logs a plain object, not a Proxy } </script>
Count: {counter.count}
counter.count++}>Increment Log Snapshot<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>
{a} + {b} = {total()}
a++}>Increment A b++}>Increment B<script> let name = $state('Alice'); </script>
<input bind:value={ () => name, (v) => name = v.trimStart() } />
Hello, {name}!
<script lang="ts"> let count = $state(0); let doubled = $derived(count * 2); // `doubled` automatically updates when `count` changes </script>
<button onclick={() => count++}> Increment
{count} doubled is {doubled}
$derived(count * 2)
ensuresdoubled
always reflects the latestcount
.- You cannot modify state inside
$derived
, e.g.,count++
inside the expression.
<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
{numbers.join(' + ')} = {total}
$derived.by(() => { ... })
is useful when the derived state requires loops or multiple steps.- The derived state (
total
) automatically updates whennumbers
change.
<script lang="ts"> let count = $state(0); let isEven = $derived(count % 2 === 0); </script>
<button onclick={() => count++}> Increment
{count} is {isEven ? 'Even' : 'Odd'}
$derived
tracks all synchronously accessed state inside its expression.- If
count
changes,isEven
will automatically update.
<script lang="ts"> let count = $state(0); let large = $derived(count > 10); </script>
<button onclick={() => count++}> Increment
Is count large? {large}
- 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.
<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>
<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>
<button onclick={() => (milliseconds *= 2)}>Slower <button onclick={() => (milliseconds /= 2)}>Faster
<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>
<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>
{message}
{/each}<button onclick={() => messages = [...messages, 'New message']}>Add Message
<script> let count = $state(0); let doubled = $derived(count * 2); // Correct approach // Incorrect approach (causes infinite loops): // $effect(() => { // doubled = count * 2; // }); </script>
<button onclick={() => count += 1}>Increment
<script lang="ts"> let { value = $bindable(), ...props } = $props(); // `value` is bindable, allowing two-way data flow </script>
<input bind:value={value} {...props} />
<script lang="ts"> import FancyInput from './FancyInput.svelte'; let message = $state('hello'); // Using `$state` to create a reactive state variable </script>
{message}
<script lang="ts"> let { value = $bindable('fallback'), ...props } = $props(); // Default value set to 'fallback' </script>
<input bind:value={value} {...props} />
<script lang="ts"> import FancyInput from './FancyInput.svelte'; </script>
<script lang="ts"> let { value = $bindable('default text'), disabled = $bindable(false), ...props } = $props(); </script>
<input bind:value={value} bind:disabled={disabled} {...props} />
<script lang="ts"> import FancyInput from './FancyInput.svelte'; let message = $state('Editable text'); let isDisabled = $state(false); </script> isDisabled = !isDisabled}> Toggle Disabled
- The input's
value
anddisabled
states can both be bound to the parent. - The button toggles the
disabled
state dynamically. - Useful for forms and UI elements requiring controlled states.
<script lang="ts"> import MyComponent from './MyComponent.svelte'; </script>
<script lang="ts"> let props = $props(); </script>
This component is {props.adjective}
<script lang="ts"> let { adjective } = $props(); </script>
This component is {adjective}
<script lang="ts"> let { adjective = 'happy' } = $props(); </script>
This component is {adjective}
<script lang="ts"> let { super: trouper = 'lights are gonna find me' } = $props(); </script>
Song lyric: {trouper}
<script lang="ts"> let { a, b, c, ...others } = $props(); </script>
a: {a}, b: {b}, c: {c}
Other props: {JSON.stringify(others)}
<script lang="ts"> import Child from './Child.svelte'; let count = $state(0); </script>
<button onclick={() => count += 1}> clicks (parent): {count}
<Child {count} />
<script lang="ts"> let { count } = $props(); </script>
<button onclick={() => count += 1}> clicks (child): {count}
<script lang="ts"> import Child from './Child.svelte'; </script>
<Child object={{ count: 0 }} />
<script lang="ts"> let { object } = $props(); </script>
<button onclick={() => object.count += 1}> clicks: {object.count}
<script lang="ts"> import Child from './Child.svelte'; let object = $state({ count: 0 }); </script>
<Child {object} />
<script lang="ts"> let { object } = $props(); </script>
<button onclick={() => object.count += 1}> clicks: {object.count}
<script lang="ts"> const uid = $props.id(); </script> First Name:
<label for="{uid}-lastname">Last Name: </label>
<input id="{uid}-lastname" type="text" />
<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 onclick={() => dispatch('increment')}>increment
<script lang="ts"> import './Stepper.svelte'; let count = $state(0); // State variable to track count </script>
<my-stepper ondecrement={() => count -= 1} onincrement={() => count += 1}
count: {count}
$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
).
<script> let count = $state(0); let message = $state('hello'); $inspect(count, message); // Logs `count` and `message` whenever they change </script>
<button onclick={() => count++}>Increment
<script> let count = $state(0); $inspect(count).with((type, count) => { if (type === 'update') { console.log(`Count updated to: ${count}`); } }); </script>
<button onclick={() => count++}>Increment
<script> let message = $state('Hello'); $inspect(message).with(console.trace); </script>
<script> import { doSomeWork } from './utils'; $effect(() => { $inspect.trace(); // Logs which state changes caused this effect to run doSomeWork(); }); </script>
Svelte 5 replaces on:
directives with direct property bindings for event handlers.
<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.
In Svelte 5, components no longer use createEventDispatcher()
. Instead, they accept callback props.
<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>
<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
toPump
.
To forward an event from an element to a component, use callback props instead of on:
.
<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.
Instead of manually listing every event, just spread props.
<script>
let props = $props();
</script>
<button {...props}>
click me
</button>
Key changes:
{...props}
automatically applies all attributes, including event handlers likeonclick
.
Instead of duplicate event bindings, combine handlers manually.
<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.
Ensure that when spreading props, local event handlers still execute.
<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 theprops.onclick
, preventing overwrites.
Modifiers like once
and preventDefault
should now be implemented manually.
<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(
window: Window,
type: Type,
handler: (this: Window, event: WindowEventMap[Type]) => any,
options?: AddEventListenerOptions
): () => void;
// Attach an event to document
function on(
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
afterbind:value
). - Case-sensitive:
onclick
≠onClick
(custom events may use uppercase).
<button onclick={() => count++}> Increment: {count}
<button {logClick}> Log Click
<input oninput={(e) => text = e.target.value} bind:value={text} placeholder="Type here" />
You typed: {text}
<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.
<script> let images = [ { src: "image1.jpg", caption: "Image 1", href: "https://example.com" }, { src: "image2.jpg", caption: "Image 2" } ]; </script>
{#snippet figure(image)}
{image.caption} {/snippet}{#each images as image} {#if image.href} {@render figure(image)} {:else} {@render figure(image)} {/if} {/each}
Inside y
{/snippet} <!-- Rendering `y` inside `x` is allowed -->
{@render y()}
{/snippet}
<!-- This will cause an error because `y` is out of scope -->
{@render y()}
{@render x()}
{#snippet blastoff()} 🚀 {/snippet}
{#snippet countdown(n)} {#if n > 0} {n}... {@render countdown(n - 1)} {:else} {@render blastoff()} {/if} {/snippet}
{@render countdown(5)}
<script> let { data, header, row } = $props(); </script> {@render header()} {#each data as item} {@render row(item)} {/each}
<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()} Fruit Qty Price Total {/snippet}
{#snippet row(d)} {d.name} {d.qty} {d.price} {d.qty * d.price} {/snippet}
<script> let { children } = $props(); </script>
{@render children()}
Click Me
<script> let { children } = $props(); </script>
{#if children} {@render children()} {:else}
Fallback content
{/if}{#snippet children()}
Custom content
{/snippet}<script module> export { add }; </script>
{#snippet add(a, b)} {a} + {b} = {a + b} {/snippet}
<script> import { add } from "./math.svelte"; </script>
{@render add(2, 3)}
<script> let { header, main, footer } = $props(); </script> {@render header?.()} {@render main?.()} {@render footer?.()}
<script> import Layout from "./Layout.svelte"; </script> {#snippet header()} {/snippet}
{#snippet main()}
<p>Welcome to my website!</p>
{/snippet}
{#snippet footer()}
<p>© 2023 My Website</p>
{/snippet}
<script> let { items, item, empty } = $props(); </script>
{#if items.length}
-
{#each items as entry}
- {@render item(entry)} {/each}
<script> import List from "./List.svelte"; const fruits = ['Apple', 'Banana', 'Cherry']; </script> {#snippet item(fruit)} {fruit} {/snippet}
{#snippet empty()}
<span>No fruits available</span>
{/snippet}
<script lang="ts"> let name: string = 'world'; function greet(name: string) { alert(`Hello, ${name}!`); } </script>
<button onclick={() => greet(name)}>Greet
- Adding
lang="ts"
enables TypeScript. - Type annotations (
string
) help with static checking. - No runtime overhead since TypeScript removes type annotations at compile time.
<script lang="ts"> interface Props { name: string; } let { name }: Props = $props(); </script>
Hello, {name}!
<script lang="ts"> import Greeting from './Greeting.svelte'; </script>
Props
interface ensuresname
is always a string.$props()
extracts the component’s props with proper typing.
<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} {/each}
<script lang="ts"> import List from './List.svelte'; const names = ['Alice', 'Bob', 'Charlie']; function handleSelect(name: string) { console.log('Selected:', name); } </script>
generics="Item"
allows the component to accept any item type.- Ensures
items
andselect
function operate on the same type.
<script lang="ts"> let count: number = $state(0); function increment() { count += 1; } </script>
Count: {count}
$state(0)
initializes a reactive variable with a type.- Without an initial value, TypeScript infers
number | undefined
.
<script lang="ts"> import type { HTMLButtonAttributes } from 'svelte/elements'; let { children, ...rest }: HTMLButtonAttributes = $props(); </script>
<button {...rest}>{@render children?.()}
<script lang="ts"> import Button from './Button.svelte'; </script>
<Button onclick={() => alert('Clicked!')}>Click Me
HTMLButtonAttributes
ensuresButton.svelte
supports all standard button attributes....rest
spreads remaining props onto the<button>
.
declare namespace svelteHTML { interface IntrinsicElements { 'custom-element': { customProp: string; 'on:customEvent': (e: CustomEvent) => void }; } }
<script lang="ts"> function handleEvent(e: CustomEvent) { console.log('Custom event:', e.detail); } </script>
- Extending
IntrinsicElements
allows TypeScript to recognize<custom-element>
and its props. - Avoids TypeScript errors when using experimental or third-party web components.
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
- Reactive variables use
$effect(() => {
if (count > 5) alert('Count is too high!');
});
- Declaring Props
- All props come from
let {...} = $props();
- Example:
- All props come from
<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:
- 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>
- 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>
- 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>
- 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
ornonpassive
, use an action to attach the event.
- 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>
- No More Class Components → Components are now functions.
- Instantiation → Use
mount
orhydrate
instead ofnew Component()
. - Mount vs. Hydrate →
hydrate
picks up server-rendered HTML, otherwise identical tomount
. - No
$on
,$set
,$destroy
$on
→ Useevents
property instead (though callbacks are preferred).$set
→ Use$state
for reactivity.$destroy
→ Useunmount
.
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
- Use
createClassComponent
fromsvelte/legacy
if needed. compatibility.componentApi
option enables auto-backwards compatibility.
export default {
compilerOptions: {
compatibility: {
componentApi: 4
}
}
};
mount
andhydrate
are not synchronous.- Use
flushSync()
if onMount must be guaranteed to run.
- No
render()
method in components. - Instead, use
render()
fromsvelte/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.
SvelteComponent
deprecated → UseComponent
type.ComponentEvents
&ComponentType
deprecated.
import type { Component } from 'svelte';
export declare const MyComponent: Component<{ foo: string }>;
- No longer returns
$set
,$on
, or$destroy
methods. - Now only returns instance exports & property accessors (if enabled).
<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} />
- Dot Notation (
<foo.bar>
) now treated as a component, not an HTML tag. - Whitespace Handling Simplified → Predictable behavior.
- Reserved
children
Prop → Cannot usechildren
as a prop name; it's reserved.