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.
- Snapshot: this creates a non-reactive snapshot of the state that we can check.
console.log($state.snapshot(stateVariable);
- 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 useconsole.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>