Svelte

Markup

<script>
	let name = 'world'
	let src = '/tutorial/image.gif'
</script>

<h1>
	Hello {name.toUpperCase()}!
</h1>
<img src={src} alt="Photo of {name}">
<img {src} alt="Example">
<PackageInfo {...pkg} />

Flow control

if

<script>
	let x = 7
</script>

{#if x > 10}
	<p>{x} is greater than 10</p>
{:else if 5 > x}
	<p>{x} is less than 5</p>
{:else}
	<p>{x} is between 5 and 10</p>
{/if}

each

<script>
	let cats = [
		{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
		{ id: 'z_AbfPXTKms', name: 'Maru' },
		{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
	];
</script>

{#each cats as { id, name }, i (id)}
	<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
		{i + 1}: {name}
	</a></li>
{/each}
{#each { length: buttonCount }}
    <Button on:click={() => alert(`Button ${i} clicked`)} />
{/each}

await

{#await promise}
	<p>...waiting</p>
{:then number}
	<p>The number is {number}</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}
{#await promise then number}
    <p>The number is {number}</p>
{/await}

Snippets and children

{#snippet monkey(emoji, description)}
    <tr>
        <td>{emoji}</td>
        <td>{description}</td>
        <td>\u{emoji.charCodeAt(0).toString(16)}\u{emoji.charCodeAt(1).toString(16)}</td>
        <td>&amp#{emoji.codePointAt(0)}</td>
    </tr>
{/snippet}

{@render monkey('🙈', 'see no evil')}
{@render monkey('🙉', 'hear no evil')}
{@render monkey('🙊', 'speak no evil')}

<CustomTable rowFunction={monkey} />
<FilteredList data={colors} field="name">
	<header>
		<span class="color"></span>
		<span class="name">name</span>
		<span class="hex">hex</span>
		<span class="rgb">rgb</span>
		<span class="hsl">hsl</span>
	</header>

	{#snippet row(d)}
		<div class="row">
			<span class="color" style="background-color: {d.hex}"></span>
			<span class="name">{d.name}</span>
			<span class="hex">{d.hex}</span>
			<span class="rgb">{d.rgb}</span>
			<span class="hsl">{d.hsl}</span>
		</div>
	{/snippet}
</FilteredList>
<!-- FilteredList.svelte -->
<script>
    import data from './data'
    let { children, row } = $props()
</script>

<div class="header">
    {@render children()}
</div>

<div class="content">
    {#each data as d}
        {@render row(d)}
    {/each}
</div>

Special tags

<script>
    const string = 'Here is some <strong>HTML</strong>'
</script>

<p>{@html string}</p>
{#each boxes as box}
    {@const area = box.width * box.height}
    {box.width} * {box.height} = {area}
{/each}

Special elements

<script module>

<script module>
	let current;
</script>

<audio onplay={(e) => {
    if (e.currentTarget !== current) {
        current?.pause()
        current = e.currentTarget
    }
}}

Styling and animation

:global {
    .foo { }
    .bar { }
}
@keyframes -global-spin { ... }

.square {
    animation: 1s spin;
}

Classes

<button class={["card", selected && "selected", { flipped }]} />

Styles

<script>
    const bgOpacity = 0.5
    const color = blue
</script>

<p style:color style:--opacity={bgOpacity}></p>

Custom properties

// Box.svelte
<div class="box"></div>

<style>
    .box {
        background-color: var(--color, #ddd);
    }
</style>
// App.svelte
<script>
    const color = $state('blue')
</script>

<Box --color={color} />

Transitions

<script>
	import { fade } from 'svelte/transition';

	let visible = $state(true);
</script>

{#if visible}
	<p transition:fade>
		Fades in and out
	</p>
{/if}
<script>
	import { fly } from 'svelte/transition';

	let visible = $state(true);
</script>

{#if visible}
	<p transition:fly={{ y: 200, duration: 2000 }}>
		Flies in and out
	</p>
{/if}

{#key i}
	<p in:typewriter={{ speed: 10 }}>
		{messages[i] || ''}
	</p>
{/key}
{#if visible}
	<p in:fly={{ y: 200, duration: 2000 }} out:fade>
		Flies in, fades out
	</p>
{/if}

Animate moving elements in the DOM

Tweening

<script>
    import { Tween } from 'svelte/motion'
    import { cubicOut } from 'svelte/easing'

    let progress = new Tween(0, {
        duration: 400,
        easing: cubicOut
    })
</script>

<progress value={progress.current}></progress>

<button onclick={() => (progress.target += 0.1)}>
	Progress
</button>

Springs

<script>
	import { Spring } from 'svelte/motion';

	let coords = new Spring({ x: 50, y: 50 }, {
		stiffness: 0.1,
		damping: 0.25
	});

	let size = new Spring(10);
</script>

<svg
	onmousemove={(e) => {
		coords.target = { x: e.clientX, y: e.clientY };
	}}
	onmousedown={() => (size.target = 30)}
	onmouseup={() => (size.target = 10)}
	role="presentation"
>
	<circle
		cx={coords.current.x}
		cy={coords.current.y}
		r={size.current}
	/>
</svg>

Preprocessors

// svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
  preprocess: [vitePreprocess()]
};

State and reactivity

let numbers = $state([1, 2, 3, 4])

function addNumber() {
    numbers.push(numbers.length + 1)
}

Reactive builtins

<script>
	import { SvelteDate } from 'svelte/reactivity';

	let date = new SvelteDate();

	const pad = (n) => n < 10 ? '0' + n : n;

	$effect(() => {
		const interval = setInterval(() => {
			date.setTime(Date.now());
		}, 1000);

		return () => {
			clearInterval(interval);
		};
	});
</script>

<p>The time is {date.getHours()}:{pad(date.getMinutes())}:{pad(date.getSeconds())}</p>

Class properties

class Box {
    #width = $state(0);
    #height = $state(0);

    constructor(width, height) {
        this.#width = width;
        this.#height = height;
    }

    get width() {
        return this.#width;
    }

    get height() {
        return this.#height;
    }

    set width(value) {
        this.#width = Math.max(0, Math.min(MAX_SIZE, value));
    }

    set height(value) {
        this.#height = Math.max(0, Math.min(MAX_SIZE, value));
    }

Derived state

let numbers = $state([1, 2, 3, 4])
let total = $derived(numbers.reduce((t, n) => t+n, 0))

Raw state

<script>
import { poll } from './data.js';
let data = $state.raw(poll());
</script>

<polyline points={data.map((d, i) => [x(i), y(d)]).join(' ')} />

Logging state ($inspect)

$inspect(numbers).with(console.trace)

Effects

let elapsed = $state(0)
let interval = $state(1000)

$effect(() => {
    const id = setInterval(() => elapsed += 1, interval)
    return () => clearInterval(id)
})

untrack

$effect(() => {
	// this will run when `data` changes, but not when `time` changes
	save(data, {
		timestamp: untrack(() => time)
	});
});

Components

<script>
    import Component from './Component.svelte'
</script>

<Component />

Props

let { answer = 42, renamed: newName, ...rest } = $props()
<script lang="ts" generics="Item extends { text: string }">
	interface Props {
		items: Item[];
		select(item: Item): void;
	}

	let { items, select }: Props = $props();
</script>
<script lang="ts">
	import type { SvelteHTMLElements } from 'svelte/elements';

	let { children, ...rest }: SvelteHTMLElements['div'] = $props();
</script>

<div {...rest}>
	{@render children?.()}
</div>

Context

// parent component
import { setContext } from 'svelte'

setContext('canvas', { addItem })

function addItem(fn) {
	$effect(() => {
		items.add(fn);
		return () => items.delete(fn);
	});
}
// child component
import { getContext } from 'svelte'

const { addItem } = getContext('canvas')

Events

<script>
	let count = 0

	function incrementCount() {
		count++
	}
</script>

<button onclick={incrementCount}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<button onclick={e => count++}>
    Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

Component events

// Stepper.svelte
<script>
    let { increment, decrement } = $props()
</script>

<button onclick={decrement}>-1</button>
<button onclick={increment}>+1</button>
// App.svelte
<script>
    import Stepper from './Stepper.svelte'
    let value = $state(0)
</script>

<Stepper
     increment={() => value += 1}
     decrement={() => value -= 1}
/>

Bindings

Form elements (controlled components)

<script>
	let name = 'world'
</script>

<input bind:value={name}>

<h1>Hello {name}!</h1>
<script>
    let questions = $state([
		{
			id: 1,
			text: `Where did you go to school?`
		},
		{
			id: 2,
			text: `What is your mother's name?`
		},
		{
			id: 3,
			text: `What is another personal fact that an attacker could easily find with Google?`
		}
	]);

	let selected = $state();

	let answer = $state('');
</script>

<form onsubmit={handleSubmit}>
	<select
		bind:value={selected}
		onchange={() => (answer = '')}
	>
		{#each questions as question}
			<option value={question}>
				{question.text}
			</option>
		{/each}
	</select>
</form>
<script>
	let scoops = $state(1);
	let flavours = $state([]);
</script>

{#each [1, 2, 3] as number}
	<label>
		<input
			type="radio"
			name="scoops"
			value={number}
			bind:group={scoops}
		/>

		{number} {number === 1 ? 'scoop' : 'scoops'}
	</label>
{/each}

{#each ['cookies and cream', 'mint choc chip', 'raspberry ripple'] as flavour}
	<label>
		<input
			type="checkbox"
			name="flavours"
			value={flavour}
			bind:group={flavours}
		/>

		{flavour}
	</label>
{/each}

Binding props

<!-- App.svelte -->
<script>
    let pin = $state('1234')
</script>

<Keypad bind:value={pin} />
<!-- Keypad.svelte -->
<script>
    let { value = $bindable('0000') } = $props()
</script>

this (refs)

<script>
    import InputField from './InputField.svelte';

    let canvas
    let field

    $effect(() => {
        const ctx = canvas.getContext('2d')
        // do canvas-y stuff
    });
</script>

<canvas bind:this={canvas}></canvas>

<!-- assuming InputField exports a `focus` method -->
<InputField bind:this={field} />

<!-- you can't simply pass `field.focus` as the listener since `field` is undefined on first render -->
<button onclick={() => field.focus()}>Focus field</button> // 

Other

Actions

<script>
	import tippy from 'tippy.js';

	let content = $state('Hello!');

	function tooltip(node, fn) {
		$effect(() => {
			const tooltip = tippy(node, fn());

			return tooltip.destroy;
		});
	}
</script>

<button use:tooltip={() => ({ content })}>
	Hover me
</button>

Lifecycle hooks

import { onMount } from 'svelte';

onMount(() => {
    const interval = setInterval(() => {
        console.log('beep');
    }, 1000);

    return () => clearInterval(interval);
});
import { tick } from 'svelte';

$effect.pre(() => {
    console.log('the component is about to update');
    tick().then(() => {
        console.log('the component just updated');
    });
});

Error boundaries

<svelte:boundary onerror={(e, reset) => console.error(e)}>>
    <FlakyComponent />

    {#snippet failed(error, reset)}
		<p>Oops! {error.message}</p>
		<button onclick={reset}>Reset</button>
	{/snippet}
</svelte:boundary>

SvelteKit

Basics

Create a new project:

npx sv create my-app

Pages and routing

Layouts

Page and navigation state

Preloading

Data fetching

// +page.server.js
import { error } from '@sveltejs/kit';
import { posts } from '../data.js';

export function load({ params, setHeaders, cookies }) {
	const post = posts.find((post) => post.slug === params.slug);

	if (!post) error(404);

    const visited = cookies.get('visited');
	cookies.set('visited', 'true', { path: '/' });

    setHeaders({
		'Cache-Control': 'no-store'
	});

	return {
		post
	};
}

<!-- +page.svelte -->
<script>
let { data } = $props()

const posts = data.posts
</script>

Form actions

// +page.js
import { fail } from '@sveltejs/kit';

export const actions = {
    create: async ({ cookies, request }) => {
		const data = await request.formData();

		try {
			db.createTodo(cookies.get('userid'), data.get('description'));
		} catch (error) {
			return fail(422, {
				description: data.get('description'),
				error: error.message
			});
		}
	},

	delete: async ({ cookies, request }) => {
		const data = await request.formData();
		db.deleteTodo(cookies.get('userid'), data.get('id'));
	}
};
<!-- +page.svelte -->
<script>
    import { fly, slide } from 'svelte/transition';
	import { enhance } from '$app/forms';

	let { data, form } = $props();

	let creating = $state(false);
	let deleting = $state([]);
</script>

<div class="centered">
	<h1>todos</h1>

	{#if form?.error}
		<p class="error">{form.error}</p>
	{/if}

	<form
		method="POST"
		action="?/create"
		use:enhance={() => {
			creating = true;

			return async ({ update }) => {
				await update();
				creating = false;
			};
		}}
	>
		<label>
			add a todo:
			<input
				disabled={creating}
				name="description"
				value={form?.description ?? ''}
				autocomplete="off"
				required
			/>
		</label>
	</form>

	<ul class="todos">
		{#each data.todos.filter((todo) => !deleting.includes(todo.id)) as todo (todo.id)}
			<li in:fly={{ y: 20 }} out:slide>
				<form
					method="POST"
					action="?/delete"
					use:enhance={() => {
						deleting = [...deleting, todo.id];
						return async ({ update }) => {
							await update();
							deleting = deleting.filter((id) => id !== todo.id);
						};
					}}
				>
					<input type="hidden" name="id" value={todo.id} />
					<span>{todo.description}</span>
					<button aria-label="Mark as complete"></button>
				</form>
			</li>
		{/each}
	</ul>

	{#if creating}
		<span class="saving">saving...</span>
	{/if}
</div>

API routes

// todo/+server.js
import { json } from '@sveltejs/kit';

export async function POST({ request, cookies }) {
	const { description } = await request.json();

	const userid = cookies.get('userid');
	const { id } = await database.createTodo({ userid, description });

	return json({ id }, { status: 201 });
}
<!-- +page.svelte -->
<script>
    let { data } = $props()
</script>

<label>
    add a todo:
    <input
        type="text"
        autocomplete="off"
        onkeydown={async (e) => {
            if (e.key !== 'Enter') return;

            const input = e.currentTarget;
            const description = input.value;
            
            const response = await fetch('/todo', {
                method: 'POST',
                body: JSON.stringify({ description }),
                headers: {
                    'Content-Type': 'application/json'
                }
            });

            const { id } = await response.json();

            const todos = [...data.todos, {
                id,
                description
            }];

            data = { ...data, todos };

            input.value = '';
        }}
    />
</label>

Error handling

Environment variables

Svelte 4

Special elements

<!-- Svelte 4 -->
<svelte:component this={CurrentComponent} />
<!-- Svelte 5 -->
<script>
    /* ... */

    let condition = $state(false)
    let CurrentComponent = $derived(condition ? ComponentA : ComponentB)
</script>

<CurrentComponent />

Styling

<button class:selected={current === 'foo'}>Click Me</button>
<script>
    $: const selected = (current === 'foo')
</script>

<button class:selected>Click Me</button>

Reactivity

<script>
	let count = 0
	$: doubled = count * 2

	function incrementCount() {
		count++
	}
</script>

<button on:click={incrementCount}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p>
let doubled: number
$: doubled = count * 2
let doubled
$: {
    doubled = count * 2
    console.log(`The count is ${count}, which is ${doubled} doubled`)
}
$: if (count >= 10) {
    alert('count is dangerously high')
}

Updating arrays & objects

function addNumber() {
    numbers.push(numbers.length + 1)
    numbers = numbers
    // or numbers = [...numbers, numbers.length + 1]
}
const foo = obj.foo
foo.bar = 'baz' // does not trigger update on obj

Props

<!-- in Nested.svelte -->
<script>
    export let answer = 42
</script>

<!-- in another component -->
<Nested answer={10} />
<Nested /> // answer === 42
<Info {...pkg} />

Events

<script>
	let count = 0

	function incrementCount() {
		count++
	}
</script>

<button on:click={incrementCount}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<button on:click={e => count++}>
    Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<button on:click|once|trusted={...}>

Component events

<script>
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher()

	function sayHello() {
		dispatch('message', {
			text: 'Hello!'
		})
	}
</script>

<!-- to listen -->
<script>
    function handleMessage(event) {
        alert(event.detail.text)
    }
</script>
<Component on:message={handleMessage} />
<!-- forwards all message events from Inner to this component's parent -->
<Inner on:message />
<!-- in CustomButton.svelte -->
<button on:click>Click Me</button>

<!-- in parent component -->
<CustomButton on:click={handleClick} />
createEventDispatcher<{
    click: null
    submit: string | null
}>()
<script>
    import type { ComponentEvents } from 'svelte'
    type ExampleEvents = ComponentEvents<Example>

    function handleSubmit(event: ExampleEvents['submit']) {
        console.log(event.detail) // will be typed as string|null
    }
</script>

<Example on:submit={handleSubmit} />

Slots

<!-- in the PageHeader component -->
<hgroup>
    <slot name="heading">Heading</slot>
    <slot name="subheading">Subheading</slot>
</hgroup>

<!-- in the parent component -->
<PageHeader>
    <h1 slot="heading">Weather Report</h1>
    <p slot="subheading">June 1, 2023</p>
</PageHeader>
{#if $slots.subheading}
    <slot name="subheading"></slot>
{/if}

Fragments

<PageHeader>
    <svelte:fragment slot="header">
        <span>This content won't be</span>
        <span>wrapped in an element</span>
    </svelte:fragment>
</PageHeader>

Stores

Auto-subscription

<script>
    import { count } from './stores.js'

    $: console.log('The count is ' + $count)
</script>

<h1>The count is {$count}</h1>

Writable

// stores.js
import { writable } from 'svelte/store'

export const count = writable(0)
<!-- App.svelte -->
<script>
    import { onDestroy } from 'svelte'
    import { count } from './stores.js'
    
    let countValue
    const unsubscribe = count.subscribe(value => {
        countValue = value
    })

    // these could all be in different components
    function increment() {
        count.update(n => n + 1)
    }

    function reset() {
        count.set(0)
    }

    onDestroy(unsubscribe)
</script>

Readable

// stores.js
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);
	}
});
<!-- App.svelte -->
<script>
	import { time } from './stores.js'

	const formatter = new Intl.DateTimeFormat('en', {
		hour12: true,
		hour: 'numeric',
		minute: '2-digit',
		second: '2-digit'
	});
</script>

<h1>The time is {formatter.format($time)}</h1>

Derived

// stores.js, continuing from the above
import { derived } from 'svelte/store'

const start = new Date()

export const elapsed = derived(time, ($time) =>
    Math.round(($time - start) / 1000)
)
<!-- App.svelte -->
<p>
	This page has been open for
	{$elapsed}
	{$elapsed === 1 ? 'second' : 'seconds'}
</p>

Custom stores

function createCount() {
    const { subscribe, set, update } = writable(0)
    return {
        subscribe,
        increment: () => update(n => n + 1),
        decrement: () => update(n => n - 1),
        reset: () => set(0),
    }
}

get

import { get } from 'svelte/store'

const value = get(store)

Lifecycle hooks