Decided to check out Svelte since I’m bored and have time. #Svelte is a framework for building dynamic, interactive user-interfaces (frontends) that compile to lightweight JS modules. Find the docs here.

I’m following their interactive tutorial. From the tutorial:

You can build your entire app with Svelte (for example, using an application framework like SvelteKit, which this tutorial will cover), or you can add it incrementally to an existing codebase. You can also ship components as standalone packages that work anywhere.

Basic Svelte

Simple component’s structure

Svelte apps are made of components, just like how we would do in React. Can be a single component, or many. #SvelteComponents are small reusable, self-contained code blocks that encompass the HTML. CSS, and necessary JS for its function.

These components are written in .svelte files and

Here’s an example of a simple Svelte component.

<script lang="ts">
	let name = "Abdu";
</script>
 
<h1>I'm {name}</h1>

Breakdown:

  • We’re writing the JS in the script tag
  • To insert variable values, just place them in {} straight in the HTML.

In Svelte any JS snippet can go in the curly braces in the markup, so we can use this for attributes and quick manipulation

<script>
	let src = '/tutorial/image.gif';
	let name = "Bob";
</script>
 
<img src={src} alt={"The man, known as " + name + " is dancing!"}/>

Svelte supports Attribute Shorthand, for wgen the attribute and its value have the same name. E.g. <img {src} alt={"The man, known as " + name + " is dancing!"}/> .

Adding Style

Just like we use the script tag, to add styles to a component, simply use the style tag.

<style>
	h1{
		color: #ff06b5;
	}
</style>
 
<script lang="ts">
	let name = "Abdu";
</script>
 
<h1>I'm {name}</h1>

These style rules are scoped only to this component, because it’s all self-contained.

Nested Components

This is what enables us to use comps in other components, eventually building out the entire app.

To import a Svelte component.

<script lang="ts">
	import Compie from './Compie.svelte';
</script>

// To use the nested component

<p>This is blah blah</p>
<Compie />

// Note that the styles from current component don’t apply to the imported component, so if you style it, it’ll be imported with those styles.

Rendering HTML

When you want to actually render the HTML stored in a string, we can do this because Svelte offers a special tag to do so. The {@html htmlContent} tag which can be place in any HTML element that takes text. Ex:

<script>
	let string = `this string contains some <strong>HTML!!!</strong>`;
</script>

<p>{@html string}</p>

// Important Notice: the content in the @html tag isn’t sanitized by Svelte which means if it comes from non-trusted sources such as user comments, then you MUST escape (sanitize) it yourself to avoid CrossSiteScriptingXSS vulnerabilities and attacks.

State and Deep State

Svelte is all about building reactive websites, where the DOM keeps up with the app’s state.

Svelte does this using SvelteRunes which are variables wrapped with special markers

$state rune

The $state(value). Now Svelte considers this a state variable to be updated, they’re NOT functions despite looking like it.

Example of button count inrement:

<script>
	let count = $state(0);

	function increment() {
		count +=1;
	}
</script>

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

This is reacting to reassignment, but it also reacts to mutation.

Here we mutate the numbers array, and Svelte maintains the state mutation in the rendered DOM.

<script>
	let numbers = $state([1, 2, 3, 4]);
 
	function addNumber() {
		let newNum = numbers[numbers.length - 1] + 1;
		numbers.push(newNum);
	}
</script>
 
<p>{numbers.join(' + ')} = ...</p>
 
<button onclick={addNumber}>
	Add a number
</button>
 

// Deep Reactivity is implemented using proxies and the mutations occur on them, not the original object.

$derived rune

This rune’s for state that’s derived from other state, whenever the value its derived from is updated, the derived state will also reevaluate. // Derived state is READ ONLY

<script>
	let numbers = $state([1, 2, 3, 4]);
	let total = $derived(numbers.reduce((t,n) => t+n, 0));
	function addNumber() {
		numbers.push(numbers.length + 1);
	}
</script>
 
<p>{numbers.join(' + ')} = {total}</p>
 
<button onclick={addNumber}>
	Add a number
</button>
 

This, let total = $derived(numbers.reduce((t,n) => t+n, 0)); , is a neat functional thing, reminds me of fold-left from OCaml. The for array.reduce docs.

Inspecting State

Reactive proxies don’t print well so console.log ain’t gonna cut it.

To inspect state we have a few options.

  1. Snapshot: this creates a non-reactive snapshot of the state that we can check. console.log($state.snapshot(stateVariable);
  2. The inspect rune, $inspect which will automatically log a snapshot of the satet for us whenever it changes, AND it’s automatically stripped from Production code by Svelte!
    function addNumber() {
    	numbers.push(numbers.length + 1);
    }
    $inspect(numbers);

You can customise how the information is displayed by using $inspect(...).with(fn) — for example, you can use console.trace to see where the state change originated from:

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

Effects

For state to be reactive there must be something reacting to it, this is referred to as an Effect. A simple example of an effect is how Svelte updates the DOM in response to a state variable being updated, that’s auto and under the hood.

Effect rune

The $effect rune lets us make our own custom effects. However:

Most of the time, you shouldn’t. $effect is best thought of as an escape hatch, rather than something to use frequently. If you can put your side effects in an event handler, for example, that’s almost always preferable.

Here’s an example effect that tracks how long a component’s been mounted to the DOM.

<script>
	let elapsed = $state(0);
	let interval = $state(1000);
	$effect(() => {
		const id = setInterval(()=>{
			elapsed += 1;
		}, interval)
		return() =>{
		clearInterval(id)
	}
	});
</script>
 
<button onclick={() => interval /= 2}>speed up</button>
<button onclick={() => interval *= 2}>slow down</button>
 
<p>elapsed: {elapsed}</p>
 
  • Speed up button makes it tick faster because the interval grows smaller.
  • Slow down button makes interval larger.
  • We clear the interval immediately before the function runs again when interval changes, this also happens when the component is destroyed.

If the effect function doesn’t read any state when it runs, it will only run once, when the component mounts.

This could've been a an event handler tbh...

Universal Reactivity

Runes can be used outside a component to have universal state. // likely a bad idea but sometimes needed.

However, Svelte runes only work in Svelte files and not normal JS files. Sooo, rename the file to fileName.svelte.js Example of universal state object in a .svelte.js file.

export const counter = $state(
	{count:  0}
)

Note:

You cannot export a $state declaration from a module if the declaration is reassigned (rather than just mutated), because the importers would have no way to know about it.

Props

Props allow passing values between a component and its children. First, props (short for properties) need to be declared, this is done using the $props rune.

Example, with two modules one takes props. App.svelte:

<script>
	import Nested from './Nested.svelte';
</script>
<Nested answer={42} />

Nested.svelte:

<script>
	let { answer } = $props();
</script>
<p>The answer is {answer}</p>

Default values

To add specify a default value for props place it in curly-braces before the props rune. E.x:

<script>
	let { answer = 42 } = $props();
</script>

<p>The answer is {answer}</p>

Spread props

To pass multiple props to a component:

<script>
	let { name, version, description, website } = $props();
</script>
<p>
	The <code>{name}</code> package is {description}. Download version {version} from
	<a href="https://www.npmjs.com/package/{name}">npm</a> and <a href={website}>learn more here</a>
</p>

Since the props are in an object, instead of passing each value one by one we spread them.

<script>
	import PackageInfo from './PackageInfo.svelte';
 
	const pkg = {
		name: 'svelte',
		version: 5,
		description: 'blazing fast',
		website: 'https://svelte.dev'
	};
</script>
<PackageInfo {...pkg} />

This only works when (as in advised to do) because the properties of pkg object correspond to the properties expected by PackageInfo and conversely we can do

<script>
	let { name,... stuff } = $props();
</script>
<p>
	The <code>{name}</code> package is {description}. Download version {version} from
	<a href="https://www.npmjs.com/package/{name}">npm</a> and <a href={website}>learn more here</a>
</p>

or skip the destructuring all together let stuf = $props(); and simply access them like you would any object stuff.name.

Logic

Logic in JS isn’t anything to talk about, but Svelte lets us write snippets in HTML.

If and else blocks

For conditional rendering in the markup, wrap in { #if cond }...{/if} block. To add a else

<script>
	let count = $state(0);
 
	function increment() {
		count += 1;
	}
</script>
 
<button onclick={increment}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>
 
{#if count > 10}
	<p> {count} is greater than 10</p>
{:else}
	<p> {count} is less than 10</p>
{/if}
 

Info

And just like that, you’ve figured out code-blocks in Svelte. {#...} opens a block, continues a block, and {/...} closes it.

To add more conditions else-if blocks can also be added using {: else if}

Each blocks

These are great for iterating over an iterable such as an array or list, while also tracking indices.

<div>
	{#each colors as color,index }
	<button
		style="background: {color}"
		aria-label="red"
		aria-current={selected === color}
		onclick={() => selected = color}
	></button>
	{/each}

In this example, colors can be replaced with any iterable.

By default, updating the value of an each block will add or remove DOM nodes at the end of the block if the size changes, and update the remaining DOM.

This isn’t always desirable…

If you’re coming from React, this might seem strange, because you’re used to the entire component re-rendering when state changes. Svelte works differently: the component ‘runs’ once, and subsequent updates are ‘fine-grained’. This makes things faster and gives you more control.

Keyed Each blocks

To fix the issue with removing specific component or DOM nodes that ends up causing all of them to refresh, we want ingrained change, we specify unique keys to each item in the Each block. For example:

{#each stuff as item (item.id)}
	...
{/each}

This works because Svelte uses a Map, and the key can be any unique object (even (item) itself), but strings and numbers are safer since identity persists without referential equality. E.g. when updating with data from an API request.

Await blocks

For all the component that deal with async data, this makes awaiting promises simple.

{#await promise}
	<p>...rolling</p>
{:then number}
	<p>you rolled a {number}!</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}

So the general syntax is {#await thing} and its content, a block, followed lastly by a {:catch error} block then it ends.

If you’re confident your promise can’t reject and nothing to show while it waits, the shorthand is:

{#await promise then number}
	<p>you rolled a {number}!</p>
{/await}

Events

DOM events

These are all the events that occur on the DOM, including familiar ones like clicks and hovers.

With Svelte we can listen to any of these with an on<name> event function. Ex:

<script>
	let m = $state({ x: 0, y: 0 });
 
	function onpointermove(event) {
		m.x = event.clientX;
		m.y = event.clientY;
	}
</script>
<div onpointermove={onpointermove}>
	The pointer is at {Math.round(m.x)} x {Math.round(m.y)}
</div

And since the name of the function matches the name of the event, we can simply do <div {onpointermove}> instead.

Inline event handlers

A JS staple that’s common in many frameworks.

<div
	onpointermove={(event) => {
		m.x = event.clientX;
		m.y = event.clientY;
	}}
>
	The pointer is at {m.x} x {m.y}
</div>

The good old {()=>{}}

Capturing

To understand this better, read these Bubbling MDN docs, basically some events bubble up when they’re nested within. When we want handlers to run during Capturing instead of Bubbling phase, we indicate this by appending capture to the handlers name and Svelte will catch it. This effectively reverses the order we’d otherwise get.

Bubbler example:

<div onkeydown={(e) => alert(`<div> ${e.key}`)} role="presentation">
	<input onkeydown={(e) => alert(`<input> ${e.key}`)} />
</div>

Captured example

<div onkeydowncapture={(e) => alert(`<div> ${e.key}`)} role="presentation">
	<input onkeydowncapture={(e) => alert(`<input> ${e.key}`)} />
</div>

//Capturing handlers will always run before non-capturing handlers.

Component Events

Event handlers (just functions really) can be passed as props, this is because functions are first-class citizens in JS.

<MyComponent
	handleez={()=>{
		...
	}}
/>

Events can also be spread just like props. Review [[Learning Svelte#Basic Svelte#Props#Spread props]]

Bindings

In Svelte, data flows Top-Down. Meaning parent components set props on children and components can set them on elements but not vice-versa…

For example, if you have an element like <input> and want to set a component’s state variable to its value to, you’d need to do event.target.value…etc. Instead, Svelte gives us bind:<variableName>.

<script>
	let name = $state('world');
</script>
 
<input bind:value={name} />
 
<h1>Hello {name}!</h1>

This is great because it means that changes to the value of name update the input value, but changes to the input value will update name.

numbers

Inputs’ values are always strings, so we often need to remember casting them to numeric values when dealing with type="number" and type="range". But Bindings handle that automatically for us. //Nice.

Checkbox inputs

Same deal but instead of bind:value in the element’s attributes we do:

<input type="checkbox" bind:checked={yes}>

Select inputs

This is also handled by Svelte using bind:value in the <select> attributes.

<select
	bind:value={selected}
	onchange={() => answer = ''}
>

This has no effect on <option> elements being objects instead of strings. If no default is set for the state variable selected, the default will be the first option in the select.

Note

Be careful with referencing uninitialized variables — until the binding is initialised, selected remains undefined, so we can’t blindly reference e.g. selected.id in the template.

Group inputs

When dealing with multiple radio or checkbox input values, the binding to use is bind:group along with the value attribute in the <input> element.

<input
	type="radio"
	name="dilemmas"
	value={number}
	bind:group={dilemmas}
/>

Select Multiple

Continuing with the <select> input, which can take multiple values that populate an array using the multiple attribute.

Example combining Group and multiple select inputs:

<script>
	let scoops = $state(1);
	let flavours = $state([]);
 
	const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
</script>
 
<h2>Size</h2>
 
{#each [1, 2, 3] as number}
	<label>
		<input
			type="radio"
			name="scoops"
			value={number}
			bind:group={scoops}
		/>
 
		{number} {number === 1 ? 'scoop' : 'scoops'}
	</label>
{/each}
 
<h2>Flavours</h2>
 
<select multiple bind:value={flavours}>
		{#each ['cookies and cream', 'mint choc chip', 'raspberry ripple'] as flavour}
			<option>{flavour}</option>
		{/each}
</select>
 
{#if flavours.length === 0}
	<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
	<p>Can't order more flavours than scoops!</p>
{:else}
	<p>
		You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
		of {formatter.format(flavours)}
	</p>
{/if}

Textarea inputs

Also covered by bind:value just like a regular text input.

<textarea bind:value={value}></textarea>

Neat example of outputting the state of the <textarea>:

<script>
	import { marked } from 'marked';
 
	let value = $state(`Some words are *italic*, some are **bold**\n\n- lists\n- are\n- cool`);
</script>
 
<div class="grid">
	input
	<textarea bind:value></textarea>
 
	output
	<div>{@html marked(value)}</div>
</div>
 
<style>
	.grid {
		display: grid;
		grid-template-columns: 5em 1fr;
		grid-template-rows: 1fr 1fr;
		grid-gap: 1em;
		height: 100%;
	}
 
	textarea {
		flex: 1;
		resize: none;
	}
</style>