The Complete Svelte 5 Guide
Published Aug 14, 2025
Table of Contents
- What is Svelte?
- Try Svelte
- TypeScript Aside
- Single File Components
- Component Logic
- Markup Poetry
- Component Styles
- Svelte Reactivity
- Reactive State
- Deeply Reactive State
- Derived State
- Effects
- State In Functions And Classes
- Reactive Global State
- How Svelte Reactivity Works
- Why You Should Avoid Effects
- Using Template Logic
- Listening To Events
- Using Data Bindings
- Svelte Components
- Component Composition
- Transitions And Animations
- Using Third Party Libraries
- Reactive Data Structures And Utilities
- Reactive Events
- Special Elements
- Legacy Svelte
- Using Svelte With AI
What is Svelte?
If we look at the definition from the Svelte website, it says:
Svelte is a UI framework that uses a compiler to let you write breathtakingly concise components that do minimal work in the browser, using languages you already know — HTML, CSS and JavaScript.
Because Svelte is a compiled language, it can wield the same syntax of a language that’s not great at making user interfaces like JavaScript and change the semantics for a better developer experience:
<script lang="ts">
// reactive state
let count = $state(0)
// reassignment updates the UI
setInterval(() => count += 1, 1000)
</script>
<p>{count}</p>
You might think how Svelte does some crazy compiler stuff under the hood to make this work, but the output is human readable JavaScript:
function App($$anchor) {
// create signal
let count = state(0)
// update signal
setInterval(() => set(count, get(count) + 1), 1000)
// create element
var p = from_html(`<p> </p>`)
var text = child(p, true)
// update DOM when `count` changes
template_effect(() => set_text(text, get(count)))
// add to DOM
append($$anchor, p)
}
In fact, Svelte’s reactivity is just based on signals! There’s nothing magical about it. You could write a basic version of Svelte by hand without using a compiler.
Just by reading the output code, you can start to understand how Svelte works. There’s no virtual DOM, or rerendering the component when state updates like in React — Svelte only updates the part of the DOM that changed.
This is what “does minimal work in the browser” means!
Svelte also has a more opinionated application framework called SvelteKit (equivalent to Next.js for React) if you need routing, server-side rendering, adapters to deploy to different platforms and so on.
Try Svelte
You can try Svelte in the browser using the Svelte Playground and follow along without having to set up anything.
localStorage
that aren't supported in the Svelte Playground.If you’re a creature of comfort and prefer your development environment, you can scaffold a Vite project and pick Svelte as the option from the CLI if you run npm create vite@latest
in a terminal — you’re going to need Node.js for that.
I also recommend using the Svelte for VS Code extension for syntax highlighting and code completion, or a similar extension for your editor.
TypeScript Aside
TypeScript has become table stakes when it comes to frontend development. For that reason, the examples are going to use TypeScript, but you can use JavaScript if you prefer.
If you’re unfamiliar with TypeScript, code after :
usually represents a type. You can omit the types and your code will work:
// TypeScript 👍️
let items: string[] = [...]
// JavaScript 👍️
let items = [...]
Some developers prefer writing JavaScript with JSDoc comments because it gives you the same benefits of TypeScript at the cost of a more verbose syntax:
/**
* This is a list of items.
* @type {string[]}
*/
let items = [...]
That is completely up to you!
Single File Components
In Svelte, files ending with .svelte
are called single file components because they contain the JavaScript, HTML, and CSS in a single file.
Here’s an example of a Svelte component:
<!-- logic -->
<script lang="ts">
let title = 'Svelte'
</script>
<!-- markup -->
<h1>{title}</h1>
<!-- styles -->
<style>
h1 {
color: orangered;
}
</style>
A Svelte component can only have one top-level <script>
and <style>
block and is unique for every component instance. A code formatter like Prettier might arrange the blocks for you, but the order of the blocks doesn’t matter.
There’s also a special <script module>
block used for sharing code across component instances we’ll learn about later.
Component Logic
Your component logic goes inside the <script>
tag. Since Svelte 5, TypeScript is natively supported:
<script lang="ts">
let title = 'Svelte'
</script>
<h1>{title as string}</h1>
Later we’re going to learn how you can even define values inside your markup which can be helpful in some cases.
Markup Poetry
In Svelte, anything that’s outside the <script>
and <style>
blocks is considered markup:
<!-- markup -->
<h1>Svelte</h1>
You can use JavaScript expressions in the template using curly braces and Svelte is going to evalute it:
<script>
let banana = 1
</script>
<p>There's {banana} {banana === 1 ? 'banana' : 'bananas'} left</p>
Later we’re going to learn about logic blocks like if
and each
to conditionally render content.
Tags with lowercase names are treated like regular HTML elements by Svelte and accept normal attributes:
<img src="image.gif" alt="Person dancing" />
You can pass values to attributes using curly braces:
<script lang="ts">
let src = 'image.gif'
let alt = 'Person dancing'
</script>
<img src={src} alt={alt} />
If the attribute name and value are the same, you can use a shorthand attribute:
<!-- 👍️ longhand -->
<img src={src} alt={alt} />
<!-- 👍️ shorthand -->
<img {src} {alt} />
Attributes can have expressions inside the curly braces:
<script lang="ts">
let src = 'image.gif'
let alt = 'Person dancing'
let lazy = true
</script>
<img {src} {alt} loading={lazy ? 'lazy' : 'eager'} />
If you want to conditionally render attributes, don’t use &&
for short-circuit evaluation or empty strings. Instead, use null
or undefined
as the value:
<script lang="ts">
let src = 'image.gif'
let alt = 'Person dancing'
let lazy = false
</script>
<!-- ⛔️ -->
<img {src} {alt} loading={lazy && 'lazy'} />
<img {src} {alt} loading={lazy ? 'lazy' : ''} />
<!-- 👍 -->
<img {src} {alt} loading={lazy ? 'lazy' : null} />
<img {src} {alt} loading={lazy ? 'lazy' : undefined} />
You can spread attributes on elements:
<script lang="ts">
let obj = {
src: 'image.gif',
alt: 'Person dancing'
}
</script>
<img {...obj} />
Component Styles
There are many ways you can style a Svelte component. 💅 I’ve heard people love inline styles with Tailwind CSS, so you could just use the style
tag…I’m joking! 😄
That being said, the style
tag can be useful. You can use the style
attribute like in regular HTML, but Svelte also has a shorthand style:
directive you can use. The only thing you can’t pass is an object:
<script lang="ts">
let color = 'orangered'
</script>
<!-- 👍️ attribute -->
<h1 style="color: {color}">Svelte</h1>
<!-- 👍️ directive -->
<h1 style:color>Svelte</h1>
<!-- ⛔️ object -->
<h1 style={{ color }}>Svelte</h1>
You can even add important
like style:color|important
to override styles. The style:
directive is also great for CSS custom properties:
<script lang="ts">
let color = 'orangered'
</script>
<!-- 👍️ custom CSS property -->
<h1 style="--color: {color}">Svelte</h1>
<!-- 👍️ shorthand -->
<h1 style:--color={color}>Svelte</h1>
<style>
h1 {
/* custom CSS property with a default value */
color: var(--color, #fff);
}
</style>
Scoped Styles
Fortunately, you’re not stuck using the style
attribute. Most of the time, you’re going to use the style
block to define styles in your component. Those styles are scoped to the component by default:
<h1>Svelte</h1>
<!-- these styles only apply to this component -->
<style>
h1 {
color: orangered;
}
</style>
Scoped styles are unique to that component and don’t affect styles in other components. If you’re using the Svelte playground, you can open the CSS output tab to view the generated CSS:
/* uniquely generated class name */
h1.svelte-ep2x9j {
color: orangered;
}
If you want to define global styles for your app, you can import a CSS stylesheet at the root of your app:
// inside a Vite project
import { mount } from 'svelte'
import App from './App.svelte'
import './app.css'
const app = mount(App, {
target: document.getElementById('app')!
})
export default app
You can also define global styles in components. This is useful if you have content from a content management system (CMS) that you have no control over.
Svelte has to “see” the styles in the component, so it doesn’t know they exist and warns you about removing unusued styles:
<script lang="ts">
let content = `
<h1>Big Banana Exposed</h1>
<p>The gorillas inside the banana cartel speak out</p>
`
</script>
<article>
{@html content}
</article>
<style>
article {
/* ⚠️ Unused CSS selector "h1" */
h1 {
font-size: 48px;
}
/* ⚠️ Unused CSS selector "p" */
p {
font-size: 20px;
}
}
</style>
@html
tag is used to render raw HTML in Svelte components. If you don't control the content, always sanitize user input to prevent XSS attacks.In that case, you can make the styles global by using the :global(selector)
modifier:
<!-- ... -->
<style>
article {
:global(h1) {
font-size: 48px;
}
:global(p) {
font-size: 20px;
}
}
</style>
Having to use :global
on every selector is tedious! Thankfully, you can nest global styles inside a :global { ... }
block:
<!-- ... -->
<style>
:global {
article {
h1 {
font-size: 48px;
}
p {
font-size: 20px;
}
}
}
</style>
You can also have “global scoped styles” where the styles inside the :global
block are scoped to the class:
<!-- ... -->
<style>
article :global {
h1 {
font-size: 48px;
}
p {
font-size: 20px;
}
}
</style>
Here’s the compiled CSS output:
article.svelte-ju1yn8 {
h1 {
font-size: 48px;
}
p {
font-size: 20px;
}
}
Keyframe animations are also scoped to the component. If you want to make them global, you have to prepend the keyframe name with -global-
. The -global-
part is removed when compiled, so you can reference the keyframe name in your app:
<style>
@keyframes -global-animation {
/* ... */
}
</style>
You can use different preprocessors like PostCSS or SCSS by simply adding the lang
attribute to the <style>
tag with the preprocessor you want to use:
<style lang="postcss">
<!-- ... -->
</style>
<style lang="scss">
<!-- ... -->
</style>
These days you probably don’t need SCSS anymore, since a lot of features such as nesting and CSS variables are supported by CSS.
Dynamic Classes
You can use an expression to apply a dynamic class, but it’s tedious and easy to make mistakes:
<script lang="ts">
let open = false
</script>
<div class="trigger {open ? 'open' : ''}">👈️</div>
<style>
.trigger {
display: inline-block;
transition: all 0.2s ease;
&.open {
rotate: -90deg;
}
}
</style>
Thankfully, Svelte can helps us out here. You can use the class:
directive to conditionally apply a class:
<div class="trigger" class:open>👈️</div>
You can also pass an object, array, or both to the class
attribute and Svelte is going to use clsx under the hood to merge the classes:
<!-- 👍️ passing an object -->
<div class={{ trigger: true, open }}>👈️</div>
<!-- 👍️ passing an array -->
<div class={['trigger', open && 'open']}>👈️</div>
<!-- 👍️ passing an array and object -->
<div class={['trigger', { open }]}>👈️</div>
If you’re using Tailwind, this is very useful when you need to apply a bunch of classes:
<div class={['transition-transform', { '-rotate-90': open }]}>👈️</div>
You can also use data attributes to make the transition state more explicit, instead of using a bunch of classes:
<script lang="ts">
let status = 'closed'
</script>
<div class="trigger" data-status={status}>👈️</div>
<style>
.trigger {
display: inline-block;
transition: rotate 2s ease;
&[data-status="closed"] {
rotate: 0deg;
}
&[data-status="open"] {
rotate: -90deg;
}
}
</style>
Svelte Reactivity
What is state?
In the context of JavaScript frameworks, application state refers to values that are essential to your application working and cause the framework to update the UI when changed.
Let’s look at a counter example:
<!-- only required for this example because of legacy mode -->
<svelte:options runes={true} />
<script lang="ts">
let count = 0
</script>
<button onclick={() => count += 1}>
{count}
</button>
The Svelte compiler knows that you’re trying to update the count
value and warns you because it’s not reactive:
count
is updated, but is not declared with$state(...)
. Changing its value will not correctly trigger updates.
This brings us to our first Svelte rune — the $state
rune.
Reactive State
The $state
rune marks a variable as reactive. Svelte’s reactivity is based on assignments. To update the UI, you just assign a new value to a reactive variable:
<script lang="ts">
// reactive value
let count = $state(0)
</script>
<!-- reactive assignment -->
<button onclick={() => count += 1}>
{count}
</button>
You can open the developer tools and see that Svelte only updated the part of the DOM that changed.
count += 1
to emphasize assignment, but you can use count++
to increment the value.The $state(...)
syntax is called a rune and is part of the language. It looks like a function, but it’s only a hint to Svelte what to do with it. This means as far as TypeScript is concerned, it’s just a function:
let value = $state<Type>(...)
The three main runes we’re going to learn about are the $state
, $derived
, and $effect
rune.
Deeply Reactive State
If you pass an array, or object to $state
it becomes a deeply reactive Proxy. This lets Svelte perform granular updates when you read or write properties and avoids mutating the state directly.
For example, changing editor.content
is going to update the UI in every place where it’s used:
<script lang="ts">
let editor = $state({
theme: 'dark',
content: '<h1>Svelte</h1>'
})
</script>
<textarea
value={editor.content}
oninput={(e) => editor.content = (e.target as HTMLTextAreaElement).value}
></textarea>
{@html editor.content}
<style>
textarea {
width: 100%;
height: 200px;
}
</style>
$state.raw
You might not want deeply reactive state where pushing to an array or updating the object would cause an update. In that case, you can use $state.raw
so state only updates when you reassign it:
<script lang="ts">
// this could be a complex object
let editor = $state.raw({
theme: 'dark',
content: '<h1>Svelte</h1>'
})
</script>
<textarea
value={editor.content}
oninput={(e) => {
// ⛔️ can't be mutated
editor.content = e.target.value
// 👍️ reassignment
editor = {
...editor,
content: e.target.value
}
}}
></textarea>
{@html editor.content}
<style>
textarea {
width: 100%;
height: 200px;
}
</style>
$state.snapshot
Because proxied state is deeply reactive, you could change it on accident when you pass it around, or run into a problem with some API that doesn’t expect it. In that case, you can use $state.snapshot
to get the normal value from the Proxy:
function saveEditorState(editor) {
// 💣️ oops! it doesn't like a Proxy object...
const editorState = structuredClone(editor)
// 👍️ normal object
const editorState = structuredClone($state.snapshot(editor))
}
$state.snapshot
when you console.log
deeply reactive values for convenience.Destructuring
You can destructure deep state where you defined it — but if you destructure it anywhere else — it loses reactivity because it’s just JavaScript, so the values are evaluated when you destructure them:
<script lang="ts">
// 👍️ reactive
let { theme, content } = $state({
theme: 'dark',
content: '<h1>Svelte</h1>'
})
// ⛔️ not reactive
let { theme, content } = editor
</script>
{@html content}
If you want to do this, you can use derived state!
Derived State
You can derive state from other state using the $derived
rune and it’s going to reactively update:
<script lang="ts">
let count = $state(0)
let factor = $state(2)
let result = $derived(count * factor)
</script>
<button onclick={() => count++}>Count: {count}</button>
<button onclick={() => factor++}>Factor: {factor}</button>
<p>{count} * {factor} = {result}</p>
Deriveds Are Lazy
Derived values only run when they’re read and are lazy evaluted which means they only update when they change and not when their dependencies change to avoid unnecessary work.
In this example, even if max
depends on count
, it only updates when max
updates:
<script lang="ts">
let count = $state(0)
let max = $derived(count >= 4)
// only logs when `max` changes
$inspect(max)
</script>
<button onclick={() => count++} disabled={max}>
{count}
</button>
$inspect
rune only runs in development and is great for seeing state updates and debugging.Derived Dependency Tracking
You can pass a function to a derived without losing reactivity, because a signal only has to be read to become a dependency:
<script lang="ts">
let count = $state(0)
let max = $derived(limit())
function limit() {
return count > 4 // 📖
}
</script>
<button onclick={() => count++} disabled={max}>
{count}
</button>
This might sound like magic, but the only magic here is the system of signals and runtime reactivity! 🪄
The reason you don’t have to pass state to the function — unless you want to be explicit — is because signals only care where they’re read, as highlighted in the compiled output.
Not passing any arguments:
let disabled = derived(limit)
function limit() {
return get(count) > 4 // 📖
}
Passing count
as argument:
let disabled = derived(() => limit(get(count))) // 📖
function limit(count) {
return count > 4
}
$derived.by
The $derived
rune only accepts an expression by default, but you can use the $derived.by
rune for a more complex derivation:
<script lang="ts">
let cart = $state([
{ item: '🍎', total: 10 },
{ item: '🍌', total: 10 }
])
let total = $derived.by(() => {
let sum = 0
for (let item of cart) {
sum += item.total
}
return sum
})
</script>
<p>Total: {total}€</p>
Svelte recommends you keep deriveds free of side-effects. You can’t update state inside of deriveds to protect you from unintended side-effects:
<script lang="ts">
let count = $state(0)
let double = $derived.by(() => {
// ⛔️ error
count++
})
</script>
Destructuring From Deriveds
Going back to a previous example, you can also use derived state to keep reactivity when using destructuring:
<script lang="ts">
let editor = $state({
theme: 'dark',
content: '<h1>Svelte</h1>'
})
// ⛔️ not reactive
let { theme, content } = editor
// 👍️ reactive
let { theme, content } = $derived(editor)
</script>
{@html content}
Effects
The last main rune you should know about is the $effect
rune.
Effects are functions that run when the component is added to the DOM and when their dependencies change.
State that is read inside of an effect will be tracked:
<script lang="ts">
let count = $state(0)
$effect(() => {
// 🕵️ tracked
console.log(count)
})
</script>
<button onclick={() => count++}>Click</button>
Values are only tracked if they’re read.
Here if condition
is true
, then condition
and count
are going to be tracked. If condition
is false, then the effect only reruns when condition
changes:
<script lang="ts">
let count = $state(0)
let condition = $state(false)
$effect(() => {
if (condition) {
console.log(count) // 📖
}
})
</script>
<button onclick={() => condition = !condition}>Toggle</button>
<button onclick={() => count++}>Click</button>
Svelte provides an untrack
function if you don’t want to track the state:
<script lang="ts">
import { untrack } from 'svelte'
let a = $state(0)
let b = $state(0)
$effect(() => {
// ⛔️ only runs when `b` changes
console.log(untrack(() => a) + b)
})
</script>
<button onclick={() => a++}>A</button>
<button onclick={() => b++}>B</button>
You can return a function from the effect callback, which reruns when the effect dependencies change, or when the component is removed from the DOM:
<script lang="ts">
let count = $state(0)
let delay = $state(1000)
$effect(() => {
// 🕵️ only `delay` is tracked
const interval = setInterval(() => count++, delay)
// 🧹 clear interval every update
return () => clearInterval(interval)
})
</script>
<button onclick={() => delay *= 2}>+</button>
<span>{count}</span>
<button onclick={() => delay /= 2}>-</button>
Effect Dependency Tracking
When it comes to deeply reactive state, effects only rerun when the object it reads changes and not its properties:
<script lang="ts">
let obj = $state({ current: 0 })
$effect(() => {
// doesn't run if property changes
console.log(obj)
})
$effect(() => {
// you have to track the property
console.log(obj.current)
})
</script>
There are ways around it though! 🤫
You can use JSON.stringify
, $state.snapshot
, or the $inspect
rune to react when the object properties change. The save
function could be some external API used to save the data:
<script lang="ts">
let obj = $state({ current: 0 })
$effect(() => {
JSON.stringify(obj) // 👍️ tracked
save(obj)
})
$effect(() => {
$state.snapshot(obj) // 👍️ tracked
save(obj)
})
$effect(() => {
$inspect(obj) // 👍️ tracked
save(obj)
})
</script>
When Not To Use Effects
Don’t use effects to synchronize state because Svelte queues your effects and runs them last. Using effects to synchronize state can cause unexpected behavior like state being out of sync:
<script lang="ts">
let count = $state(0)
let double = $state(0)
$effect(() => {
// effects run last
double = count * 2
})
</script>
<button onclick={() => {
count++ // 1
console.log(double) // ⚠️ 0
}}>
{double}
</button>
Always derive state when you can instead:
<script lang="ts">
let count = $state(0)
let double = $derived(count * 2)
</script>
<button onclick={() => {
count++ // 1
console.log(double) // 👍️ 2
}}>
{double}
</button>
If you don’t want to track values, you can use the onMount
lifecycle function instead of an effect:
<script lang="ts">
import { onMount } from 'svelte'
onMount(() => {
console.log('Component added 👋')
return () => console.log('🧹 Component removed')
})
</script>
async
callbacks to onMount
and $effect
as their cleanup won't run. You can use async functions, or an IIFE instead.When To Use Effects
Effects should be a last resort when you have to synchronize with an external system that doesn’t understand Svelte’s reactivity. They should only be used for side-effects like fetching data from an API, or working with the DOM directly.
In this example, we’re using the Pokemon API and getAbortSignal
from Svelte to avoid making a bunch of requests when doing a search:
<script lang="ts">
import { getAbortSignal } from 'svelte'
let pokemon = $state('charizard')
let image = $state('')
async function getPokemon(pokemon: string) {
const baseUrl = 'https://pokeapi.co/api/v2/pokemon'
const response = await fetch(`${baseUrl}/${pokemon}`, {
// aborts when derived and effect reruns
signal: getAbortSignal()
})
if (!response.ok) throw new Error('💣️ oops!')
return response.json()
}
$effect(() => {
getPokemon(pokemon).then(data => {
image = data.sprites.front_default
})
})
</script>
<input
type="search"
placeholder="Enter Pokemon name"
oninput={(e) => pokemon = (e.target as HTMLInputElement).value}
/>
<img src={image} alt={pokemon} />
$effect.pre
Your effects run after the DOM updates in a microtask, but sometimes you might need to do work before the DOM updates like measuring an element, or scroll position — in that case, you can use the $effect.pre
rune.
A great example is the GSAP Flip plugin for animating view changes when you update the DOM. It needs to measure the position, size, and rotation of elements before and after the DOM update.
In this example, we measure the elements before the DOM updates, and use tick
to wait for the DOM update:
<script lang="ts">
import { gsap } from 'gsap'
import { Flip } from 'gsap/Flip'
import { tick } from 'svelte'
gsap.registerPlugin(Flip)
let items = $state([...Array(20).keys()])
$effect.pre(() => {
// track `items` as a dependency
items
// measure elements before the DOM updates
const state = Flip.getState('.item')
// wait for the DOM update
tick().then(() => {
// do the FLIP animation
Flip.from(state, { duration: 1, stagger: 0.01, ease: 'power1.inOut' })
})
})
function shuffle() {
items = items.toSorted(() => Math.random() - 0.5)
}
</script>
<div class="container">
{#each items as item (item)}
<div class="item">{item}</div>
{/each}
</div>
<button onclick={shuffle}>Shuffle</button>
<style>
.container {
width: 600px;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.5rem;
color: orangered;
font-size: 3rem;
font-weight: 700;
text-shadow: 2px 2px 0px #000;
.item {
display: grid;
place-content: center;
aspect-ratio: 1;
background-color: #222;
border: 1px solid #333;
border-radius: 1rem;
}
}
button {
margin-top: 1rem;
font-size: 2rem;
}
</style>
tick
is a useful lifecyle function that schedules a task to run in the next microtask when all the work is done, and before the DOM updates.
State In Functions And Classes
So far, we only used runes at the top-level of our components, but you can use state, deriveds, and effects inside functions and classes.
You can use runes in a JavaScript module by using the .svelte.js
or .svelte.ts
extension to tell Svelte that it’s a special file, so it doesn’t have to check every file for runes.
In this example, we’re creating a createCounter
function that holds the count
value and returns a increment
and decrement
function:
export function createCounter(initial: number) {
let count = $state(initial)
$effect(() => {
console.log(count)
})
const increment = () => count++
const decrement = () => count--
return {
get count() { return count },
set count(v) { count = v },
increment,
decrement
}
}
Here’s how it’s used inside of a Svelte component:
<script lang="ts">
import { createCounter } from './counter.svelte'
const counter = createCounter(0)
</script>
<button onclick={counter.decrement}>-</button>
<span>{counter.count}</span>
<button onclick={counter.increment}>+</button>
Reactive Properties
You’re probably wondering, what’s the deal with the get
and set
methods?
Those are called getters and setters, and they create accessor properties which let you define custom behavior when you read and write to a property.
They’re just part of JavaScript, and you could use functions instead:
export function createCounter(initial: number) {
let count = $state(initial)
const increment = () => count++
const decrement = () => count--
return {
count() { return count },
setCount(v: number) { count = v },
increment,
decrement
}
}
You could return a tuple instead to make the API nicer and destructure the read and write functions like [count, setCount] = createCounter(0)
.
As you can see, the syntax is not as nice compared to using accessors, since you have to use functions everywhere:
<script lang="ts">
import { createCounter } from './counter.svelte'
const counter = createCounter(0)
</script>
<!-- using functions -->
<button onclick={() => counter.setCurrent(counter.count() + 1)}>
{counter.count()}
</button>
<!-- using accessors -->
<button onclick={() => counter.count++}>
{counter.count}
</button>
Reactive Containers
The accessor syntax looks a lot nicer! 😄 You might be wondering, can’t you just return state from the function?
export function createCounter(initial: number) {
let count = $state(initial)
// ⛔️ this doesn't work
return count
}
The reason this doesn’t work is because state is just a regular value. It’s not some magic reactive container. If you want something like that, you could return deeply reactive proxied state:
export function createCounter(initial: number) {
let count = $state({ current: initial })
// 👍️ proxied state
return count
}
You can create a reactive container yourself if you want:
// reactive container utility
export function reactive<T>(initial: T) {
let value = $state<{ current: T }>({ current: initial })
return value
}
export function createCounter(initial: number) {
// reactive container
let count = reactive(initial)
const increment = () => count.current++
const decrement = () => count.current--
return { count, increment, decrement }
}
Even destructuring works, since count
is not just a regular value:
<script lang="ts">
import { createCounter } from './counter.svelte'
const { count } = createCounter(0)
</script>
<button onclick={() => count.current++}>
{count.current}
</button>
That seems super useful…so why doesn’t Svelte provide this utility?
It’s mostly because it’s a few lines of code, but another reason is classes. If you use state inside classes, you get extra benefits which you don’t get using functions.
Svelte turns any class fields declared with state into private fields with matching get
/set
methods, unless you declare them yourself:
export class Counter {
constructor(initial: number) {
// turned into `get` and `set` methods
this.count = $state(initial)
}
increment() {
this.count++
}
decrement() {
this.count--
}
}
If you look at the output, you would see something like this:
class Counter {
#count
get count() { ... }
set count(v) { ... }
}
There’s only one gotcha with classes and it’s how this
works.
For example, using a method like counter.increment
inside onclick
doesn’t work, because this
refers to where it was called:
<script lang="ts">
import { Counter } from './counter.svelte'
const counter = new Counter(0)
</script>
<button onclick={counter.decrement}>-</button>
<span>{counter.count}</span>
<button onclick={counter.increment}>+</button>
You can see it for yourself:
increment() {
console.log(this) // button
this.count++
}
decrement() {
console.log(this) // button
this.count--
}
You either have to pass an anonymous function like () => counter.increment()
to onclick
, or define the methods using arrow functions that don’t bind their own this
:
increment = () =>
console.log(this) // class
this.current++
}
decrement = () => {
console.log(this) // class
this.current--
}
The only downside with arrow functions is that you’re creating a new function every time time you call it, but everything works as expected.
Passing State Into Functions And Classes
Because state is a regular value, it loses reactivity when you pass it into a function or a class.
In this example, we pass count
to a Doubler
class to double the value when count
updates. However, it’s not reactive since count
is a regular value:
<script lang="ts">
class Doubler {
constructor(count: number) {
this.current = $derived(count * 2) // 0 * 2
}
}
let count = $state(0)
const double = new Doubler(count) // 0
</script>
<button onclick={() => count++}>
{double.current}
</button>
Svelte even gives you a warning with a hint:
This reference only captures the initial value of
count
. Did you mean to reference it inside a closure instead?
To get the latest count
value, we can pass a function instead:
<script lang="ts">
class Doubler {
constructor(count: () => number) {
this.value = $derived(count() * 2) // () => get(count) * 2
}
}
let count = $state(0)
const doubler = new Doubler(() => count) // () => get(count)
</script>
<button onclick={() => count++}>
{doubler.value}
</button>
You could use the reactive utility from before! Let’s use a class version this time:
<script lang="ts">
class Reactive<T> {
constructor(initial: T) {
this.current = $state<T>(initial)
}
}
class Doubler {
constructor(count: Reactive<number>) {
this.current = $derived(count.current * 2)
}
}
const count = new Reactive(0)
const double = new Doubler(count)
</script>
<button onclick={() => count.current++}>
{double.current}
</button>
Runes are reactive primitives that give you the flexibility to create your own reactivity system.
Reactive Global State
Creating global reactive state in Svelte is simple as exporting deep state from a module, like a config which can be used across your app:
interface Config {
theme: 'light' | 'dark'
}
export const config = $state<Config>({ theme: 'dark' })
export function toggleTheme() {
config.theme = config.theme === 'light' ? 'dark' : 'light'
}
<script>
import { config, toggleTheme } from './config.svelte'
</script>
<button onclick={toggleTheme}>
{config.theme}
</button>
You could use a function, or a class for the config:
type Themes = 'light' | 'dark'
class Config {
theme = $state<Themes>('dark')
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light'
}
}
export const config = new Config()
It doesn’t matter if you use functions or classes, as long as you understand how Svelte reactivity works.
How Svelte Reactivity Works
I believe that understanding how something works gives you greater enjoyment in life by being more competent at what you do.
I mentioned how Svelte uses signals for reactivity, but so do many other frameworks like Angular, Solid, Vue, and Qwik. There’s even a proposal to add signals to JavaScript itself.
So far we learned that assignments cause updates in Svelte. There’s nothing special about =
though! It just creates a function call to update the value:
<script lang="ts">
let value = $state('🍎')
value = '🍌' // set(value, '🍌')
</script>
<!-- how does this get updated? -->
{value}
A signal is just a container that holds a value and subscribers that are notified when that value updates, so it doesn’t do anything on its own:
function createSignal(value) {
const signal = {
value,
subscribers: new Set()
}
return signal
}
You need effects to react to signals and effects are just functions that run when a signal updates.
That’s how Svelte updates the DOM by compiling your template into effects. This is referred to as a tracking context:
<!-- template_effect(() => set_text(text, get(value))) -->
{value}
Everything starts with a root effect and your component is a nested effect inside of it. This way, Svelte can keep track of effects for cleanup.
When the effect runs, it invokes the callback function and sets it as the active effect in some variable:
let effect = null
function template_effect(fn) {
// set active effect
effect = fn
}
The magic happens when you read a signal inside of an effect. When value
is read, it adds the active effect as a subscriber:
let effect = fn
function get(signal) {
// add effect to subscribers
signal.subscribers.add(effect)
// return value
return signal.value
}
Later, when you write to count
it notifies the subscribers and recreates the dependency graph when it reads the signal inside the effect:
function set(signal, value) {
// update signal
signal.value = value
// notify subscribers
signal.subscribers.forEach(effect => effect())
}
This is oversimplified, but it happens every update and that’s why it’s called runtime reactivity, because it happens as your code runs!
Svelte doesn’t compile reactivity, it only compiles the implementation details. That’s how you can use signals like a regular value. In other frameworks, you always have to read and write them using functions, or accessors.
Deriveds are also effects that track their own dependencies and return a signal. You can pass a function with state inside to a derived, and it’s tracked when it’s read inside like an effect:
<script lang="ts">
let value = $state('🍎')
let code = $derived(getCodePoint())
function getCodePoint() {
// `value` is read inside derived effect
return value.codePointAt(0).toString(16)
}
value = '🍌'
</script>
<!-- `code` is read inside template effect -->
{code}
I want to emphasize how $state
is not some magic reactive container, but a regular value; which is why you need a function or a getter to get the latest value when the effect reruns — unless you’re using deep state.
If emoji.code
was a regular value and not a getter, then the effect would always return the same value, even though it reacts to the change:
<script lang="ts">
class Emoji {
constructor(emoji: string) {
// turned into `get` and `set` methods
this.current = $state(emoji)
this.code = $derived(this.current.codePointAt(0).toString(16))
}
}
const emoji = new Emoji('🍎')
emoji.current = '🍌'
</script>
<!-- template_effect(() => set_text(text, emoji.code)) -->
{emoji.code}
As the React people love to say, “it’s just JavaScript!” 😄
Why You Should Avoid Effects
I don’t want to scare you from using effects. Honestly, it’s not a big deal if you sometimes use effects when you shouldn’t.
The problem is that it’s easy to overcomplicate your code with effects, because it seems like the right thing to do.
In this example, I have a counter
value I want to read and write using the Web Storage API each time it updates. That’s a side-effect! It seems resonable to use an effect:
class Counter {
constructor(initial: number) {
this.count = $state(initial)
$effect(() => {
const savedCount = localStorage.getItem('count')
if (savedCount) this.count = parseInt(savedCount)
})
$effect(() => {
localStorage.setItem('count', this.count.toString())
})
}
}
The problem only arises if you create the counter outside the component initialization phase:
export const counter = new Counter(0)
Oops! Immediately, there’s an error:
effect_orphan
$effect
can only be used inside an effect (e.g. during component initialisation).
In the previous section we learned that everything starts with a root effect, so Svelte can run the teardown logic for nested effects when the component is removed.
In this case, you’re trying to create an effect outside that root effect, which is not allowed.
Svelte provides an advanced $effect.root
rune to create your own root effect, but now you have to run the cleanup manually:
class Counter {
#cleanup
constructor(initial: number) {
this.count = $state(initial)
// manual cleanup 😮💨
this.cleanup = $effect.root(() => {
$effect(() => {
const savedCount = localStorage.getItem('count')
if (savedCount) this.count = parseInt(savedCount)
})
$effect(() => {
localStorage.setItem('count', this.count.toString())
})
return () => console.log('🧹 cleanup')
})
}
destroy() {
this.#cleanup()
}
}
Then you learn about the $effect.tracking
rune used to know if you’re inside a tracking context like the effect in your template, so maybe that’s it:
class Counter {
constructor(initial: number) {
this.count = $state(initial)
if ($effect.tracking()) {
$effect(() => {
const savedCount = localStorage.getItem('count')
if (savedCount) this.count = parseInt(savedCount)
})
$effect(() => {
localStorage.setItem('count', this.count.toString())
})
}
}
}
But there’s another problem! The effect is never going to run when the counter is created because you’re not inside a tracking context. 😩
It would make more sense to move the effect where you read the value — this way, it’s read inside of a tracking context like the template effect:
export class Counter {
constructor(initial: number) {
this.#count = $state(initial)
}
get count() {
if ($effect.tracking()) {
$effect(() => {
const savedCount = localStorage.getItem('count')
if (savedCount) this.#count = parseInt(savedCount)
})
}
return this.#count
}
set count(v: number) {
localStorage.setItem('count', v.toString())
this.#count = v
}
}
There’s another problem…
Each time we read the value, we’re creating an effect! 😱 Alright, that’s a simple fix. We can use a variable to track if we already ran the effect:
export class Counter {
#first = true
constructor(initial: number) {
this.#count = $state(initial)
}
get count() {
if ($effect.tracking()) {
$effect(() => {
if (!this.#first) return
const savedCount = localStorage.getItem('count')
if (savedCount) this.#count = parseInt(savedCount)
this.#first = false
})
}
return this.#count
}
set count(v: number) {
localStorage.setItem('count', v.toString())
this.#count = v
}
}
The point I want to make is that none of this is necessary, and you can make everything simpler by doing side-effects inside event handlers like onclick
instead of using effects:
export class Counter {
#first = true
constructor(initial: number) {
this.#count = $state(initial)
}
get count() {
if (this.#first) {
const savedCount = localStorage.getItem('count')
if (savedCount) this.#count = parseInt(savedCount)
this.#first = false
}
return this.#count
}
set count(v: number) {
localStorage.setItem('count', v.toString())
this.#count = v
}
}
Unless you know what you’re doing — if you catch yourself using advanced runes like $effect.root
or $effect.tracking
, you’re doing something wrong.
Using Template Logic
HTML doesn’t have conditionals or loops, but Svelte has control flow blocks ranging from {#if ...}
, {#each ...}
to data loading blocks like {#await ...}
.
Using Conditionals
In Svelte, you can use the {#if ...}
block to conditionally render content:
<script lang="ts">
type Status = 'loading' | 'success' | 'error'
let status = $state<Status>('loading')
</script>
{#if status === 'loading'}
<p>Loading...</p>
{:else if status === 'success'}
<p>Success!</p>
{:else if status === 'error'}
<p>Error</p>
{:else}
<p>Impossible state</p>
{/if}
Looping Over Data
To loop over a list of items, you use the {#each ...}
block:
<script lang="ts">
let todos = $state([
{ id: 1, text: 'Todo 1', done: false },
{ id: 2, text: 'Todo 2', done: false },
{ id: 3, text: 'Todo 3', done: false },
{ id: 4, text: 'Todo 4', done: false }
])
</script>
<ul>
{#each todos as todo}
<li>
<input checked={todo.done} type="checkbox" />
<span>{todo.text}</span>
</li>
{:else}
<p>No items</p>
{/each}
</ul>
You can destructure the items values you’re iterating over, get the current item index and provide a key, so Svelte can keep track of changes:
<ul>
{#each todos as { id, text, done }, i (id)}
<li>
<input checked={done} type="checkbox" />
<span style:color={i % 2 === 0 ? 'orangered' : ''}>{text}</span>
</li>
{/each}
</ul>
You can omit the as
part inside the {#each ...}
block if you just want to loop over an arbitrary amount of items to create a grid for example:
<div class="grid">
{#each Array(10), row}
{#each Array(10), col}
<div class="cell">{row},{col}</div>
{/each}
{/each}
</div>
<style>
.grid {
max-width: 400px;
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 0.5rem;
.cell {
padding: 1rem;
border: 1px solid #ccc;
}
}
</style>
You can loop over any iterable that works with Array.from
from a Map
and Set
object, to generators:
<script lang="ts">
let itemsMap = new Map([
['🍎', 'apple'],
['🍌', 'banana'],
])
let itemsSet = new Set(['🍎', '🍌'])
function* itemsGenerator() {
yield '🍎'
yield '🍌'
}
</script>
<ul>
{#each itemsMap as [key, value]}
<li>{key}: {value}</li>
{/each}
</ul>
<ul>
{#each itemsSet as item}
<li>{item}</li>
{/each}
</ul>
<ul>
{#each itemsGenerator() as item}
<li>{item}</li>
{/each}
</ul>
Svelte even has reactive versions of built-in JavaScript objects, which we’re going to look at later.
Asynchronous Data Loading
Previously, we fetched some pokemon data inside of an effect, but we haven’t handled the loading, error, or success state.
Svelte has an {#await ...}
block for dealing with promises which handles loading, error, and success states:
<script lang="ts">
async function getPokemon(pokemon: string) {
const baseUrl = 'https://pokeapi.co/api/v2/pokemon'
const response = await fetch(`${baseUrl}/${pokemon}`)
if (!response.ok) throw new Error('💣️ oops!')
let { name, sprites } = await response.json()
return { name, image: sprites['front_default'] }
}
</script>
{#await getPokemon('charizard')}
<p>loading...</p>
{:then pokemon}
<p>{pokemon.name}</p>
<img src={pokemon.image} alt={pokemon.name} />
{:catch error}
<p>{error.message}</p>
{/await}
You can omit the catch
block if you don’t care about errors, and the initial block if you only care about the result:
{#await getPokemon('charizard') then pokemon}
<p>{pokemon.name}</p>
<img src={pokemon.image} alt={pokemon.name} />
{/await}
Asynchronous Svelte Aside
In the near future, you’re going to be able to await
a promise directly in a Svelte component. You can try it today by enabling the experimental async flag in your Svelte config:
export default {
compilerOptions: {
experimental: {
async: true
}
}
}
At the moment you have to create a boundary at the root of your app, or where you want to use the await
keyword:
<script lang="ts">
let { children } = $props()
</script>
<svelte:boundary>
{#snippet pending()}
<!-- only shows when the component is added -->
<p>loading...</p>
{/snippet}
{@render children?.()}
</svelte:boundary>
Then inside of a component, you can use the await
keyword in the script block, or template:
<script lang="ts">
// same Pokemon API as before
import { getPokemon } from './pokemon.ts'
let pokemon = await getPokemon('charizard')
</script>
<p>{pokemon.name}</p>
<img src={pokemon.image} alt={pokemon.name} />
You can use the $effect.pending
rune to show a loading state:
<!-- shows when loading new data -->
{#if $effect.pending()}
<p>loading...</p>
{:else}
<p>{(await pokemon).name}</p>
<img src={(await pokemon).image} alt={(await pokemon).name} />
{/if}
SvelteKit takes this even further with remote functions, where you can fetch data from a server by invoking a function on the client.
Recreating Elements
You can use the {#key ...}
block to recreate elements when state updates. This is useful for replaying transitions, which we’re going to learn about later:
<script lang="ts">
import { fade } from 'svelte/transition'
let value = $state(0)
</script>
{#key value}
<div in:fade>👻</div>
{/key}
<button onclick={() => value++}>Spook</button>
Local Constants
You can use the @const
tag to define block-scoped readonly local constants in the Svelte template.
Local constants can only be defined as a child of blocks like {#if ...}
, {#else ...}
, {#await ...}
, and <Component />
.
In this example, we can destructure text
and done
from the todo
object while keeping the original reference:
<ul>
{#each todos as todo}
{@const { text, done: checked } = todo}
<li>
<input {checked} type="checkbox" />
<span>{text}</span>
</li>
{/each}
</ul>
In this example, we’re creating a SVG grid of squares using local constants to keep everything organized and legible:
<script lang="ts">
let size = 800
let tiles = 8
</script>
<svg width={size} height={size}>
{#each Array(tiles), col}
{#each Array(tiles), row}
{@const tile = size / tiles}
{@const x = col * tile}
{@const y = row * tile}
{@const width = tile}
{@const height = tile}
{@const fill = (col + row) % 2 === 0 ? 'orangered' : 'white'}
<rect {x} {y} {width} {height} {fill} />
{/each}
{/each}
</svg>
Listening To Events
You can listen to DOM events by adding attributes that start with on
to elements. In the case of a mouse click, you would add the onclick
attribute to a <button>
element:
<script lang="ts">
function onclick() {
console.log('clicked')
}
</script>
<!-- using an inline function -->
<button onclick={() => console.log('clicked')}>Click</button>
<!-- passing a function -->
<button onclick={onclick}>Click</button>
<!-- using the shorthand -->
<button {onclick}>Click</button>
You can spread events, since they’re just attributes:
<script lang="ts">
const events = {
onclick: () => console.log('clicked'),
ondblclick: () => console.log('double clicked')
}
</script>
<button {...events}>Click</button>
This example uses the onmousemove
event to update the mouse position:
<script lang="ts">
let mouse = $state({ x: 0, y: 0 })
// the event is automatically passed
function onmousemove(e: MouseEvent) {
mouse.x = e.clientX
mouse.y = e.clientY
}
</script>
<div {onmousemove}>
The mouse position is {mouse.x} x {mouse.y}
</div>
<style>
div {
width: 100%;
height: 100%;
}
</style>
You can prevent the default behavior by using e.preventDefault()
. This is useful for things like when you want to control a form with JavaScript and avoid a page reload:
<script lang="ts">
function onsubmit(e: SubmitEvent) {
e.preventDefault()
const data = new FormData(this)
const email = data.get('email')
console.log(email)
}
</script>
<form {onsubmit}>
<input type="email" name="email" />
<button type="submit">Subscribe</button>
</form>
Using Data Bindings
In JavaScript, it’s common to listen for the user input on the <input>
element through the input
event, and update a value. This is called one-way data binding since updating the value doesn’t update the input. In Svelte, you can use the bind:
directive to keep them in sync.
Two-Way Data Binding
Having to do value={search}
and oninput={(e) => search = e.target.value}
on the <input>
element to update search
is mundane for something you do often:
<script lang="ts">
let list = $state(['angular', 'react', 'svelte', 'vue'])
let filteredList = $derived(list.filter(item => item.includes(search)))
let search = $state('')
</script>
<input
type="search"
value={search}
oninput={(e) => search = (e.target as HTMLInputElement).value}
/>
<ul>
{#each filteredList as item}
<li>{item}</li>
{/each}
</ul>
Thankfully, Svelte supports two-way data binding using the bind:
directive. If you update the value, it updates the input and vice versa:
<input type="search" bind:value={search} />
One of the more useful bindings is bind:this
to get a reference to a DOM node such as the <canvas>
element for example:
<script lang="ts">
// this is `undefined` until the component is added
let canvas: HTMLCanvasElement
$effect(() => {
// ⛔️ don't do this
const canvas = document.querySelector('canvas')!
// 👍️ bind the value instead
const ctx = canvas.getContext('2d')
})
</script>
<canvas bind:this={canvas}></canvas>
Function Bindings
Another useful thing to know about are function bindings if you need to validate some input, or link one value to another.
This example transforms the text the user types into the Mocking SpongeBob case:
<script lang="ts">
let text = $state('I love Svelte')
function toSpongeBobCase(text: string) {
return text
.split('')
.map((c, i) => i % 2 === 1 ? c.toUpperCase() : c.toLowerCase())
.join('')
}
</script>
<textarea
value={toSpongeBobCase(text)}
oninput={(e) => {
text = toSpongeBobCase((e.target as HTMLInputElement).value)
}}
></textarea>
Instead of passing an expression like bind:property={expression}
, you can pass a function binding like bind:property={get, set}
to have more control over what happens when you read and write a value:
<!-- ... -->
<textarea
bind:value={
() => toSpongeBobCase(text),
(v: string) => text = toSpongeBobCase(v)
}
></textarea>
Readonly Bindings
Svelte provides two-way bindings, and readonly bindings for different elements you can find in the Svelte documentation for bind.
There are media bindings for <audio>
, <video>
, and <img>
elements:
<script lang="ts">
let clip = 'video.mp4'
let currentTime = $state(0)
let duration = $state(0)
let paused = $state(true)
</script>
<div class="container">
<video src={clip} bind:currentTime bind:duration bind:paused></video>
<div class="controls">
<button onclick={() => paused = !paused}>
{paused ? 'Play' : 'Pause'}
</button>
<span>{currentTime.toFixed()}/{duration.toFixed()}</span>
<input type="range" bind:value={currentTime} max={duration} />
</div>
</div>
<style>
.container {
width: 600px;
video {
width: 100%;
border-radius: 0.5rem;
}
.controls {
display: flex;
gap: 0.5rem;
input[type="range"] {
flex-grow: 1;
}
}
}
</style>
There are also readonly bindings for visible elements that use ResizeObserver to measure any dimension changes:
<script lang="ts">
let width = $state()
let height = $state()
</script>
<div class="container" bind:clientWidth={width} bind:clientHeight={height}>
<div class="text" contenteditable>Edit this text</div>
<div class="size">{width} x {height}</div>
</div>
<style>
.container {
position: relative;
display: inline-block;
padding: 0.5rem;
border: 1px solid orangered;
.text {
font-size: 2rem;
}
.size {
position: absolute;
left: 50%;
bottom: 0px;
padding: 0.5rem;
translate: -50% 100%;
color: black;
background-color: orangered;
font-weight: 700;
white-space: pre;
}
}
</style>
In the next section we’re going to learn about components and how we can also bind the properties we pass to them, making the data flow from child to parent.
Svelte Components
Frameworks are not tools for organizing your code, they are tools for organizing your mind. — Rich Harris
A Svelte component is a file that ends with a .svelte
extension. You can think of components as blocks that include the markup, styles, and logic that can be used across your app, and can be combined with other blocks.
Let’s use a basic todo list app as an example:
<script lang="ts">
import { slide } from 'svelte/transition'
type Todo = { id: string; text: string; completed: boolean }
type Filter = 'all' | 'active' | 'completed'
let todo = $state('')
let todos = $state<Todo[]>([])
let filter = $state<Filter>('all')
let filteredTodos = $derived(filterTodos())
let remaining = $derived(remainingTodos())
function addTodo(e: SubmitEvent) {
e.preventDefault()
todos.push({
id: crypto.randomUUID(),
text: todo,
completed: false
})
todo = ''
}
function removeTodo(todo: Todo) {
todos = todos.filter((t) => t.id !== todo.id)
}
function filterTodos() {
return todos.filter((todo) => {
if (filter === 'all') return true
if (filter === 'active') return !todo.completed
if (filter === 'completed') return todo.completed
})
}
function setFilter(newFilter: Filter) {
filter = newFilter
}
function remainingTodos() {
return todos.filter((todo) => !todo.completed).length
}
function clearCompleted() {
todos = todos.filter((todo) => !todo.completed)
}
</script>
<form onsubmit={addTodo}>
<input type="text" bind:value={todo} />
</form>
<ul>
{#each filteredTodos as todo (todo.id)}
<li transition:slide>
<input type="checkbox" bind:checked={todo.completed} />
<input type="text" bind:value={todo.text} />
<button onclick={() => removeTodo(todo)}>🗙</button>
</li>
{/each}
</ul>
<div>
<p>{remaining} {remaining === 1 ? 'item' : 'items'} left</p>
{#each ['all', 'active', 'completed'] as const as filter}
<button onclick={() => setFilter(filter)}>{filter}</button>
{/each}
<button onclick={clearCompleted}>Clear completed</button>
</div>
Let’s take the contents of the Todos.svelte
file and break it into multiple components. You can keep everything organized and place the files inside a todos
folder:
todos/
├── Todos.svelte
├── AddTodo.svelte
├── TodoList.svelte
├── TodoItem.svelte
└── TodoFilter.svelte
Component have to use a capitalized tag such as <Component>
, or dot notation like <my.component>
. How you name the file is irrelevant, but most often you’re going to see PascalCase, so that’s what I’m going to use. Personally, I prefer kebab-case.
Let’s create the <AddTodo>
component that’s going to handle adding a new todo. To pass data from one component to another, we use properties, or props for short — similar to how you pass attributes to elements.
To receive the props, we use the $props
rune:
<script lang="ts">
interface Props {
todo: string
addTodo: () => void
}
let props: Props = $props()
</script>
<form onsubmit={props.addTodo}>
<input type="text" bind:value={props.todo} />
</form>
You can destructure props, rename them, set a default value, and spread the rest of the props:
<script lang="ts">
interface Props {
todo: string
addTodo: () => void
}
let { todo = 'Fallback', addTodo, ...props }: Props = $props()
</script>
<form onsubmit={addTodo} {...props}>
<input type="text" bind:value={todo} />
</form>
To update todo
from the child component, we have to let Svelte know it’s okay for the child to mutate the parent state by using the $bindable
rune:
<script lang="ts">
interface Props {
todo: string
addTodo: () => void
}
let { todo = $bindable('Fallback'), addTodo } = $props()
</script>
<form onsubmit={addTodo}>
<input type="text" bind:value={todo} />
</form>
You can now safely bind the todo
prop:
<script lang="ts">
import AddTodo from './AddTodo.svelte'
let todo = $state('')
// ...
</script>
<AddTodo bind:todo {addTodo} />
In reality, you don’t have to do this. It makes more sense to move the todo
state inside <AddTodo>
and use a callback prop to change it:
<script lang="ts">
function addTodo(todo: Todo) {
todos.push({
id: crypto.randomUUID(),
text: todo,
completed: false
})
}
// ...
</script>
<AddTodo {addTodo} />
Let’s update the <AddTodo>
component:
<script lang="ts">
interface Props {
addTodo: (todo: string) => void
}
let { addTodo }: Props = $props()
let todo = $state('')
function onsubmit(e: SubmitEvent) {
e.preventDefault()
addTodo(todo)
todo = ''
}
</script>
<form {onsubmit}>
<input type="text" bind:value={todo} />
</form>
You can submit the todo by pressing enter, and it won’t reload the page. Instead of binding the value, you can also get the value from the form onsubmit
event.
Let’s create the <TodoList>
component to render the list of todos, and use a Svelte transition to spice it up:
<script lang="ts">
import { slide } from 'svelte/transition'
interface Props {
todos: { id: number; text: string; completed: boolean }[]
removeTodo: (id: number) => void
}
let { todos, removeTodo }: Props = $props()
</script>
<ul>
{#each todos as todo, i (todo.id)}
<li transition:slide>
<input type="checkbox" bind:checked={todo.completed} />
<input type="text" bind:value={todo.text} />
<button onclick={() => removeTodo(todo.id)}>🗙</button>
</li>
{/each}
</ul>
Let’s pass the filteredTodos
and removeTodo
props:
<script lang="ts">
import AddTodo from './AddTodo.svelte'
import TodoList from './TodoList.svelte'
</script>
<AddTodo {todo} {addTodo} />
<TodoList todos={filteredTodos} {removeTodo} />
Let’s create the <TodoFilter>
component to filter the todos:
<script lang="ts">
type Filter = 'all' | 'active' | 'completed'
interface Props {
remaining: number
setFilter: (filter: Filter) => void
clearCompleted: () => void
}
let { remaining, setFilter, clearCompleted }: Props = $props()
</script>
<div>
<p>{remaining} {remaining === 1 ? 'item' : 'items'} left</p>
{#each ['all', 'active', 'completed'] as const as filter}
<button onclick={() => setFilter(filter)}>{filter}</button>
{/each}
<button onclick={clearCompleted}>Clear completed</button>
</div>
Let’s pass the remaining
, setFilter
, and clearCompleted
props:
<script lang="ts">
import AddTodo from './AddTodo.svelte'
import TodoList from './TodoList.svelte'
import TodoFilter from './TodoFilter.svelte'
</script>
<AddTodo {todo} {addTodo} />
<TodoList todos={filteredTodos} {removeTodo} />
<TodoFilter {remaining} {setFilter} {clearCompleted} />
I left the <TodoItem>
component for last to show the downside of abusing bindings:
<script lang="ts">
import { slide } from 'svelte/transition'
interface Props {
todo: { id: number; text: string; completed: boolean }
removeTodo: (id: number) => void
}
let { todo = $bindable(), removeTodo }: Props = $props()
</script>
<li transition:slide>
<input type="checkbox" bind:checked={todo.completed} />
<input type="text" bind:value={todo.text} />
<button onclick={() => removeTodo(todo.id)}>🗙</button>
</li>
This works, but you’re going to get warnings for mutating todos
in the parent state if you don’t make todos
bindable:
<script lang="ts">
import AddTodo from './AddTodo.svelte'
import TodoList from './TodoList.svelte'
import TodoFilter from './TodoFilter.svelte'
</script>
<AddTodo {todo} {addTodo} />
<TodoList bind:todos={filteredTodos} {removeTodo} />
<TodoFilter {remaining} {setFilter} {clearCompleted} />
You have to bind each todo
to the todos
array:
<script lang="ts">
import TodoItem from './TodoItem.svelte'
// ...
let { todos = $bindable(), removeTodo }: Props = $props()
</script>
<ul>
{#each todos as todo, i (todo.id)}
<li transition:slide>
<TodoItem bind:todo={todos[i]} {removeTodo} />
</li>
{/each}
</ul>
For this reason, you should avoid mutating props to avoid unexpected state changes. You can use a callback prop instead, to update a value from a child component.
Let’s update the <Todos>
component to use callback props:
<script lang="ts">
function addTodo(e: SubmitEvent) {
e.preventDefault()
const formData = new FormData(this)
todos.push({
id: crypto.randomUUID(),
text: formData.get('todo'),
completed: false
})
form.reset()
}
function toggleTodo(todo: Todo) {
const index = todos.findIndex((t) => t.id === todo.id)
todos[index].completed = !todos[index].completed
}
function updateTodo(todo: Todo) {
const index = todos.findIndex((t) => t.id === todo.id)
todos[index].text = todo.text
}
</script>
<AddTodo {addTodo} />
<TodoList todos={filteredTodos} {toggleTodo} {updateTodo} {removeTodo} />
<TodoFilter {remaining} {setFilter} {clearCompleted} />
The last thing to do is to update the rest of the components to accept callback props:
<script lang="ts">
// ...
let { addTodo }: Props = $props()
</script>
<form onsubmit={addTodo}>
<input type="text" name="todo" />
</form>
<script lang="ts">
import TodoItem from './TodoItem.svelte'
// ...
let { todos, toggleTodo, updateTodo, removeTodo }: Props = $props()
</script>
<ul>
{#each todos as todo (todo.id)}
<TodoItem {todo} {toggleTodo} {updateTodo} {removeTodo} />
{/each}
</ul>
<script lang="ts">
import { slide } from 'svelte/transition'
// ...
let { todo, toggleTodo, updateTodo, removeTodo }: Props = $props()
</script>
<li transition:slide>
<input
type="checkbox"
onchange={() => toggleTodo(todo)}
checked={todo.completed}
/>
<input
type="text"
oninput={() => updateTodo(todo)}
bind:value={todo.text}
/>
<button onclick={() => removeTodo(todo)}>🗙</button>
</li>
In my opinion, you should avoid creating components. If you’re not sure what to turn into a component — don’t. Instead, write everything inside a single component until it gets complicated, or the reusable parts become obvious.
Later we’re going to learn how to talk between components without props, using the context API.
Component Composition
You can compose components through nesting and snippets which hold content that can be passed as props to components similar to slots. Components can also talk to each other through the context API without props or events.
Component Nesting
To show component composition in Svelte, let’s create an accordion component that can have many accordion items.
You can create these files inside an accordion
folder:
accordion/
├── Accordion.svelte
├── AccordionItem.svelte
└── index.ts
Let’s export the accordion components from the index.ts
file:
export { default as Accordion } from './Accordion.svelte'
export { default as AccordionItem } from './AccordionItem.svelte'
In HTML, you can nest elements inside other elements:
<div class="accordion">
<div class="accordion-item">
<button>
<div>Item A</div>
<div class="accordion-icon">👈️</div>
</button>
<div class="accordion-content">Content</div>
</div>
</div>
The fun part of using a framework like Svelte is that you get to decide how you want to compose components.
The <Accordion>
and <AccordionItem>
components can accept children like regular HTML elements using a children
snippet:
<script lang="ts">
import { Accordion, AccordionItem } from './accordion'
</script>
<Accordion>
{#snippet children()}
<AccordionItem title="Item A">
{#snippet children()}
Content
{/snippet}
</AccordionItem>
{/snippet}
</Accordion>
This is tedious, so every component has an implicit children
prop. Any content inside the component tags becomes part of the children
snippet:
<script lang="ts">
import { Accordion, AccordionItem } from './accordion'
</script>
<Accordion>
<AccordionItem title="Item A">
Content
</AccordionItem>
</Accordion>
You can get the children
from the props, and render them using the @render
tag:
<script lang="ts">
let { children } = $props()
</script>
<div class="accordion">
<!-- using a conditional with a fallback -->
{#if children}
{@render children()}
{:else}
<p>Fallback content</p>
{/if}
<!-- using optional chaining -->
{@render children?.()}
</div>
The <AccordionItem>
accepts a label
prop, and we can show the accordion item content using the children
prop which acts like a catch-all for any content inside the component:
<script lang="ts">
import type { Snippet } from 'svelte'
import { slide } from 'svelte/transition'
interface Props {
label: string
children: Snippet
}
let { label, children }: Props = $props()
let open = $state(false)
function toggle() {
open = !open
}
</script>
<div class="accordion-item">
<button onclick={toggle} class="accordion-heading">
<div>{label}</div>
<div class="accordion-icon">👈️</div>
</button>
{#if open}
<div transition:slide class="accordion-content">
{@render children?.()}
</div>
{/if}
</div>
That’s it! 😄 You can now use the <Accordion>
component in your app.
That being said, this has limited composability if you want to change the icon, or position of the individual accordion elements.
Before you know it, you end up with an explosion of props:
<script lang="ts">
import { Accordion, AccordionItem } from './accordion'
</script>
<Accordion>
<AccordionItem
title="Item A"
icon="👈️"
iconPosition="left"
...
>
Content
</AccordionItem>
</Accordion>
That’s not a way to live your life! Instead, you can use inversion of control so the user can render the accordion item however they want.
Snippets
Let’s modify the <AccordionItem>
component to accept an accordionItem
snippet as a prop instead, and pass it the open
state and toggle
function, so we have access to them inside the snippet:
<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
accordionItem?: Snippet<[accordionItem: { open: boolean; toggle: () => void }]>
}
let { accordionItem }: Props = $props()
let open = $state(false)
function toggle() {
open = !open
}
</script>
<div class="accordion-item">
{@render accordionItem?.({ open, toggle })}
</div>
You can define and render a snippet in your component for markup reuse, or delegate the rendering to another component by passing it as a prop:
<script lang="ts">
import { slide } from 'svelte/transition'
import { Accordion, AccordionItem } from './accordion'
</script>
{#snippet accordionItem({ open, toggle })}
<button onclick={toggle} class="accordion-heading">
<div>Item A</div>
<div class"accordion-icon">👈️</div>
</button>
{#if open}
<div transition:slide class="accordion-content">
Content
</div>
{/if}
{/snippet}
<Accordion>
</AccordionItem {accordionItem}>
</Accordion>
You can create an implicit prop by using a snippet inside the component tags. In this example, the accordionItem
snippet becomes a prop on the component:
<script lang="ts">
import { slide } from 'svelte/transition'
import { Accordion, AccordionItem } from './accordion'
</script>
<Accordion>
<AccordionItem>
{#snippet accordionItem({ open, toggle })}
<button onclick={toggle} class="accordion-heading">
<div>Item A</div>
<div class"accordion-icon">👈️</div>
</button>
{#if open}
<div transition:slide class="accordion-content">
Content
</div>
{/if}
{/snippet}
</AccordionItem>
</Accordion>
<script module>
if they don't reference any state in a script
block, and use them in other components.This gives you complete control how the accordion item is rendered.
The Context API
Alright, but what if you’re tasked to add a feature which lets the user control the open and closed state of the accordion items?
You might bind the open
prop from the <Accordion>
component, but then you have an extra prop:
<script lang="ts">
import { slide } from 'svelte/transition'
import { Accordion, AccordionItem } from './accordion'
let open = $state(false)
</script>
<button onclick={() => (open = !open)}>
{open ? 'Close' : 'Open'}
</button>
<Accordion bind:open>
<!-- extra prop -->
<AccordionItem {open} />
</Accordion>
Instead of using props, you can use the context API. The context API is just a JavaScript Map object that holds key-value pairs.
You can set the context in the parent component by using the setContext
function, which accepts a key and a value:
<script lang="ts">
import { setContext } from 'svelte'
import type { Snippet } from 'svelte'
interface Props {
open: boolean
children: Snippet
}
let { open = $bindable(), children }: Props = $props()
setContext('accordion', {
get open() { return open }
})
</script>
<div class="accordion">
{@render children?.()}
</div>
You can get the context in a child component with the getContext
function. Since accordion.open
is a reactive value, we can change the open
state to be a derived value, which updates when accordion.open
changes:
<script lang="ts">
import { getContext } from 'svelte'
// ...
let { accordionItem }: Props = $props()
const accordion = getContext('accordion')
let open = $derived(accordion.open)
function toggle() {
open = !open
}
</script>
<div>
{@render accordionItem?.({ open, toggle })}
</div>
That’s it! 😄
Type-Safe Context
You can make the context API more type-safe by creating a context file with helper functions:
import { getContext, setContext } from 'svelte'
interface Accordion {
open: boolean
}
// unique key
const key = {}
export function setAccordionContext(open: Accordion) {
setContext(key, open)
}
export function getAccordionContext() {
return getContext(key) as Accordion
}
Passing State Into Context
You’ve probably noticed how you can store reactive state in context. Let’s take a step back, and explain how this works:
// why this?
setContext('accordion', {
get open() { return open }
})
// ...and not this?
setContext('accordion', { open })
If you remember how state is a regular value, then you already know how this isn’t reactive because you’re only reading the current value of open
which is never going to update.
Let’s say this is the context API:
const context = new Map()
function setContext(key, value) {
context.set(key, value)
}
function getContext(key) {
return context.get(key)
}
The value passed to context is not a reference to the value
variable, but the 🍌
value itself.
If value
changes after the context is set, it won’t update:
let emoji = '🍌'
setContext('ctx', { emoji })
const ctx = getContext('ctx')
console.log(ctx.emoji) // 🍌
emoji = '🍎'
console.log(ctx.emoji) // 🍌
Svelte doesn’t change how JavaScript works — you need a mechanism which returns the latest value:
let emoji = '🍌'
setContext('ctx', {
getLatestValue() { return emoji }
})
const ctx = getContext('ctx')
console.log(ctx.getLatestValue()) // 🍌
emoji = '🍎'
console.log(ctx.getLatestValue()) // 🍎
Same as before, you can use a function, class, accessor, or proxied state to get and set the value:
import { setContext } from 'svelte'
let emoji = $state('🍌')
// 👍 function
setContext('ctx', {
getEmoji() { return emoji },
updateEmoji(v) { emoji = v },
})
const ctx = getContext('ctx')
ctx.getEmoji()
ctx.updateEmoji('🍎')
// 👍 class
class Emoji {
current = $state('🍌')
}
setContext('ctx', { emoji: new Emoji() })
const ctx = getContext('ctx')
ctx.emoji.current
ctx.emoji.current = '🍎'
// 👍 property accesors
setContext('ctx', {
get emoji() { return emoji },
set emoji(v) { emoji = v },
})
const ctx = getContext('ctx')
ctx.emoji
ctx.emoji = '🍎'
// 👍 proxied state
let emoji = $state({ current: '🍌'})
setContext('ctx', { emoji })
const ctx = getContext('ctx')
ctx.emoji.current
ctx.emoji.current = '🍎'
Module Context
There’s one more trick you should know when it comes to component composition, and it’s the module
script block.
So far, we used the regular script block for component logic that’s unique for every instance:
<script lang="ts">
// unique for every instance
let uid = crypto.randomUUID()
</script>
<p>{uid}</p>
If you want to share code across component instances, you can use the module
script block:
<script lang="ts" module>
// same for every instance
let uid = crypto.randomUUID()
</script>
<p>{uid}</p>
$props.id
.You can also share state between instances:
<script lang="ts" module>
// same for every instance
let uid = crypto.randomUUID()
// state
let count = $state(0)
</script>
<p>{uid}</p>
<button onclick={() => count++}>{count}</button>
This could be useful to control media playback across instances, or exporting snippets and functions the module:
<script lang="ts" module>
// outputs different random number for every instance
let uid = crypto.randomUUID()
// state
let count = $state(0)
// export
export function reset() {
count = 0
}
</script>
<p>{uid}</p>
<button onclick={() => count++}>{count}</button>
This works great for sharing state across component instances, or just exporting some functions from the module:
<script>
import Counter, { reset } from './Counter.svelte'
</script>
<Counter />
<Counter />
<Counter />
<Counter />
<button onclick={() => reset()}>Reset</button>
You can also have a regular script block, and a module
script block in the same component.
Transitions And Animations
In this section, I’m going to show you how you can use Svelte’s built-in transitions and animations to create delightful user interactions.
Transitions
To use a transition, you use the transition:
directive on an element. Transitions play when the element is added to the DOM, and in reverse when the element is removed from the DOM.
This example uses the fade
transition from Svelte to fade in and out two elements. The first element has a duration
option of 600
milliseconds, and the second element has a delay
option of 600
milliseconds:
<script lang="ts">
import { fade } from 'svelte/transition'
let play = $state(false)
</script>
<button onclick={() => (play = !play)}>Play</button>
{#if play}
<div>
<span transition:fade={{ duration: 600 }}>Hello</span>
<span transition:fade={{ delay: 600 }}>World</span>
</div>
{/if}
You can have separate intro and outro transitions using the in:
and out:
directives:
<script lang="ts">
import { fade, fly } from 'svelte/transition'
import { cubicInOut } from 'svelte/easing'
let play = $state(false)
</script>
<button onclick={() => (play = !play)}>Play</button>
{#if play}
<div>
<span
in:fly={{ x: -10, duration: 600, easing: cubicInOut }}
out:fade
>
Hello
</span>
<span
in:fly={{ x: 10, delay: 600, easing: cubicInOut }}
out:fade
>
World
</span>
</div>
{/if}
Svelte also has a lot of built-in easing functions you can use to make a transition feel more natural, or give it more character.
There’s also a bunch of transition events you can listen to, including introstart
, introend
, outrostart
, and outroend
.
Local And Global Transitions
Let’s say you have an {#each ...}
block that renders a list of items using a staggered transition inside of an {#if ...}
block:
<script lang="ts">
import { fade } from 'svelte/transition'
let play = $state(false)
</script>
{#if play}
<div class="grid">
{#each { length: 50 }, i}
<div transition:fade={{ delay: i * 100 }}>
{i + 1}
</div>
{/each}
</div>
{/if}
It doesn’t work, but why?
Transitions are local by default which means they only play when the block they belong to is added or removed from the DOM and not the parent block.
The solution is to use the global
modifier:
<div transition:fade|global={{ delay: i * 100 }}>
{i + 1}
</div>
Transitions were global by default in older versions of Svelte, so keep that in mind if you come across older Svelte code.
Playing Transitions Immediately
You might have noticed that transitions don’t play immediately when you open a page.
If you want that behavior, you can create a component with an effect to trigger the transition when it’s added to the DOM:
<script lang="ts">
import { fade, type FadeParams } from 'svelte/transition'
import type { Snippet } from 'svelte'
interface Props {
children: Snippet
options?: FadeParams
}
let { children, options }: Props = $props()
let play = $state(false)
$effect(() => {
play = true
})
</script>
{#if play}
<div transition:fade={options}>
{@render children?.()}
</div>
{/if}
Now you can use the <Fade>
component in your app:
<script lang="ts">
import { Fade } from './transitions'
</script>
<Fade options={{ duration: 2000 }}>
Isn't this cool?
</Fade>
You could create a more general <Transition>
component that conditionally renders the type of transition you want like <Transition type="fade">
.
Custom Transitions
You can find more built-in transitions in the Svelte documentation. If that isn’t enough, you can also create custom transitions.
Custom transitions are regular function which have to return an object with the transition options and a css
, or tick
function:
<script lang="ts">
import { elasticOut } from 'svelte/easing'
interface Options {
delay?: number
duration?: number
easing?: (t: number) => number
}
function customTransition(node: HTMLElement, options: Options = {}) {
return {
delay: options.delay || 0,
duration: options.duration || 2000,
easing: options.easing || elasticOut,
css: (t: number) => `
color: hsl(${360 * t} , 100%, 80%);
transform: scale(${t});
`
}
}
let play = $state(false)
</script>
<button onclick={() => (play = !play)}>Play</button>
{#if play}
<div in:customTransition>Whoooo!</div>
{/if}
You should always return a css
function, because Svelte is going to create keyframes using the Web Animations API which is always more performant.
The t
argument is the transition progress from 0
to 1
after the easing has been applied — if you have a transition that lasts 2
seconds, where you move an item from 0
pixels to 100
pixels, it’s going to start from 0
pixels and end at 100
pixels.
You can reverse the transition by using the u
argument which is a transition progress from 1
to 0
— if you have a transition that lasts 2
seconds, where you move an item from 100
pixels to 0
pixels, it’s going to start from 100
pixels and end at 0
pixels.
Alternatively, you can return a tick
function when you need to use JavaScript for a transition and Svelte is going to use the requestAnimationFrame API:
<script lang="ts">
interface Options {
duration?: number
}
const chars = '!@#$%&*1234567890-=_+[]{}|;:,.<>/?'
function getRandomCharacter() {
return chars[Math.floor(Math.random() * chars.length)]
}
function scrambleText(node: HTMLElement, options: Options = {}) {
const finalText = node.textContent
const length = finalText.length
return {
duration: options.duration || 2000,
tick: (t) => {
let output = ''
for (let i = 0; i < length; i++) {
if (t > i / length) {
output += finalText[i]
} else {
output += getRandomCharacter()
}
}
node.textContent = output
}
}
}
let play = $state(false)
</script>
<button onclick={() => (play = !play)}>Scramble text</button>
{#if play}
<p transition:scrambleText>Scrambling Text Effect</p>
{/if}
You would of course define these custom transitions using whichever method you prefer in a separate file and import them in your app.
Coordinating Transitions Between Different Elements
In this example, we have a section for published posts and archived posts where you can archive and unarchive post:
<script lang="ts">
interface Post {
id: number
title: string
description: string
published: boolean
}
let posts = $state<Post[]>([
{
id: 1,
title: 'Title',
description: 'Content',
published: true,
},
// ...
])
function togglePublished(post: Post) {
const index = posts.findIndex((p) => p.id === post.id)
posts[index].published = !posts[index].published
}
function removePost(post: Post) {
const index = posts.findIndex((p) => p.id === post.id)
posts.splice(index, 1)
}
</script>
<div>
<h2>Posts</h2>
<section>
{#each posts.filter((posts) => posts.published) as post (post)}
<article>
<h3>{post.title}</h3>
<p >{post.description}</p>
<div>
<button onclick={() => togglePublished(post)}>💾</button>
<button onclick={() => removePost(post)}>❌</button>
</div>
</article>
{:else}
<p>There are no posts.</p>
{/each}
</section>
</div>
<div>
<h2>Archive</h2>
<section>
{#each posts.filter((posts) => !posts.published) as post (post)}
<article>
<h3>{post.title}</h3>
<div>
<button onclick={() => togglePublished(post)}>♻️</button>
</div>
</article>
{:else}
<p>Archived items go here.</p>
{/each}
</section>
</div>
This works, but the user experience is not great!
In the real world, items don’t simply teleport around like that. The user should have more context for what happened when performing an action. In Svelte, you can coordinate transitions between different elements using the crossfade
transition.
The crossfade
transition creates two transitions named send
and receive
which accept a unique key to know what to transition:
<script lang="ts">
import { crossfade } from 'svelte/transition'
const [send, receive] = crossfade({})
// ...
</script>
<!-- published posts -->
<article
in:receive={{ key: post }}
out:send={{ key: post }}
>
<!-- ... -->
<!-- archived posts -->
<article
in:receive={{ key: post }}
out:send={{ key: post }}
>
<!-- ... -->
You can also pass duration
and a custom fallback
transition options when there are no matching transitions:
const [send, receive] = crossfade({
// the duration is based on the distance
duration: (d) => Math.sqrt(d * 200),
// custom transition
fallback(node, params) {
return {
css: (t) => `
transform: scale(${t});
opacity: ${t};
`
}
}
})
That’s it! 😄
These days there are web APIs to transition view changes like the View Transitions API, but they’re not supported in all browsers yet.
FLIP Animations
In the previous example, we used the crossfade
transition to coordinate transitions between different elements, but it’s not perfect. When you move a post between being archived and published, all the items “wait” for the transition to end before they “snap” into their new position.
We can fix this by using Svelte’s animate:
directive and the flip
function, which calculates the start and end position of an element and animates between them:
<script lang="ts">
import { flip } from 'svelte/animate'
import { crossfade } from 'svelte/transition'
const [send, receive] = crossfade({})
// ...
</script>
<!-- published posts -->
<article
animate:flip={{ duration: 200 }}
in:receive={{ key: post }}
out:send={{ key: post }}
>
<!-- ... -->
<!-- archived posts -->
<article
animate:flip={{ duration: 200 }}
in:receive={{ key: post }}
out:send={{ key: post }}
>
<!-- ... -->
Isn’t it magical? 🪄
FLIP is an animation technique for buttery smooth layout animations. In Svelte, you can only FLIP items inside of an each
block. It’s not reliant on crossfade
, but they work great together.
You can make your own custom animation functions! Animations are triggered only when the contents of an each
block change. You get a reference to the node
, a from
and to
DOMRect which has the size and position of the element before and after the change and parameters
.
Here’s a simplified version of a custom FLIP animation I yoinked from the Svelte source code:
interface Options {
duration?: number
}
function flip(
node: HTMLElement,
{ from, to }: { from: DOMRect; to: DOMRect },
options: Options = {}
) {
const dx = from.left - to.left
const dy = from.top - to.top
const dsx = from.width / to.width
const dsy = from.height / to.height
return {
duration: options.duration || 2000,
css: (t: number, u: number) => {
const x = dx * u
const y = dy * u
const sx = dsx + (1 - dsx) * t
const sy = dsy + (1 - dsy) * t
return `transform: translate(${x}px, ${y}px) scale(${sx}, ${sy})`
}
}
}
This works the same as custom transitions, so you can remind yourself how that works by revisiting it — like with custom transitions, you can also return a tick
function with the same arguments.
Tweened Values And Springs
Imagine if you could take the CSS animation engine, but interpolate any number, including objects and arrays. This is where the Tween
and Spring
classes come in handy.
The Tween
class accepts a target value and options. You can use the current
property to get the current value, and target
to update the value:
<script lang="ts">
import { Tween } from 'svelte/motion'
import { cubicInOut } from 'svelte/easing'
const size = new Tween(50, { duration: 300, easing: cubicInOut })
function onmousedown() {
size.target = 150
}
function onmouseup() {
size.target = 50
}
</script>
<svg width="400" height="400" viewBox="0 0 400 400">
<circle
{onmousedown}
{onmouseup}
cx="200"
cy="200"
r={size.current}
fill="aqua"
/>
</svg>
The Tween
class has the same methods as Tween
, but uses spring physics and doesn’t have a duration. Instead, it has stiffness
, damping
, and precision
options:
<script lang="ts">
import { Spring } from 'svelte/motion'
const size = new Spring(50, { stiffness: 0.1, damping: 0.25, precision: 0.1 })
function onmousedown() {
size.target = 150
}
function onmouseup() {
size.target = 50
}
</script>
<svg width="400" height="400" viewBox="0 0 400 400">
<circle
{onmousedown}
{onmouseup}
cx="200"
cy="200"
r={size.current}
fill="aqua"
/>
</svg>
They both have a set
function, which returns a promise and lets you override the options:
async function onmousedown() {
// using `target` to update the value
size.target = 150
// using `set` to update the value
await size.set(150, { duration: 200 })
}
If you want to update the Tween
or Spring
value when a reactive value changes, you can use the of
method:
<script lang="ts">
import { Spring, Tween } from 'svelte/motion'
let { value, options } = $props()
Tween.of(() => value, options)
Spring.of(() => value, options)
</script>
Using Third Party Libraries
If a specific Svelte package isn’t available, you have the entire JavaScript ecosystem at your fingertips. In this section, we’re going to learn methods at your disposal you can use to integrate third party JavaScript libraries with Svelte.
Component Lifecycle Functions
So far, we got used to Svelte’s declarative syntax and reactivity. Unfortunately, third-party JavaScript libraries usually require direct access to the DOM, and they don’t understand Svelte’s reactivity.
Let’s look at how we can use the popular GSAP JavaScript animation library in Svelte. You can install GSAP with npm i gsap
(if you’re using the Svelte Playground, you can skip this and use imports directly).
Here’s a basic GSAP example for creating a tween animation:
<script type="module">
import gsap from 'gsap'
gsap.to('.box', { rotation: 180, x: 100, duration: 1 })
</script>
<div class="box"></div>
<style>
.box {
width: 100px;
height: 100px;
background-color: orangered;
border-radius: 1rem;
}
</style>
If you tried this example in Svelte, you would get a GSAP target .box not found.
warning. This is because the <script>
part runs first in Svelte, before the component is added to the DOM.
For this reason, Svelte provides an onMount
lifecycle function. The “lifecyle” part refers to the life of the component, since it accepts a callback that runs when it’s added and removed:
<script lang="ts">
import { onMount } from 'svelte'
import gsap from 'gsap'
onMount(() => {
gsap.to('.box', { rotation: 180, x: 100, duration: 1 })
})
</script>
<div class="box"></div>
<style>
.box {
width: 100px;
height: 100px;
background-color: orangered;
border-radius: 1rem;
}
</style>
This works! That being said, it’s not ideal that we query any element with a .box
class on the page.
Using Svelte, we should get a reference to the element instead. You can also return a function from onMount
, or use the onDestroy
lifecycle function for any cleanup when the component is removed:
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import gsap from 'gsap'
let tween: gsap.core.Tween
let target: HTMLElement
onMount(() => {
tween = gsap.to(target, { rotation: 180, x: 100, duration: 1 })
return () => tween.kill()
})
// alternative cleanup
onDestroy(() => {
tween.kill()
})
</script>
<div class="box" bind:this={target}></div>
<style>
.box {
width: 100px;
height: 100px;
background-color: orangered;
border-radius: 1rem;
}
</style>
Effects Versus Lifecycle Functions
You can also use effects to achieve the same thing:
<script lang="ts">
import gsap from 'gsap'
let tween: gsap.core.Tween
let target: HTMLElement
$effect(() => {
tween = gsap.to(target, { rotation: 180, x: 100, duration: 1 })
return () => tween.kill()
})
</script>
<div class="box" bind:this={target}></div>
<style>
.box {
width: 100px;
height: 100px;
background-color: orangered;
border-radius: 1rem;
}
</style>
So why do both of them exist?
Effects aren’t component lifecycle functions, because their “lifecycle” depends on the value inside of them updating.
You could end up tracking some state inside of the effect and then have to untrack the value:
import { untrack } from 'svelte'
let value_you_dont_want_to_track = $state('')
let value_you_want_to_track = $state('')
$effect(() => {
untrack(() => value_you_dont_want_to_track)
console.log(value_you_want_to_track)
})
It’s your choice, of course! If you understand how $effect
works, you won’t get unexpected surprises.
Alright, our code works! Let’s go a step further and create a <Tween>
component which accepts tween
, vars
and children
as props:
<script lang="ts">
import gsap from 'gsap'
import type { Snippet } from 'svelte'
type Props = {
tween: gsap.core.Tween
vars: gsap.TweenVars
children: Snippet
}
let { tween = $bindable(), vars, children }: Props = $props()
let target: HTMLElement
$effect(() => {
tween = gsap.to(target, vars)
return () => tween.kill()
})
</script>
<div bind:this={target}>
{@render children?.()}
</div>
This gives us a generic animation component we can pass any element to, and bind the tween
prop to get the animation controls:
<script lang="ts">
import Tween from './Tween.svelte'
let animation: gsap.core.Tween
</script>
<Tween bind:tween={animation} vars={{ rotation: 180, x: 100, duration: 1 }}>
<div class="box"></div>
</Tween>
<button onclick={() => animation.restart()}>Play</button>
<style>
.box {
width: 100px;
height: 100px;
background-color: #ff4500;
border-radius: 1rem;
}
</style>
Element Lifecycle Functions Using Attachments
So far, we learned how we can use onMount
to get a reference to an element when the component is added.
What if you had onMount
for elements instead of components? You would have attachments.
Attachments are functions you can “attach” to regular elements that run when the element is added to the DOM, or when state inside of them updates:
<script lang="ts">
let color = $state('orangered')
</script>
<canvas
width={400}
height={400}
{@attach (canvas) => {
const context = canvas.getContext('2d')!
$effect(() => {
context.fillStyle = color
context.fillRect(0, 0, canvas.width, canvas.height)
})
}}
></canvas>
Instead of the animation component, we can create an attachment function which can be used on any element.
In this example, the tween
function accept the animations options and an optional callback to get a reference to the tween:
<script lang="ts">
import { gsap } from 'gsap'
function tween(vars, ref) {
let tween: gsap.core.Tween
return (target: HTMLElement) => {
tween = gsap.to(target, vars)
ref?.(tween)
return () => tween.kill()
}
}
let animation: gsap.core.Tween
</script>
<div
{@attach tween(
{ rotation: 180, x: 100, duration: 1 },
(tween) => animation = tween
)}
class="box"
></div>
<button onclick={() => animation.restart()}>Play</button>
The fun comes from picking the API shape you want that works in harmony with Svelte — for example, it would be cool to have different attachments like {@attach tween.from(...)}
or {@attach tween.to(...)}
.
Reactive Data Structures And Utilities
Svelte has reactive versions of JavaScript built-in objects like Map
, Set
, Date
, and URL
.
In this example, we use the reactive version of the built-in Map
object in JavaScript to cache the pokemon data:
<script lang="ts">
import { getAbortSignal } from 'svelte'
import { SvelteMap } from 'svelte/reactivity'
let name = $state('')
// pokemon cache
const pokemon = new SvelteMap<string, unknown>()
async function getPokemon() {
// hits the cache
if (!name || pokemon.has(name)) return
const baseUrl = 'https://pokeapi.co/api/v2/pokemon'
const response = await fetch(`${baseUrl}/${name}`, {
signal: getAbortSignal()
})
if (!response.ok) throw new Error('💣️ oops!')
const data = await response.json()
// add to cache
pokemon.set(name, data)
}
$effect(() => {
getPokemon()
})
</script>
<div class="container">
<div class="actions">
<input type="search" bind:value={name} placeholder="Enter Pokemon name" />
<button onclick={() => pokemon.clear()}>🧹 Clear</button>
</div>
{#each pokemon as [name, details]}
<details>
<summary>{name}</summary>
<div class="details">
<pre>{JSON.stringify(details, null, 2)}</pre>
</div>
</details>
{/each}
</div>
<style>
.container {
width: 800px;
height: 600px;
.actions {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-inline: auto;
margin-bottom: 2rem;
input {
padding: 1rem;
}
}
summary {
text-transform: capitalize;
}
.details {
max-height: 400px;
overflow: hidden scroll;
}
}
</style>
You can find more reactive built-ins like MediaQuery
and prefersReducedMotion
with examples in the Svelte documentation.
Svelte also provides a convenient way to make external APIs reactive, which we’re going to learn about in the next section.
Reactive Events
This is a more advanced topic, but I think it’s useful to know whenever you’re trying to make an external event-based system reactive in Svelte.
An external event is any event you can subscribe to and listen for changes. For example, let’s say I want to create a GSAP animation timeline that I can control with state.
Let’s start by creating the GSAP timeline:
<script lang="ts">
import { onMount } from 'svelte'
import gsap from 'gsap'
type Tween = [string | HTMLElement, gsap.TweenVars]
class Timeline {
#timeline = gsap.timeline()
constructor(tweens: Tween[]) {
this.populateTimeline(tweens)
}
populateTimeline(tweens: Tween[]) {
onMount(() => {
tweens.forEach(([element, vars]) => {
this.#timeline.to(element, vars)
})
})
}
}
const tl = new Timeline([
['.box1', { x: 200, duration: 1 }],
['.box2', { x: 200, duration: 1 }]
])
</script>
<div class="box box1"></div>
<div class="box box2"></div>
<style>
.box {
aspect-ratio: 1;
width: 100px;
background-color: orangered;
border-radius: 1rem;
}
</style>
The next step is to subscribe for updates using the eventCallback
from GSAP.
Here we’re using an effect to synchronize with an external system, so when we update the time, it updates the playhead and causes onUpdate
to fire:
<script lang="ts">
import { onMount } from 'svelte'
import gsap from 'gsap'
type Tween = [string | HTMLElement, gsap.TweenVars]
class Timeline {
#timeline = gsap.timeline()
#time = $state(0)
constructor(tweens: Tween[]) {
this.populateTimeline(tweens)
$effect(() => {
this.#timeline.seek(this.#time)
})
this.#timeline.eventCallback('onUpdate', () => {
this.#time = this.#timeline.time()
})
}
populateTimeline(tweens: Tween[]) {
onMount(() => {
tweens.forEach(([element, vars]) => {
this.#timeline.to(element, vars)
})
})
}
get time() {
return this.#time
}
set time(v) {
this.#time = v
}
}
const tl = new Timeline([
['.box1', { x: 200, duration: 1 }],
['.box2', { x: 200, duration: 1 }]
])
</script>
<label>
<p>Time:</p>
<input bind:value={tl.time} type="range" min={0} max={2} step={0.01} />
</label>
<div class="box box1"></div>
<div class="box box2"></div>
<style>
.box {
aspect-ratio: 1;
width: 100px;
background-color: orangered;
border-radius: 1rem;
}
</style>
So what is the downside of this approach?
If you create an effect outside of a Svelte component, you might run into an effect orphan error in the constructor:
export const tl = new Timeline(...) // ⚠️ effect orphan
The reason this happens is because effects need to be created inside a parent root effect. This is how Svelte keeps track of effects, and runs the cleanup when the component is removed from the DOM.
Previously, we learned that you can use the $effect.root
rune and why you should avoid using it.
We also learned how it makes more sense to create the effect when we read the value, but then we’re creating an effect each time we read the value:
// ...
get time() {
// oops 😅
$effect(() => {
this.#timeline.seek(this.#time)
})
return this.#time
}
Thankfully, Svelte has a createSubscriber function you can use to create a subscriber to subscribe to.
The createSubscriber
function provides a callback, which gives you an update
function. When update
is invoked, it reruns the subscriber. In our example, the subscriber is the time
method:
<script lang="ts">
import { onMount } from 'svelte'
import { createSubscriber } from 'svelte/reactivity'
import gsap from 'gsap'
type Tween = [string | HTMLElement, gsap.TweenVars]
class Timeline {
#timeline = gsap.timeline()
#subscribe
constructor(tweens: Tween[]) {
this.populateTimeline(tweens)
this.#subscribe = createSubscriber((update) => {
this.#timeline.eventCallback('onUpdate', update)
return () => this.#timeline.eventCallback('onUpdate', null)
})
}
populateTimeline(tweens: Tween[]) {
onMount(() => {
tweens.forEach(([element, vars]) => {
this.#timeline.to(element, vars)
})
})
}
get time() {
this.#subscribe()
return this.#timeline.time()
}
set time(v) {
this.#timeline.seek(v)
}
}
const tl = new Timeline([
['.box1', { x: 200, duration: 1 }],
['.box2', { x: 200, duration: 1 }]
])
</script>
<label>
<p>Time:</p>
<input bind:value={tl.time} type="range" min={0} max={2} step={0.01} />
</label>
<div class="box box1"></div>
<div class="box box2"></div>
<style>
.box {
aspect-ratio: 1;
width: 100px;
background-color: orangered;
border-radius: 1rem;
}
</style>
This makes our code much simpler. 🧘
We also don’t need extra state to keep track of the time! Instead, we can just return, and set the current time for the timeline using the methods it provides and easily do a cleanup. 🧹
The createSubscriber
function uses an effect that tracks a value that increments when update
runs, and reruns subscribers while keeping track of the active effects.
Do you remember the counter example from before, when we learned how you don’t need effects, and you can do side-effects inside event handlers?
export class Counter {
#first = true
constructor(initial: number) {
this.#count = $state(initial)
}
get count() {
if (this.#first) {
const savedCount = localStorage.getItem('count')
if (savedCount) this.#count = parseInt(savedCount)
this.#first = false
}
return this.#count
}
set count(v: number) {
localStorage.setItem('count', v.toString())
this.#count = v
}
}
This can also be made simpler by using createSubscriber
.
You only have to listen for the storage
event on the window
and run update
when it changes to notify subscribers, so you don’t even need to use state:
import { createSubscriber } from 'svelte/reactivity'
import { on } from 'svelte/events'
class Counter {
#subscribe
constructor(initial: number) {
this.#subscribe = createSubscriber((update) => {
if (!localStorage.getItem('count')) {
localStorage.setItem('count', initial.toString())
}
const off = on(window, 'storage', update)
return () => off()
})
}
get count() {
this.#subscribe()
return parseInt(localStorage.getItem('count') ?? '0')
}
set count(v: number) {
localStorage.setItem('count', v.toString())
}
}
In this example, we also use the on
event from Svelte rather than addEventListener
, because it returns a cleanup function that removes the handler for convenience.
Special Elements
Svelte has special elements you can use at the top-level of your component like <svelte:window>
to add event listeners on the window
without having to do the cleanup yourself, <svelte:head>
to add things to the <head>
element for things like SEO, or <svelte:element>
to dynamically render elements and more.
This is how it would look like if you had to add and take care of event listeners on the window
object, <document>
, or <body>
element yourself:
<script lang="ts">
import { onMount } from 'svelte'
let scrollY = $state(0)
function handleScroll() {
scrollY = window.scrollY
}
onMount(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
})
</script>
<div>{scrollY}px</div>
<style>
:global(body) {
height: 8000px;
}
div {
position: fixed;
top: 50%;
left: 50%;
translate: -50% -50%;
font-size: 8vw;
font-weight: 700;
}
</style>
Thankfully, Svelte makes this easy with special elements like <svelte:window>
and it does the cleanup for you:
<script lang="ts">
let scrollY = $state(0)
function handleScroll() {
scrollY = window.scrollY
}
</script>
<svelte:window onscroll={handleScroll} />
<div>{scrollY}px</div>
There are also bindings for properties like the scroll position:
<script lang="ts">
let scrollY = $state(0)
</script>
<svelte:window bind:scrollY />
Svelte also exports reactive window
values from reactivity/window
so you don’t even have to use a special element and bind the property to a value:
<script lang="ts">
import { scrollY } from 'svelte/reactivity/window'
</script>
<div>{scrollY.current}px</div>
Legacy Svelte
Svelte 5 was a large shift from previous versions of Svelte that introduced a new system of reactivity with runes, and snippets replacing slots. You’re going to run into legacy Svelte code at some point, so it’s worth reading about the legacy APIs in the Svelte documentation.
Keep in mind that Svelte components are by default in legacy mode for backwards compatibility. If you use runes in your component, it’s going to be in runes mode.
This is worth noting because you might run into unexpected behavior when you’re using legacy components. If you use the Svelte for VS Code extension, it’s going to show the mode in the top left corner of the editor.
You can always make sure that you’re in runes mode by changing the Svelte compiler options in svelte.config.js
for the entire project, or per component:
<svelte:options runes={true} />
Using Svelte With AI
I live in the stone age when it comes to AI and use free tools like Supermaven for code completion and Perplexity as my search engine, so I don’t use paid AI coding editors.
Newer AI models seem to be getting better at supporting the latest Svelte syntax, but it’s still not perfect and it’s often going to hallucinate features that don’t exist with overwhelming confidence.
If you’re using AI and want the latest Svelte syntax suggestions, Svelte has LLM friendly documentation you can feed to an AI context window for more accurate suggestions.