Svelte
Markup
- HTML markup goes in the root level of a
.sveltefile- elements follow the same self-closing rules as standard HTML (ex.
<div />is incorrect)
- elements follow the same self-closing rules as standard HTML (ex.
- JS code goes in the
<script>tag- use
<script lang="ts">for TypeScript - all variables in the
<script>block can be used in the markup
- use
- expressions in markup and attributes use single curly braces
- you can use expressions inside quoted string attributes
<script>
let name = 'world'
let src = '/tutorial/image.gif'
</script>
<h1>
Hello {name.toUpperCase()}!
</h1>
<img src={src} alt="Photo of {name}">
- if an attribute name and value are the same, you can leave out the name as a shorthand (don't forget the curly braces)
<img {src} alt="Example">
- objects can be spread onto components and elements
<PackageInfo {...pkg} />
Flow control
if
- conditionally render using
{#if},{:else if},{:else}
<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 (foreach)
- use
{#each iterable as value, index (key)}to loop over iterables- index is optional
- can use destructuring in the value
- you can use an object as the key, but a primitive is safer
<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}
- repeat a block a certain number of times
{#each { length: buttonCount }}
<Button on:click={() => alert(`Button ${i} clicked`)} />
{/each}
await
- await promises directly in markup using
{#await promise}
{#await promise}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
- to show nothing until the promise resolves, use
{#await promise then value}
{#await promise then number}
<p>The number is {number}</p>
{/await}
Snippets and children
- allow you to store blocks of markup in variables, somewhat similar to JSX
- snippets can take arguments, and can be passed as props
{#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>&#{emoji.codePointAt(0)}</td>
</tr>
{/snippet}
{@render monkey('🙈', 'see no evil')}
{@render monkey('🙉', 'hear no evil')}
{@render monkey('🙊', 'speak no evil')}
<CustomTable rowFunction={monkey} />
- snippets declared inside a component's tag automatically become a prop on that component
- content inside a component's tag that isn't in a snippet becomes part of the
childrenprop
- content inside a component's tag that isn't in a snippet becomes part of the
<!-- inner component -->
<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>
<!-- outer component -->
<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
- render HTML strings using
{@html string}
<script>
const string = 'Here is some <strong>HTML</strong>'
</script>
<p>{@html string}</p>
-
{@debug var1, var2, ...}will log the values of each variable when they change, and pause execution if devtools are open- use
{@debug}without arguments to pause when any state changes
- use
-
{@const assignment}creates a local constant
{#each boxes as box}
{@const area = box.width * box.height}
{box.width} * {box.height} = {area}
{/each}
Special elements
-
<svelte:element this={tagName}>lets you render different elements based on a string tag name- if
thisis falsy, nothing will be rendered
- if
-
<svelte:head>lets you insert elements into the<head> -
Elements for adding event listeners to objects outside the component:
<svelte:window>- also has bindings for
inner(Width|Height),outer(Width|Height),scroll(X|Y),online(matches window.navigator.onLine)- all except
scroll(X|Y)are readonly
- all except
- also has bindings for
<svelte:document>and<svelte:body>- for
mouseenterandmouseleaveuse<svelte:body> - allows you to use [[#Actions]] on these elements
- for
- All of these must appear at the top level
- Listeners on all of these will be cleaned up automatically when the component is destroyed, and are safe to use with SSR
Element IDs
- use
$props.id()to generate a unique ID (ex. for theforattribute) that is consistent between SSR and hydration
<script module>
<script>blocks with themoduleattribute will run once per module, not once per component- the rest of the component has access to variables in the
<script module>block, but not vice versa- variables in
<script module>can be exported and used in other files
- variables in
- example: an audio player component that pauses if another instance of the same component starts playing
<script module>
let current;
</script>
<audio onplay={(e) => {
if (e.currentTarget !== current) {
current?.pause()
current = e.currentTarget
}
}}
Styling and animation
- styles go in the
<style>tag, and are scoped to the component- scoped selectors take precedence over identical global selectors
- make selectors global using
:global()- can wrap parts of selectors, ex.
.foo :global(.bar)
- can wrap parts of selectors, ex.
- you can also create a global block:
:global {
.foo { }
.bar { }
}
- keyframes are scoped by default
- to create global keyframes, prefix the name with
-global-, but reference it in rules without the prefix
- to create global keyframes, prefix the name with
@keyframes -global-spin { ... }
.square {
animation: 1s spin;
}
Classes
- classes can be added with a string as normal, or conditionally using array or object syntax (like the
classnamespackage for React) - in the example, the
cardclass is always applied, and theselectedandflippedclasses are applied if the state values of the same name are truthy
<button class={["card", selected && "selected", { flipped }]} />
Styles
- inline styles can be set individually with
style:propName, including custom properties- mark them as important by adding
|importantto the name - you can also use
style="foo: bar"as normal, and mix and match
- mark them as important by adding
<script>
const bgOpacity = 0.5
const color = blue
</script>
<p style:color style:--opacity={bgOpacity}></p>
Custom properties
- custom properties can be set on components without using
style:- this wraps the component in an element with
display: contents, which may affect global CSS selectors such as.parent > .child
- this wraps the component in an element with
// 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
- apply transitions from
svelte/transitionwhen an element is added or removed
<script>
import { fade } from 'svelte/transition';
let visible = $state(true);
</script>
{#if visible}
<p transition:fade>
Fades in and out
</p>
{/if}
- transitions can accept parameters, and can reverse mid-transition if the element is toggled while a transition is playing
<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}
-
add
|globalto the transition name to make it play when any parent is added or removed, not just the element with the transition itself -
transitions fire
onintrostart,onintroend,onoutrostart,onoutroendevents -
use
{#key expression}to destroy and recreate the element each timeexpressionchanges, which will replay transitions- in the example below, the typewriter transition will replay whenever the displayed message changes
{#key i}
<p in:typewriter={{ speed: 10 }}>
{messages[i] || ''}
</p>
{/key}
- you can also use
inandoutto apply separate transitions on entry or leave, and these are not reversible
{#if visible}
<p in:fly={{ y: 200, duration: 2000 }} out:fade>
Flies in, fades out
</p>
{/if}
- transitions are written in JavaScript - see Transitions / Custom CSS transitions • Svelte Tutorial
Animate moving elements in the DOM
- use the
crossfadefunction to link an element being removed from one place in the DOM to an element being added in another place using a key, and animate between the two - use the
animate:directive to apply FLIP animations to the surrounding elements (must be in a keyed each block) - example: Advanced transitions / Animations • Svelte Tutorial
Tweening
- use the
Tweenclass to tween values, along with easing functions fromsvelte/easing - has a writable
targetproperty and a read-onlycurrentproperty
<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
Springis good for values that change frequently- in the example,
coordsupdates to follow the mouse cursor, but with a spring effect
<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
- Vite supports PostCSS, SCSS, Less, Stylus, and SugarSS (already set up in SvelteKit)
- may be prompted to install dependencies
// svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: [vitePreprocess()]
};
State and reactivity
- declare reactive variables with the
$staterune - state is deeply reactive, and can be written to or modified directly
- state can be placed inside any file named with
.svelte.js, and shared between components like a store $state.snapshotcan be used to take a static snapshot of a state value, useful for passing to third-party libraries
let numbers = $state([1, 2, 3, 4])
function addNumber() {
numbers.push(numbers.length + 1)
}
- state can be TypeScript typed like a normal variable
Reactive builtins
- Svelte includes reactive versions of
Map,Set,Date,URLandURLSearchParamsinsvelte/reactivity
<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
- state can be used to make class properties reactive, and you can use getters and setters to validate the state
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
- use
$derivedto compute state from other state$derivedaccepts expressions,$derived.byaccepts functions
- derived state is read-only
let numbers = $state([1, 2, 3, 4])
let total = $derived(numbers.reduce((t, n) => t+n, 0))
Raw state
- use
$state.raw()to create state that isn't deeply reactive, useful if the state is only needed during rendering - raw state can't be mutated, but it can be reassigned
- in the example below, the
<polyline>will be updated wheneverdatachanges, but since we never look at the individualdatavalues we dont' need them to be reactive
<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)
- use the
$inspectrune to log state whenever it changes- will automatically be stripped out of production builds
- you can pass a custom logging function to
.with()- the first argument is either "init" or "update", and the rest of the arguments are the inspected values
$inspect(numbers).with(console.trace)
Effects
- use
$effectto re-run a function when any of the state it uses changes- prefer using
$derivedor event listeners when possible
- prefer using
- return a cleanup function to run before the effect re-runs or the component is destroyed
- effects don't run during server-side rendering
$effect.preruns before the DOM updates - if checking DOM elements, make sure they exist first!
let elapsed = $state(0)
let interval = $state(1000)
$effect(() => {
const id = setInterval(() => elapsed += 1, interval)
return () => clearInterval(id)
})
untrack
- use
untrackto exclude state inside$derivedor$effectfrom being treated as a dependency
$effect(() => {
// this will run when `data` changes, but not when `time` changes
save(data, {
timestamp: untrack(() => time)
});
});
Components
- import components within the
<script>tag - component names are always capitalized
- can be self-closing
- don't need to have a single root element
<script>
import Component from './Component.svelte'
</script>
<Component />
- components use the
ComponentTypeScript type - component props can be extracted with
ComponentProps<Component>
Props
- destructured from the
$propsrune - can be given default values, renamed, and have rest properties, just like standard destructuring
let { answer = 42, renamed: newName, ...rest } = $props()
-
the child component can temporarily reassign a prop, and it will keep the new value until the prop is updated by the parent - useful for ephemeral state
- however, props should not be mutated (use $bindable instead)
-
props can be TypeScript typed like any object
- to use generics, add a
genericsattribute to thescripttag
- to use generics, add a
<script lang="ts" generics="Item extends { text: string }">
interface Props {
items: Item[];
select(item: Item): void;
}
let { items, select }: Props = $props();
</script>
- HTML element types can be imported from
svelte/elements- if an element doesn't have its own type definitions use
SvelteHTMLElements
- if an element doesn't have its own type definitions use
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
let { children, ...rest }: SvelteHTMLElements['div'] = $props();
</script>
<div {...rest}>
{@render children?.()}
</div>
Context
- lets you pass any data (including state) down the tree using a key
- the key can be any value (not just a string), which lets you control who can access the context
// parent component
import { setContext } from 'svelte'
setContext('canvas', { addItem })
function addItem(item) {
...
}
// child component
import { getContext } from 'svelte'
const { addItem } = getContext('canvas')
createContext
- use the
createContextAPI to create type-safe getters and setters that automatically manage the context key- export them from a shared file and import in components as needed
const [getCanvasContext, setCanvasContext] = createContext<{
addItem: (item: Item) => void
}>()
Events
- bind event listeners using
on<name>, just like standard HTML
<script>
let count = 0
function incrementCount() {
count++
}
</script>
<button onclick={incrementCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
- you can also declare listeners inline using a function
<button onclick={e => count++}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
- make a handler capturing (instead of bubbling) by adding
captureto the name - ex.onclickcapture
Component events
- pass event listeners to components as props
// 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)
- declare two-way bindings for
<input>and<textarea>values usingbind:value={var}- if the variable name is also
valueyou can just usebind:value
- if the variable name is also
<script>
let name = 'world'
</script>
<input bind:value={name}>
<h1>Hello {name}!</h1>
- you can use
bind:valuewith<select>elements, and use objects as<option>values- if the
multipleattribute is used, selected values will be collected into an array
- if the
<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>
- for checkboxes use
bind:checked - for radio buttons and grouped checkboxes, use
bind:group- checkbox values will be collected into an array
<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
- you can bind to a prop of a child component, if the child marks it as bindable by setting it equal to
$bindable $bindablecan take a fallback value, which is used if the prop is not bound
<!-- App.svelte -->
<script>
let pin = $state('1234')
</script>
<Keypad bind:value={pin} />
<!-- Keypad.svelte -->
<script>
let { value = $bindable('0000') } = $props()
</script>
this (refs)
- the
thisbinding lets you get a readonly binding to an element or component, similar to refs in Vue or React- the value is undefined until the component has mounted, so you should only work with the elements inside effects or event listeners
- refs on components can access any exported data
<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
- elements have readonly
clientWidth,clientHeight,offsetWidth,offsetHeightbindings- inline elements without intrinsic dimensions can't be observed unless their
displayvalue is changed (see ResizeObserver)
- inline elements without intrinsic dimensions can't be observed unless their
- elements with
contenteditable="true"supportbind:textContentandbind:innerHTML - media elements support bindings like
currentTime,duration,paused
Actions
#todo learn about Attachments: https://svelte.dev/docs/svelte/@attach
- actions are functions that are called when an element is created, and can do things like add event listeners or interface with third-party libraries
- typically use $effect so they are cleaned up when the element unmounts
- actions receive the element as the first argument, and the attribute's contents as a second argument
- attached with
use:name - not called during SSR
<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
onMount- if onMount returns a function synchronously, it will be called when the component is unmounted
- does not run on the server
onDestroy- does run on the server
- these must be called during initialization (not within effects or the like), but can be called from external modules
import { onMount } from 'svelte';
onMount(() => {
const interval = setInterval(() => {
console.log('beep');
}, 1000);
return () => clearInterval(interval);
});
tick: returns a promise that resolves when pending state changes have been applied- can be used inside
$effect.preto continue once the UI has updated
- can be used inside
import { tick } from 'svelte';
$effect.pre(() => {
console.log('the component is about to update');
tick().then(() => {
console.log('the component just updated');
});
});
Error boundaries
- use
<svelte:boundary>to capture errors - can take an
onerrorhandler and/or afailedsnippet, both of which receive the error and a reset function as arguments
<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
npx sv create my-app
- shared components and code go in
src/lib, and are imported from$lib- server-only code can be placed in
src/lib/serverand imported from$lib/server
- server-only code can be placed in
Pages and routing
- pages are stored in
src/routes/{page name}/+page.sveltesrc/routes/+page.svelteis the index page
- create dynamic route parameters by adding square brackets to the folder (ex.
/src/routes/blog/[slug]/+page.svelte)- advanced routing: Advanced routing / Optional parameters • Svelte Tutorial
- programmatic routing functions and callbacks are in
$app/navigation
Layouts
+layout.svelteapplies to every sibling and child page, and receives the page content as the children prop- use layouts to add nav, import shared stylesheets, etc
- you can "break out" of a nested layout by adding a parent segment to "reset" to - ex.
+page@b.sveltewill render layouts as if the page was in thebfolder+page@.sveltewould only use the root layout
Page and navigation state
- layouts and pages can import these read-only state objects from
$app/state:page:url— the URL of the current pageparams— the current page’s parametersroute— an object with an id property representing the current routestatus— the HTTP status code of the current pageerror— the error object of the current page, if any (you’ll learn more about error handling in later exercises)data— the data for the current page, combining the return values of all load functionsform— the data returned from a form action
navigating:fromandto:paramsrouteurl- can be used to show a loader (if
navigating.tohas a value)
type:link,popstate,goto
Preloading
- preload pages by adding the
data-sveltekit-preload-dataattribute to a link (<a>), or any element containing links- this will run the
loadfunctions inpage.jsandlayout.js - the default template has this on the body, so all links are preloaded
- accepts these values:
hover: start preloading when the link is hoveredtap: start preloading when the link is tappedoff: don't preload (override higher level attributes)
- this will run the
data-sveltekit-preload-codepreloads the page's JavaScript, but not the dataeagerpreloads the whole page as soon as navigation finishesviewportpreloads what's in the viewporthover,tap,offbehave the same as above
- you can also use the
preloadDataandpreloadCodefunctions in your script
Data fetching
- create a
+page.jsor+page.server.jsfile next to+page.svelte+page.jsruns on server and client,+page.server.jsruns only on the server- you can also use both, and
+page.jscan access the data from+page.server.jsin thedataproperty- the data isn't merged, so
+page.jsmust return anything from the server's data that should be accessible to the client
- the data isn't merged, so
- export a
loadfunction that returns your data- receives an object with the route params, and functions for setting headers and cookies
- make sure to specify a path when setting cookies
- deeper load functions can access data from their parents using
await parent()
- receives an object with the route params, and functions for setting headers and cookies
- use a
dataprop in the page to receive the data +layout.js(or+layout.server.js) is the same, but runs for every sibling and child layout, and data is merged with the data frompage.js- data can be accessed from the layout or page files
- use the
redirectfunction from@sveltejs/kitto redirect - can export page options, which can be overridden further down the tree
ssr: enable/disable SSR for a page or group of pagesprerender: set totrueto render the page at build time instead of per-request
// +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
- the
+page.js(or+page.server.js) file can export actions which can handle receiving form data - you can have a single
defaultaction, or multiple named actions- if there is only one action you can omit the
actionprop on the<form>element - forms can use actions defined on other pages - ex.
action="/todos?/create"
- if there is only one action you can omit the
- actions can return data which is accessible in the
formprop- the
failfunction can be used to return an error (also accessible in theformprop)
- the
- form actions work without JavaScript, but the
use:enhancedirective on the form avoids reloading the page if JavaScript is enabled (which allows adding transitions)use:enhancecan take a function to update state in order to show a message while the action is happening
// +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
- a
+server.jsfile next to your page can export functions corresponding to HTTP methodsrequestis a standardRequestobject- must return a
Responseobject- use the
jsonfunction to respond with JSON - if you don't need to return anything, return
new Response(null, { status: 204 })
- use the
- prefer [[#Form actions]] when possible for mutating data, since they can work without JavaScript
- you can update the
dataprop after mutating data, but it's not deeply reactive, so replace it- only update it in such a way that you would get the same result by reloading the page
// 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
- expected errors are ones thrown from the
errorhelper in@sveltejs/kit, and are shown to users - any other errors are considered unexpected, and the error info is redacted
- create a
+error.sveltecomponent to show an error page- will be rendered inside the layout
- like layouts, different parts of the route hierarchy can have their own error pages, but they aren't nested
src/error.htmlis used if an error happens while loading the root layout or rendering an error page- can take a couple variables -
%sveltekit.status%and%sveltekit.error.message%
- can take a couple variables -
Environment variables
- can use
.env,.env.local(ignored by git),.env.[mode](only loaded in that mode) - import static (replaced at build time) variables from
$env/static/private, or dynamic (read at runtime) from$env/dynamic/private - variables can only be loaded on the server unless their name begins with
PUBLIC_, and they are imported from/publicinstead
Svelte 4
Special elements
<svelte:component>lets an element render different components based on thethispropthisshould be a component constructor- if
thisis falsy, nothing will be rendered
<!-- Svelte 4 -->
<svelte:component this={CurrentComponent} />
- in Svelte 5, a component will re-render if the value of its constructor changes (similar to React)
<!-- Svelte 5 -->
<script>
/* ... */
let condition = $state(false)
let CurrentComponent = $derived(condition ? ComponentA : ComponentB)
</script>
<CurrentComponent />
<svelte:self>lets a component contain itself (since it can't import itself)- in Svelte 5 components can import themselves
Styling
- conditionally add classes using
class:className
<button class:selected={current === 'foo'}>Click Me</button>
- if the class name is the same as the value, you can use shorthand
<script>
$: const selected = (current === 'foo')
</script>
<button class:selected>Click Me</button>
Reactivity
- declare reactive statements (which re-run when the values they depend on change) using a
$:label- somewhat like useEffect in React
<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>
- to type reactive variables, declare them with
letseparately
let doubled: number
$: doubled = count * 2
$:will re-run the block whenever any referenced values change, so you can also use it for statements with side effects (like watchers)- you can group reactive statements into a block
- if you do this you need to declare any new variables outside the block
let doubled
$: {
doubled = count * 2
console.log(`The count is ${count}, which is ${doubled} doubled`)
}
- you can mark things like
ifstatements with$:
$: if (count >= 10) {
alert('count is dangerously high')
}
Updating arrays & objects
- mutating arrays or objects does not trigger an update - to fix this, assign the object to itself
function addNumber() {
numbers.push(numbers.length + 1)
numbers = numbers
// or numbers = [...numbers, numbers.length + 1]
}
- assigning to array or object properties (ex.
object.foo += 1orarray[i] = x) does trigger update - indirect assignments do not trigger update
- the updated variable must directly appear on the left hand side of the assignment
const foo = obj.foo
foo.bar = 'baz' // does not trigger update on obj
Props
- props are declared using
export- assign a value to use it as the default value
<!-- in Nested.svelte -->
<script>
export let answer = 42
</script>
<!-- in another component -->
<Nested answer={10} />
<Nested /> // answer === 42
- you can spread properties onto a component
<Info {...pkg} />
- you can access all of a component's props using
$$props, but this is not recommended
Events
- bind event listeners using
on:event
<script>
let count = 0
function incrementCount() {
count++
}
</script>
<button on:click={incrementCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
- you can also declare event listeners inline using a function
<button on:click={e => count++}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
- add & chain modifiers using
|- preventDefault
- stopPropagation
- passive
- nonpassive
- capture
- once
- self
- trusted
<button on:click|once|trusted={...}>
Component events
- Create an event dispatcher to fire events
event.detailholds the payload
<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} />
- component events do not bubble - to forward events up from a child, use
on:eventwithout a listener
<!-- forwards all message events from Inner to this component's parent -->
<Inner on:message />
- this also works for DOM events, ex. for creating a custom Button component
<!-- in CustomButton.svelte -->
<button on:click>Click Me</button>
<!-- in parent component -->
<CustomButton on:click={handleClick} />
createEventDispatchercan accept a type argument with a list of event names and parameters
createEventDispatcher<{
click: null
submit: string | null
}>()
- the component event listeners can be typed with
ComponentEvents
<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
- declare a default location for a component's children using
<slot>- put fallback content inside
<slot></slot>
- put fallback content inside
- to use multiple slots, give each
<slot>anameattribute, and in the parent component addslot="name"to the content - you can declare multiple slots using
nameattributes, and provide content by addingslotattributes as children
<!-- 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>
- you can check if a slot has content using
$$slots[name]
{#if $slots.subheading}
<slot name="subheading"></slot>
{/if}
Fragments
- Add elements to a slot without a wrapping element using
<svelte:fragment>
<PageHeader>
<svelte:fragment slot="header">
<span>This content won't be</span>
<span>wrapped in an element</span>
</svelte:fragment>
</PageHeader>
Stores
- stores are just objects with a
subscribemethod that notifies whenever the value changes- stores should be placed in standard
.jsfiles, not.sveltefiles
- stores should be placed in standard
- multiple components can subscribe to the same store
- calling
subscribereturns anunsubscribefunction that should be called before the component is destroyed
Auto-subscription
- if a store is imported at the top level, you can access its value using
$to subscribe and unsubscribe automatically- you can also directly assign to store values using
$
- you can also directly assign to store values using
<script>
import { count } from './stores.js'
$: console.log('The count is ' + $count)
</script>
<h1>The count is {$count}</h1>
Writable
- writable stores also have
setandupdatemethodssetdirectly takes a value,updatetakes a function that gets the current value and returns a new value- you can use component binding on writable stores
// 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
- readable stores are used for values that shouldn't be set by components
- readable stores take an initial value, and have a
startfunction which takes asetcallback and returns astopfunctionstartis called on first subscriber,stopis called when the last subscriber unsubscribes in order to clean up
// 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
- derived stores are based on other stores
// 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
- any object that implements
subscribeis a store, so you can create custom stores that add their own logic and/or hide the default set and update methods
function createCount() {
const { subscribe, set, update } = writable(0)
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0),
}
}
get
- Use
getif you need to get the value of a store once, without subscribing to it- This subscribes, reads the value, then unsubscribes, so it should be used sparingly
import { get } from 'svelte/store'
const value = get(store)
Lifecycle hooks
beforeUpdate(before DOM updates)- first runs before the component has mounted, so test for existence of any DOM elements
- in Svelte 5 use
$effect.preinstead
afterUpdate(after DOM updates)- in Svelte 5 use
$effectinstead
- in Svelte 5 use