Última atividade 3 months ago

Aunali revisou este gist 3 months ago. Ir para a revisão

1 file changed, 4634 insertions

svelte5-complete-guide.md(arquivo criado)

@@ -0,0 +1,4634 @@
1 + ---
2 + title: The Complete Svelte 5 Guide
3 + description: The ultimate guide for the most beloved JavaScript framework.
4 + slug: learn-svelte
5 + published: '2025-8-14'
6 + category: svelte
7 + ---
8 +
9 + <script lang="ts">
10 + import Card from '$lib/components/card.svelte'
11 + import YouTube from '$lib/components/youtube.svelte'
12 + import Example from './examples/example-loader.svelte'
13 + </script>
14 +
15 + ## Table of Contents
16 +
17 + ## What is Svelte?
18 +
19 + If we look at the definition from the [Svelte](https://svelte.dev/) website, it says:
20 +
21 + > 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.
22 +
23 + 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:
24 +
25 + ```svelte:App.svelte
26 + <script lang="ts">
27 + // reactive state
28 + let count = $state(0)
29 +
30 + // reassignment updates the UI
31 + setInterval(() => count += 1, 1000)
32 + </script>
33 +
34 + <p>{count}</p>
35 + ```
36 +
37 + You might think how Svelte does some crazy compiler stuff under the hood to make this work, but the output is human readable JavaScript:
38 +
39 + ```ts:output
40 + function App($$anchor) {
41 + // create signal
42 + let count = state(0)
43 +
44 + // update signal
45 + setInterval(() => set(count, get(count) + 1), 1000)
46 +
47 + // create element
48 + var p = from_html(`<p> </p>`)
49 + var text = child(p, true)
50 +
51 + // update DOM when `count` changes
52 + template_effect(() => set_text(text, get(count)))
53 +
54 + // add to DOM
55 + append($$anchor, p)
56 + }
57 + ```
58 +
59 + Svelte's reactivity is based on [signals](https://www.youtube.com/watch?v=1TSLEzNzGQM), so there's nothing magical about it — you could write Svelte code without a compiler, but it would be tedious like writing [JSX](https://react.dev/learn/writing-markup-with-jsx) by hand using functions.
60 +
61 + 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.
62 +
63 + This is what **"does minimal work in the browser"** means!
64 +
65 + Svelte also has a more opinionated application framework called [SvelteKit](https://svelte.dev/docs/kit/introduction) (equivalent to [Next.js](https://nextjs.org/) for React) if you need routing, server-side rendering, adapters to deploy to different platforms and so on.
66 +
67 + ## Try Svelte
68 +
69 + You can try Svelte in the browser using the [Svelte Playground](https://svelte.dev/playground) and follow along without having to set up anything.
70 +
71 + <Card type="warning">
72 + Some of the examples use browser APIs like the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API" target="_blank">Web Storage API</a> that won't work in the Svelte Playground, but you can use an online IDE like <a href="https://www.sveltelab.dev/" target="_blank">SvelteLab</a>.
73 + </Card>
74 +
75 + 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](https://nodejs.org/) for that.
76 +
77 + I also recommend using the [Svelte for VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) for syntax highlighting and code completion, or a similar extension for your editor.
78 +
79 + ## TypeScript Aside
80 +
81 + [TypeScript](https://www.typescriptlang.org/) 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.
82 +
83 + If you're unfamiliar with TypeScript, code after `:` usually represents a type. You can omit the types and your code will work:
84 +
85 + ```ts:example
86 + // TypeScript 👍️
87 + let items: string[] = [...]
88 +
89 + // JavaScript 👍️
90 + let items = [...]
91 + ```
92 +
93 + Some developers prefer writing JavaScript with [JSDoc](https://jsdoc.app/) comments because it gives you the same benefits of TypeScript at the cost of a more verbose syntax:
94 +
95 + ```ts:example
96 + /**
97 + * This is a list of items.
98 + * @type {string[]}
99 + */
100 + let items = [...]
101 + ```
102 +
103 + That is completely up to you!
104 +
105 + ## Single File Components
106 +
107 + In Svelte, files ending with `.svelte` are called **single file components** because they contain the JavaScript, HTML, and CSS in a single file.
108 +
109 + Here's an example of a Svelte component:
110 +
111 + ```svelte:App.svelte
112 + <!-- logic -->
113 + <script lang="ts">
114 + let title = 'Svelte'
115 + </script>
116 +
117 + <!-- markup -->
118 + <h1>{title}</h1>
119 +
120 + <!-- styles -->
121 + <style>
122 + h1 {
123 + color: orangered;
124 + }
125 + </style>
126 + ```
127 +
128 + <Example name="single-file-component" />
129 +
130 + 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**.
131 +
132 + There's also a special `<script module>` block used for sharing code across component instances we'll learn about later.
133 +
134 + ## Component Logic
135 +
136 + Your component logic goes inside the `<script>` tag. Since Svelte 5, TypeScript is [natively supported](https://svelte.dev/docs/kit/integrations):
137 +
138 + ```svelte:App.svelte
139 + <script lang="ts">
140 + let title = 'Svelte'
141 + </script>
142 +
143 + <h1>{title as string}</h1>
144 + ```
145 +
146 + Later we're going to learn how you can even define values inside your markup, which can be helpful in some cases.
147 +
148 + ## Markup Poetry
149 +
150 + In Svelte, anything that's outside the `<script>` and `<style>` blocks is considered markup:
151 +
152 + ```svelte:App.svelte
153 + <!-- markup -->
154 + <h1>Svelte</h1>
155 + ```
156 +
157 + You can use JavaScript expressions in the template using curly braces and Svelte is going to evalute it:
158 +
159 + ```svelte:App.svelte
160 + <script lang="ts">
161 + let banana = 1
162 + </script>
163 +
164 + <p>There's {banana} {banana === 1 ? 'banana' : 'bananas'} left</p>
165 + ```
166 +
167 + <Example name="expressions" />
168 +
169 + Later we're going to learn about logic blocks like `{#if ...}` and `{#each ...}` to conditionally render content.
170 +
171 + Tags with lowercase names are treated like regular HTML elements by Svelte and accept normal attributes:
172 +
173 + ```svelte:App.svelte
174 + <img src="image.gif" alt="Person dancing" />
175 + ```
176 +
177 + You can pass values to attributes using curly braces:
178 +
179 + ```svelte:App.svelte
180 + <script lang="ts">
181 + let src = 'image.gif'
182 + let alt = 'Person dancing'
183 + </script>
184 +
185 + <img src={src} alt={alt} />
186 + ```
187 +
188 + <Example name="attributes" />
189 +
190 + If the attribute name and value are the same, you can use a shorthand attribute:
191 +
192 + ```svelte:App.svelte
193 + <!-- 👍️ longhand -->
194 + <img src={src} alt={alt} />
195 +
196 + <!-- 👍️ shorthand -->
197 + <img {src} {alt} />
198 + ```
199 +
200 + Attributes can have expressions inside the curly braces:
201 +
202 + ```svelte:App.svelte
203 + <script lang="ts">
204 + let src = 'image.gif'
205 + let alt = 'Person dancing'
206 + let lazy = true
207 + </script>
208 +
209 + <img {src} {alt} loading={lazy ? 'lazy' : 'eager'} />
210 + ```
211 +
212 + You can spread objects on elements:
213 +
214 + ```svelte:App.svelte
215 + <script lang="ts">
216 + let obj = {
217 + src: 'image.gif',
218 + alt: 'Person dancing'
219 + }
220 + </script>
221 +
222 + <img {...obj} />
223 + ```
224 +
225 + To conditionally render attributes, use `null` or `undefined` instead of `&&` for short-circuit evaluation and empty strings:
226 +
227 + ```svelte:App.svelte
228 + <script lang="ts">
229 + let src = 'image.gif'
230 + let alt = 'Person dancing'
231 + let lazy = false
232 + </script>
233 +
234 + <!-- ⛔️ loading is `false` -->
235 + <img {src} {alt} loading={lazy && 'lazy'} />
236 +
237 + <!-- ⛔️ orphan attribute -->
238 + <img {src} {alt} loading={lazy ? 'lazy' : ''} />
239 +
240 + <!-- 👍 using `null` -->
241 + <img {src} {alt} loading={lazy ? 'lazy' : null} />
242 +
243 + <!-- 👍 using `undefined` -->
244 + <img {src} {alt} loading={lazy ? 'lazy' : undefined} />
245 + ```
246 +
247 + ## Component Styles
248 +
249 + There are many ways you can style a Svelte component. 💅 I've heard people love inline styles with [Tailwind CSS](https://tailwindcss.com/), so you could just use the `style` tag...I'm joking! 😄
250 +
251 + 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:
252 +
253 + ```svelte:App.svelte
254 + <script lang="ts">
255 + let color = 'orangered'
256 + </script>
257 +
258 + <!-- 👍️ attribute -->
259 + <h1 style="color: {color}">Svelte</h1>
260 +
261 + <!-- 👍️ directive -->
262 + <h1 style:color>Svelte</h1>
263 +
264 + <!-- ⛔️ object -->
265 + <h1 style={{ color }}>Svelte</h1>
266 + ```
267 +
268 + You can even add `important` like `style:color|important` to override styles. The `style:` directive is also great for CSS custom properties:
269 +
270 + ```svelte:App.svelte
271 + <script lang="ts">
272 + let color = 'orangered'
273 + </script>
274 +
275 + <!-- 👍️ custom CSS property -->
276 + <h1 style="--color: {color}">Svelte</h1>
277 +
278 + <!-- 👍️ shorthand -->
279 + <h1 style:--color={color}>Svelte</h1>
280 +
281 + <style>
282 + h1 {
283 + /* custom CSS property with a default value */
284 + color: var(--color, #fff);
285 + }
286 + </style>
287 + ```
288 +
289 + ### Scoped Styles
290 +
291 + 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:
292 +
293 + ```svelte:App.svelte
294 + <h1>Svelte</h1>
295 +
296 + <!-- these styles only apply to this component -->
297 + <style>
298 + h1 {
299 + color: orangered;
300 + }
301 + </style>
302 + ```
303 +
304 + 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:
305 +
306 + ```css:output
307 + /* uniquely generated class name */
308 + h1.svelte-ep2x9j {
309 + color: orangered;
310 + }
311 + ```
312 +
313 + If you want to define global styles for your app, you can import a CSS stylesheet at the root of your app:
314 +
315 + ```ts:main.ts {4}
316 + // inside a Vite project
317 + import { mount } from 'svelte'
318 + import App from './App.svelte'
319 + import './app.css'
320 +
321 + const app = mount(App, {
322 + target: document.getElementById('app')!
323 + })
324 +
325 + export default app
326 + ```
327 +
328 + 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.
329 +
330 + Svelte has to "see" the styles in the component, so it doesn't know they exist and warns you about removing unusued styles:
331 +
332 + ```svelte:App.svelte {15-17,20-22}
333 + <script lang="ts">
334 + let content = `
335 + <h1>Big banana exposed</h1>
336 + <p>The gorillas inside the banana cartel speak out</p>
337 + `
338 + </script>
339 +
340 + <article>
341 + {@html content}
342 + </article>
343 +
344 + <style>
345 + article {
346 + /* ⚠️ Unused CSS selector "h1" */
347 + h1 {
348 + text-transform: capitalize;
349 + }
350 +
351 + /* ⚠️ Unused CSS selector "p" */
352 + p {
353 + text-wrap: pretty;
354 + }
355 + }
356 + </style>
357 + ```
358 +
359 + <Card type="danger">
360 + The <code>@html</code> tag is used to render raw HTML in Svelte components. If you don't control the content, always sanitize user input to prevent <a href="https://owasp.org/www-community/attacks/xss/" target="_blank">XSS attacks</a>.
361 + </Card>
362 +
363 + In that case, you can make the styles global by using the `:global(selector)` modifier:
364 +
365 + ```svelte:App.svelte {4-6,8-10}
366 + <!-- ... -->
367 + <style>
368 + article {
369 + :global(h1) {
370 + text-transform: capitalize;
371 + }
372 +
373 + :global(p) {
374 + text-wrap: pretty;
375 + }
376 + }
377 + </style>
378 + ```
379 +
380 + Having to use `:global` on every selector is tedious! Thankfully, you can nest global styles inside a `:global { ... }` block:
381 +
382 + ```svelte:App.svelte {3}
383 + <!-- ... -->
384 + <style>
385 + :global {
386 + article {
387 + h1 {
388 + text-transform: capitalize;
389 + }
390 +
391 + p {
392 + text-wrap: pretty;
393 + }
394 + }
395 + }
396 + </style>
397 + ```
398 +
399 + You can also have "global scoped styles" where the styles inside the `:global` block are scoped to the class:
400 +
401 + ```svelte:App.svelte {3}
402 + <!-- ... -->
403 + <style>
404 + article :global {
405 + h1 {
406 + text-transform: capitalize;
407 + }
408 +
409 + p {
410 + text-wrap: pretty;
411 + }
412 + }
413 + </style>
414 + ```
415 +
416 + Here's the compiled CSS output:
417 +
418 + ```css:output
419 + article.svelte-ju1yn8 {
420 + h1 {
421 + text-transform: capitalize;
422 + }
423 +
424 + p {
425 + text-wrap: pretty;
426 + }
427 + }
428 + ```
429 +
430 + 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:
431 +
432 + ```svelte:App.svelte
433 + <style>
434 + @keyframes -global-animation {
435 + /* ... */
436 + }
437 + </style>
438 + ```
439 +
440 + You can use different [preprocessors](https://svelte.dev/docs/kit/integrations#vitePreprocess) like [PostCSS](https://postcss.org/) or [SCSS](https://sass-lang.com/) by simply adding the `lang` attribute to the `<style>` tag with the preprocessor you want to use:
441 +
442 + ```svelte:example
443 + <style lang="postcss">
444 + <!-- ... -->
445 + </style>
446 +
447 + <style lang="scss">
448 + <!-- ... -->
449 + </style>
450 + ```
451 +
452 + These days you probably don't need SCSS anymore, since a lot of features such as nesting and CSS variables are supported by CSS.
453 +
454 + ### Dynamic Classes
455 +
456 + You can use an expression to apply a dynamic class, but it's tedious and easy to make mistakes:
457 +
458 + ```svelte:App.svelte {2,7,15-17}
459 + <script lang="ts">
460 + let open = $state(false)
461 + </script>
462 +
463 + <button onclick={() => open = !open}>
464 + <span>Accordion</span>
465 + <span class="trigger {open ? 'open' : ''}">👈️</span>
466 + </button>
467 +
468 + <style>
469 + .trigger {
470 + display: inline-block;
471 + transition: rotate 0.2s ease;
472 +
473 + &.open {
474 + rotate: -90deg;
475 + }
476 + }
477 + </style>
478 + ```
479 +
480 + <Example name='dynamic-classes' />
481 +
482 + Thankfully, Svelte can helps us out here. You can use the `class:` directive to conditionally apply a class:
483 +
484 + ```svelte:App.svelte
485 + <span class="trigger" class:open>👈️</span>
486 + ```
487 +
488 + You can also pass an object, array, or both to the `class` attribute and Svelte is going to use [clsx](https://github.com/lukeed/clsx) under the hood to merge the classes:
489 +
490 + ```svelte:App.svelte
491 + <!-- 👍️ passing an object -->
492 + <span class={{ trigger: true, open }}>👈️</span>
493 +
494 + <!-- 👍️ passing an array -->
495 + <span class={['trigger', open && 'open']}>👈️</span>
496 +
497 + <!-- 👍️ passing an array and object -->
498 + <span class={['trigger', { open }]}>👈️</span>
499 + ```
500 +
501 + If you're using Tailwind, this is very useful when you need to apply a bunch of classes:
502 +
503 + ```svelte:App.svelte
504 + <span class={['transition-transform', { '-rotate-90': open }]}>👈️</span>
505 + ```
506 +
507 + Also consider using [data attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/Use_data_attributes) for explicit transition states for easier orchestration, instead of using a bunch of classes:
508 +
509 + ```svelte:App.svelte {2,5,12-14,16-18}
510 + <script lang="ts">
511 + let status = $state('closed')
512 + </script>
513 +
514 + <span class="trigger" data-status={status}>👈️</span>
515 +
516 + <style>
517 + .trigger {
518 + display: inline-block;
519 + transition: rotate 0.2s ease;
520 +
521 + &[data-status="closed"] {
522 + rotate: 0deg;
523 + }
524 +
525 + &[data-status="open"] {
526 + rotate: -90deg;
527 + }
528 + }
529 + </style>
530 + ```
531 +
532 + ## Svelte Reactivity
533 +
534 + 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.
535 +
536 + Let's look at a counter example:
537 +
538 + ```svelte:App.svelte
539 + <!-- only required for this example because of legacy mode -->
540 + <svelte:options runes={true} />
541 +
542 + <script lang="ts">
543 + let count = 0
544 + </script>
545 +
546 + <button onclick={() => count += 1}>
547 + {count}
548 + </button>
549 + ```
550 +
551 + The Svelte compiler knows that you're trying to update the `count` value and warns you because it's not reactive:
552 +
553 + > `count` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates.
554 +
555 + This brings us to our first Svelte rune — the `$state` rune.
556 +
557 + ## Reactive State
558 +
559 + 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:
560 +
561 + ```svelte:App.svelte {3,7}
562 + <script lang="ts">
563 + // reactive value
564 + let count = $state(0)
565 + </script>
566 +
567 + <!-- reactive assignment -->
568 + <button onclick={() => count += 1}>
569 + Count: {count}
570 + </button>
571 + ```
572 +
573 + <Example name="counter" />
574 +
575 + You can open the developer tools and see that Svelte only updated the part of the DOM that changed.
576 +
577 + <Card type="info">
578 + The example uses <code>count += 1</code> to emphasize assignment, but using <code>count++</code> to increment the value also works.
579 + </Card>
580 +
581 + The `$state(...)` syntax is called a **rune** and is part of the language. It looks like a function, but it's only a hint for the Svelte compiler to know what to do with it. This also means as far as TypeScript is concerned, it's just a function:
582 +
583 + ```ts:example
584 + let value = $state<Type>(...)
585 + ```
586 +
587 + The three main runes we're going to learn about are the `$state`, `$derived`, and `$effect` rune.
588 +
589 + ## Deeply Reactive State
590 +
591 + If you pass an array, or object to `$state` it becomes a deeply reactive [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). This lets Svelte perform granular updates when you read or write properties and avoids mutating the state directly.
592 +
593 + For example, changing `editor.content` is going to update the UI in every place where it's used:
594 +
595 + ```svelte:App.svelte {2-5,10,14}
596 + <script lang="ts">
597 + let editor = $state({
598 + theme: 'dark',
599 + content: '<h1>Svelte</h1>'
600 + })
601 + </script>
602 +
603 + <textarea
604 + value={editor.content}
605 + oninput={(e) => editor.content = (e.target as HTMLTextAreaElement).value}
606 + spellcheck="false"
607 + ></textarea>
608 +
609 + {@html editor.content}
610 +
611 + <style>
612 + textarea {
613 + width: 600px;
614 + height: 200px;
615 + padding: 1rem;
616 + border-radius: 0.5rem;
617 + }
618 + </style>
619 + ```
620 +
621 + <Example name="editor" />
622 +
623 + ### $state.raw
624 +
625 + 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:
626 +
627 + ```svelte:App.svelte {3-6,13,16-19}
628 + <script lang="ts">
629 + // this could be a complex data structure
630 + let editor = $state.raw({
631 + theme: 'dark',
632 + content: '<h1>Svelte</h1>'
633 + })
634 + </script>
635 +
636 + <textarea
637 + value={editor.content}
638 + oninput={(e) => {
639 + // ⛔️ can't be mutated
640 + editor.content = e.target.value
641 +
642 + // 👍️ reassignment
643 + editor = {
644 + ...editor,
645 + content: e.target.value
646 + }
647 + }}
648 + spellcheck="false"
649 + ></textarea>
650 +
651 + {@html editor.content}
652 +
653 + <style>
654 + textarea {
655 + width: 100%;
656 + height: 200px;
657 + }
658 + </style>
659 + ```
660 +
661 + ### $state.snapshot
662 +
663 + 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:
664 +
665 + ```ts:editor.ts
666 + function saveEditorState(editor) {
667 + // 💣️ oops! it doesn't like a Proxy object...
668 + const editorState = structuredClone(editor)
669 + // 👍️ normal object
670 + const editorState = structuredClone($state.snapshot(editor))
671 + }
672 +
673 + // later
674 + saveEditorState(editor)
675 + ```
676 +
677 + <Card type="info">
678 + Svelte uses <code>$state.snapshot</code> when you <code>console.log</code> deeply reactive values for convenience.
679 + </Card>
680 +
681 + ### Destructuring
682 +
683 + 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:
684 +
685 + ```svelte:App.svelte
686 + <script lang="ts">
687 + // 👍️ reactive
688 + let { theme, content } = $state({
689 + theme: 'dark',
690 + content: '<h1>Svelte</h1>'
691 + })
692 +
693 + // ⛔️ not reactive
694 + let { theme, content } = editor
695 + </script>
696 +
697 + {@html content}
698 + ```
699 +
700 + If you want to do this, you can use derived state!
701 +
702 + ## Derived State
703 +
704 + You can derive state from other state using the `$derived` rune and it's going to reactively update:
705 +
706 + ```svelte:App.svelte {4}
707 + <script lang="ts">
708 + let count = $state(0)
709 + let factor = $state(2)
710 + let result = $derived(count * factor)
711 + </script>
712 +
713 + <p>{count} * {factor} = {result}</p>
714 +
715 + <button onclick={() => count++}>Count: {count}</button>
716 + <button onclick={() => factor++}>Factor: {factor}</button>
717 + ```
718 +
719 + <Example name="derived" />
720 +
721 + ### Deriveds Only Update When They Change
722 +
723 + 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.
724 +
725 + Here even if `max` depends on `count`, it only updates when `max` updates:
726 +
727 + ```svelte:App.svelte {3,6,9}
728 + <script lang="ts">
729 + let count = $state(0)
730 + let max = $derived(count >= 4)
731 +
732 + // only logs when `max` changes
733 + $inspect(max)
734 + </script>
735 +
736 + <button onclick={() => count++} disabled={max}>
737 + {count}
738 + </button>
739 + ```
740 +
741 + <Card type="info">
742 + The <code>$inspect</code> rune only runs in development and is great for seeing state updates and debugging.
743 + </Card>
744 +
745 + ### Derived Dependency Tracking
746 +
747 + You can pass a function with state to a derived without losing reactivity:
748 +
749 + ```svelte:App.svelte {3,5-7}
750 + <script lang="ts">
751 + let count = $state(0)
752 + let max = $derived(limit())
753 +
754 + function limit() {
755 + return count > 4 // 📖
756 + }
757 + </script>
758 +
759 + <button onclick={() => count++} disabled={max}>
760 + {count}
761 + </button>
762 + ```
763 +
764 + This might sound like magic, but the only magic here is the system of signals and runtime reactivity! 🪄 In a later section, we're going to learn how this exactly works.
765 +
766 + 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.
767 +
768 + Passing state as argument:
769 +
770 + ```ts:output {1}
771 + let disabled = derived(() => limit(get(count))) // 📖
772 +
773 + function limit(count) {
774 + return count > 4
775 + }
776 + ```
777 +
778 + Not passing any arguments:
779 +
780 + ```ts:output {4}
781 + let disabled = derived(limit())
782 +
783 + function limit() {
784 + return get(count) > 4 // 📖
785 + }
786 + ```
787 +
788 + The only thing that matters is that `count` is read and tracked inside of an effect, so the `limit` function reruns when it updates.
789 +
790 + ### $derived.by
791 +
792 + The `$derived` rune only accepts an expression by default, but you can use the `$derived.by` rune for more complex derivations:
793 +
794 + ```svelte:App.svelte {6-12}
795 + <script lang="ts">
796 + let cart = $state([
797 + { item: '🍎', total: 10 },
798 + { item: '🍌', total: 10 }
799 + ])
800 + let total = $derived.by(() => {
801 + let sum = 0
802 + for (let item of cart) {
803 + sum += item.total
804 + }
805 + return sum
806 + })
807 + </script>
808 +
809 + <p>Total: {total}</p>
810 + ```
811 +
812 + <Example name="derived-by" />
813 +
814 + Svelte recommends you keep deriveds free of side-effects and you can't update state inside of deriveds to protect you from unintended side-effects:
815 +
816 + ```svelte:App.svelte {5}
817 + <script lang="ts">
818 + let count = $state(0)
819 + let double = $derived.by(() => {
820 + // ⛔️ error
821 + count++
822 + })
823 + </script>
824 + ```
825 +
826 + ### Destructuring From Deriveds
827 +
828 + Going back to the previous example, you can also use derived state to keep reactivity when using destructuring:
829 +
830 + ```svelte:App.svelte {8,11}
831 + <script lang="ts">
832 + let editor = $state({
833 + theme: 'dark',
834 + content: '<h1>Svelte</h1>'
835 + })
836 +
837 + // ⛔️ not reactive
838 + let { theme, content } = editor
839 +
840 + // 👍️ reactive
841 + let { theme, content } = $derived(editor)
842 + </script>
843 +
844 + {@html content}
845 + ```
846 +
847 + ## Effects
848 +
849 + The last rune in the holy trinity of reactivity in Svelte you should know about is the `$effect` rune.
850 +
851 + 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.
852 +
853 + Here `count` is going to be logged when it updates, since it's read inside of the effect and tracked as a dependency:
854 +
855 + ```svelte:App.svelte {2,6}
856 + <script lang="ts">
857 + let count = $state(0)
858 +
859 + $effect(() => {
860 + // 🕵️ tracked
861 + console.log(count)
862 + })
863 + </script>
864 +
865 + <button onclick={() => count++}>Click</button>
866 + ```
867 +
868 + Values are only tracked if they're **read** — here if `condition` is `true`, then `condition` and `count` are going to be tracked, but if `condition` is false, then the effect only reruns when `condition` changes:
869 +
870 + ```svelte:App.svelte {3,7,9}
871 + <script lang="ts">
872 + let count = $state(0)
873 + let condition = $state(false)
874 +
875 + $effect(() => {
876 + // 👍️ tracked
877 + if (condition) {
878 + // ⛔️ not tracked
879 + console.log(count)
880 + }
881 + })
882 + </script>
883 +
884 + <button onclick={() => count++}>Click</button>
885 + <button onclick={() => condition = !condition}>Toggle</button>
886 + ```
887 +
888 + <Card type="info">
889 + Use the <a href="https://svelte.dev/docs/svelte/$inspect" target="_blank">$inspect</a> rune instead of effects to log when a reactive value updates.
890 + </Card>
891 +
892 + 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.
893 +
894 + Values that are read **asynchronously** like inside promises and timers are **not tracked** inside effects:
895 +
896 + ```svelte:App.svelte {9}
897 + <script lang="ts">
898 + let count = $state(0)
899 + let delay = $state(1000)
900 +
901 + $effect(() => {
902 + // 🕵️ only `delay` is tracked
903 + const interval = setInterval(() => count++, delay)
904 + // 🧹 clear interval every update
905 + return () => clearInterval(interval)
906 + })
907 + </script>
908 +
909 + <button onclick={() => delay *= 2}>slower</button>
910 + <span>{count}</span>
911 + <button onclick={() => delay /= 2}>faster</button>
912 + ```
913 +
914 + Svelte provides an `untrack` function if you don't want state to be tracked:
915 +
916 + ```svelte:App.svelte {2,9}
917 + <script lang="ts">
918 + import { untrack } from 'svelte'
919 +
920 + let a = $state(0)
921 + let b = $state(0)
922 +
923 + $effect(() => {
924 + // ⚠️ only logs when `b` changes
925 + console.log(untrack(() => a) + b)
926 + })
927 + </script>
928 +
929 + <button onclick={() => a++}>A</button>
930 + <button onclick={() => b++}>B</button>
931 + ```
932 +
933 + ### Effect Dependency Tracking
934 +
935 + When it comes to deeply reactive state, effects only rerun when the object it reads changes and not its properties:
936 +
937 + ```svelte:App.svelte
938 + <script lang="ts">
939 + let obj = $state({ current: 0 })
940 + let arr = $state([])
941 +
942 + $effect(() => {
943 + // ⛔️ not tracked
944 + console.log(obj)
945 + // 👍️ tracked
946 + console.log(obj.current)
947 + })
948 +
949 + $effect(() => {
950 + // ⛔️ not tracked
951 + console.log(arr)
952 + // 👍️ tracked
953 + console.log(arr.length)
954 + })
955 +
956 + // later
957 + obj.current++
958 + arr.push(1)
959 + </script>
960 + ```
961 +
962 + There are exceptions to the rule! If you use `JSON.stringify` or `$state.snapshot`, then everything is tracked:
963 +
964 + ```svelte:App.svelte {6,10}
965 + <script lang="ts">
966 + let obj = $state({ current: 0 })
967 + let arr = $state([])
968 +
969 + $effect(() => {
970 + JSON.stringify(obj) // 👍️ tracked
971 + })
972 +
973 + $effect(() => {
974 + JSON.stringify(arr) // 👍️ tracked
975 + })
976 + </script>
977 + ```
978 +
979 + ### When Not To Use Effects
980 +
981 + In general, you should **always** avoid effects and **never use effects to synchronize state**, because Svelte queues your effects to ensure they run in the correct order and runs them last.
982 +
983 + Using effects to synchronize state can cause unexpected behavior like state being out of sync:
984 +
985 + ```svelte:App.svelte {7,12-13}
986 + <script lang="ts">
987 + let count = $state(0)
988 + let double = $state(0)
989 +
990 + $effect(() => {
991 + // effects run last
992 + double = count * 2
993 + })
994 + </script>
995 +
996 + <button onclick={() => {
997 + count++ // 1
998 + console.log(double) // ⚠️ 0
999 + }}>
1000 + {double}
1001 + </button>
1002 + ```
1003 +
1004 + **Always derive state** when you can instead:
1005 +
1006 + ```svelte:App.svelte {3,7-8}
1007 + <script lang="ts">
1008 + let count = $state(0)
1009 + let double = $derived(count * 2)
1010 + </script>
1011 +
1012 + <button onclick={() => {
1013 + count++ // 1
1014 + console.log(double) // 👍️ 2
1015 + }}>
1016 + {double}
1017 + </button>
1018 + ```
1019 +
1020 + <Card type="info">
1021 + Deriveds are effects under the hood, but they rerun immediately when their dependencies change.
1022 + </Card>
1023 +
1024 + If you want to do something when the component is added to the DOM and don't care about tracking state, you can use the `onMount` lifecycle function instead of an effect:
1025 +
1026 + ```svelte:App.svelte
1027 + <script lang="ts">
1028 + import { onMount } from 'svelte'
1029 +
1030 + onMount(() => {
1031 + console.log('Component added 👋')
1032 + return () => console.log('🧹 Component removed')
1033 + })
1034 + </script>
1035 + ```
1036 +
1037 + <Card type="warning">
1038 + Avoid passing <code>async</code> callbacks to <code>onMount</code> and <code>$effect</code> as their cleanup won't run. You can use async functions, or an <a href="https://developer.mozilla.org/en-US/docs/Glossary/IIFE" target="_blank">IIFE</a> instead.
1039 + </Card>
1040 +
1041 + ### When To Use Effects
1042 +
1043 + **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.
1044 +
1045 + In this example, we're using the Pokemon API and `getAbortSignal` from Svelte to avoid making a bunch of requests when searching for a Pokemon:
1046 +
1047 + ```svelte:App.svelte {17-21}
1048 + <script lang="ts">
1049 + import { getAbortSignal } from 'svelte'
1050 +
1051 + let pokemon = $state('charizard')
1052 + let image = $state('')
1053 +
1054 + async function getPokemon(pokemon: string) {
1055 + const baseUrl = 'https://pokeapi.co/api/v2/pokemon'
1056 + const response = await fetch(`${baseUrl}/${pokemon}`, {
1057 + // aborts when derived and effect reruns
1058 + signal: getAbortSignal()
1059 + })
1060 + if (!response.ok) throw new Error('💣️ oops!')
1061 + return response.json()
1062 + }
1063 +
1064 + $effect(() => {
1065 + getPokemon(pokemon).then(data => {
1066 + image = data.sprites.front_default
1067 + })
1068 + })
1069 + </script>
1070 +
1071 + <input
1072 + type="search"
1073 + placeholder="Enter Pokemon name"
1074 + oninput={(e) => pokemon = (e.target as HTMLInputElement).value}
1075 + />
1076 + <img src={image} alt={pokemon} />
1077 +
1078 + <style>
1079 + input {
1080 + padding: 1rem;
1081 + border-radius: 1rem;
1082 + }
1083 +
1084 + img {
1085 + width: 200px;
1086 + display: block;
1087 + margin-top: 1rem;
1088 + image-rendering: pixelated;
1089 + }
1090 + </style>
1091 + ```
1092 +
1093 + <Example name="pokemon-search" />
1094 +
1095 + ### $effect.pre
1096 +
1097 + Your effects run after the DOM updates in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide), 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.
1098 +
1099 + Let's look at an example that uses the [GSAP Flip plugin](https://gsap.com/docs/v3/Plugins/Flip/) to animate changes in the DOM and needs to measure the position, size, and rotation of elements before, and after the DOM update.
1100 +
1101 + In this example, we measure the elements before the DOM updates, and use `tick` to wait for the DOM update:
1102 +
1103 + ```svelte:App.svelte {10-20}
1104 + <script lang="ts">
1105 + import { gsap } from 'gsap'
1106 + import { Flip } from 'gsap/Flip'
1107 + import { tick } from 'svelte'
1108 +
1109 + gsap.registerPlugin(Flip)
1110 +
1111 + let items = $state([...Array(10).keys()])
1112 +
1113 + $effect.pre(() => {
1114 + // track `items` as a dependency
1115 + items
1116 + // measure elements before the DOM updates
1117 + const state = Flip.getState('.item')
1118 + // wait for the DOM update
1119 + tick().then(() => {
1120 + // do the FLIP animation
1121 + Flip.from(state, { duration: 1, stagger: 0.01, ease: 'power1.inOut' })
1122 + })
1123 + })
1124 +
1125 + function shuffle() {
1126 + items = items.toSorted(() => Math.random() - 0.5)
1127 + }
1128 + </script>
1129 +
1130 + <div class="container">
1131 + {#each items as item (item)}
1132 + <div class="item">{item}</div>
1133 + {/each}
1134 + </div>
1135 +
1136 + <button onclick={shuffle}>Shuffle</button>
1137 +
1138 + <style>
1139 + .container {
1140 + width: 600px;
1141 + display: grid;
1142 + grid-template-columns: repeat(5, 1fr);
1143 + gap: 0.5rem;
1144 + color: orangered;
1145 + font-size: 3rem;
1146 + font-weight: 700;
1147 + text-shadow: 2px 2px 0px #000;
1148 +
1149 + .item {
1150 + display: grid;
1151 + place-content: center;
1152 + aspect-ratio: 1;
1153 + background-color: #222;
1154 + border: 1px solid #333;
1155 + border-radius: 1rem;
1156 + }
1157 + }
1158 +
1159 + button {
1160 + margin-top: 1rem;
1161 + font-size: 2rem;
1162 + }
1163 + </style>
1164 + ```
1165 +
1166 + <Example name="gsap-flip" />
1167 +
1168 + <Card type="info">
1169 + The <code>tick</code> function is a lifecyle function that uses the <code>queueMicrotask</code> method to schedule a task to run in the next microtask when all the work is done, and before the DOM updates.
1170 + </Card>
1171 +
1172 + ## State In Functions And Classes
1173 +
1174 + So far, we only used runes at the top-level of the component, but you can use state, deriveds, and effects inside functions and classes.
1175 +
1176 + 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.
1177 +
1178 + Here's a `createCounter` function that holds and returns the `count` state:
1179 +
1180 + ```ts:counter.svelte.ts
1181 + export function createCounter(initial: number) {
1182 + let count = $state(initial)
1183 +
1184 + return {
1185 + get count() { return count },
1186 + set count(v) { count = v }
1187 + }
1188 + }
1189 + ```
1190 +
1191 + Here's how it's used inside of a Svelte component:
1192 +
1193 + ```svelte:App.svelte
1194 + <script lang="ts">
1195 + import { createCounter } from './counter.svelte'
1196 +
1197 + const counter = createCounter(0)
1198 + </script>
1199 +
1200 + <button onclick={() => counter.count--}>-</button>
1201 + <span>{counter.count}</span>
1202 + <button onclick={() => counter.count++}>+</button>
1203 + ```
1204 +
1205 + ### Reactive Properties
1206 +
1207 + You're probably wondering, what's the deal with the `get` and `set` methods?
1208 +
1209 + 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.
1210 +
1211 + They're just part of JavaScript, and you could use functions instead:
1212 +
1213 + ```ts:counter.svelte.ts
1214 + export function createCounter(initial: number) {
1215 + let count = $state(initial)
1216 +
1217 + return {
1218 + count() { return count },
1219 + setCount(v: number) { count = v }
1220 + }
1221 + }
1222 + ```
1223 +
1224 + You could return a tuple instead to make the API nicer and destructure the read and write functions like `let [count, setCount] = createCounter(0)`.
1225 +
1226 + As you can see, the syntax is not as nice compared to using accessors, since you have to use functions everywhere:
1227 +
1228 + ```svelte:App.svelte
1229 + <!-- using functions -->
1230 + <button onclick={() => counter.setCurrent(counter.count() + 1)}>
1231 + {counter.count()}
1232 + </button>
1233 +
1234 + <!-- using accessors -->
1235 + <button onclick={() => counter.count++}>
1236 + {counter.count}
1237 + </button>
1238 + ```
1239 +
1240 + ### Reactive Containers
1241 +
1242 + The accessor syntax looks a lot nicer! 😄 You might be wondering, can't you just return state from the function?
1243 +
1244 + ```ts:counter.svelte.ts
1245 + export function createCounter(initial: number) {
1246 + let count = $state(initial)
1247 + // ⛔️ this doesn't work
1248 + return { count }
1249 + }
1250 + ```
1251 +
1252 + The reason this doesn't work is because **state is just a regular value** and **not** some magic reactive container. If you want something like that, you could return deeply reactive proxied state:
1253 +
1254 + ```ts:counter.svelte.ts
1255 + export function createCounter(initial: number) {
1256 + let counter = $state({ count: initial })
1257 + // 👍️ proxied state
1258 + return counter
1259 + }
1260 + ```
1261 +
1262 + You can create a reactive container if you want:
1263 +
1264 + ```ts:counter.svelte.ts {2-5,9}
1265 + // reactive container utility
1266 + function reactive<T>(value: T) {
1267 + let state = $state<T>(value)
1268 + return state
1269 + }
1270 +
1271 + function createCounter(initial: number) {
1272 + // reactive container
1273 + let counter = reactive({ count: initial })
1274 + return { counter }
1275 + }
1276 + ```
1277 +
1278 + Even destructuring works, since `counter` is not just a regular value:
1279 +
1280 + ```svelte:App.svelte {4}
1281 + <script lang="ts">
1282 + import { createCounter } from './counter.svelte'
1283 +
1284 + const { counter } = createCounter(0)
1285 + </script>
1286 +
1287 + <button onclick={() => counter.count++}>
1288 + {counter.count}
1289 + </button>
1290 + ```
1291 +
1292 + That seems super useful, so why doesn't Svelte provide this utility?
1293 +
1294 + 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.
1295 +
1296 + Svelte turns any class fields declared with state into private fields with matching `get`/`set` methods, unless you declare them yourself:
1297 +
1298 + ```ts:counter.svelte.ts {4}
1299 + export class Counter {
1300 + constructor(initial: number) {
1301 + // turned into `get`/`set` methods
1302 + this.count = $state(initial)
1303 + }
1304 +
1305 + increment() {
1306 + this.count++
1307 + }
1308 +
1309 + decrement() {
1310 + this.count--
1311 + }
1312 + }
1313 + ```
1314 +
1315 + If you look at the output, you would see something like this:
1316 +
1317 + ```ts:output
1318 + class Counter {
1319 + #count
1320 + get count() { ... }
1321 + set count(v) { ... }
1322 + }
1323 + ```
1324 +
1325 + There's only one gotcha with classes and it's how `this` works.
1326 +
1327 + For example, using a method like `counter.increment` inside `onclick` doesn't work, because `this` refers to where it was called:
1328 +
1329 + ```svelte:App.svelte
1330 + <script lang="ts">
1331 + import { Counter } from './counter.svelte'
1332 +
1333 + const counter = new Counter(0)
1334 + </script>
1335 +
1336 + <button onclick={counter.decrement}>-</button>
1337 + <span>{counter.count}</span>
1338 + <button onclick={counter.increment}>+</button>
1339 + ```
1340 +
1341 + You can see it for yourself:
1342 +
1343 + ```ts:counter.svelte.ts
1344 + increment() {
1345 + console.log(this) // button
1346 + this.count++
1347 + }
1348 +
1349 + decrement() {
1350 + console.log(this) // button
1351 + this.count--
1352 + }
1353 + ```
1354 +
1355 + 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`:
1356 +
1357 + ```ts:counter.svelte.ts
1358 + increment = () =>
1359 + console.log(this) // class
1360 + this.current++
1361 + }
1362 +
1363 + decrement = () => {
1364 + console.log(this) // class
1365 + this.current--
1366 + }
1367 + ```
1368 +
1369 + 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.
1370 +
1371 + ### Passing State Into Functions And Classes
1372 +
1373 + Because state is a regular value, it loses reactivity when you pass it into a function or a class.
1374 +
1375 + In this example, we pass `count` into `Doubler` to double the value when `count` updates. However, it's not reactive since `count` is a regular value:
1376 +
1377 + ```svelte:App.svelte {2-6,9}
1378 + <script lang="ts">
1379 + class Doubler {
1380 + constructor(count: number) {
1381 + this.count = $derived(count * 2) // get(count) * 2
1382 + }
1383 + }
1384 +
1385 + let count = $state(0)
1386 + const double = new Doubler(count) // get(count)
1387 + </script>
1388 +
1389 + <button onclick={() => count++}>
1390 + {double.count}
1391 + </button>
1392 + ```
1393 +
1394 + Svelte even gives you a warning with a hint:
1395 +
1396 + > This reference only captures the initial value of `count`. Did you mean to reference it inside a closure instead?
1397 +
1398 + To get the latest `count` value, we can pass a function instead:
1399 +
1400 + ```svelte:App.svelte {3-5,9}
1401 + <script lang="ts">
1402 + class Doubler {
1403 + constructor(count: () => number) {
1404 + this.count = $derived(count() * 2) // () => get(count) * 2
1405 + }
1406 + }
1407 +
1408 + let count = $state(0)
1409 + const double = new Doubler(() => count) // () => get(count)
1410 + </script>
1411 +
1412 + <button onclick={() => count++}>
1413 + {double.count}
1414 + </button>
1415 + ```
1416 +
1417 + Also, we already have a reactive `Counter` class we can use:
1418 +
1419 + ```svelte:App.svelte {2-6,9-11,14-15}
1420 + <script lang="ts">
1421 + class Counter {
1422 + constructor(initial: number) {
1423 + this.count = $state(initial)
1424 + }
1425 + }
1426 +
1427 + class Doubler {
1428 + constructor(counter: Counter) {
1429 + this.count = $derived(counter.count * 2)
1430 + }
1431 + }
1432 +
1433 + const counter = new Counter(0)
1434 + const double = new Doubler(counter)
1435 + </script>
1436 +
1437 + <button onclick={() => counter.count++}>
1438 + {double.count}
1439 + </button>
1440 + ```
1441 +
1442 + Runes are **reactive primitives** that give you the flexibility to create your own reactivity system.
1443 +
1444 + ## Reactive Global State
1445 +
1446 + 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:
1447 +
1448 + ```ts:config.svelte.ts
1449 + interface Config {
1450 + theme: 'light' | 'dark'
1451 + }
1452 +
1453 + export const config = $state<Config>({ theme: 'dark' })
1454 +
1455 + export function toggleTheme() {
1456 + config.theme = config.theme === 'light' ? 'dark' : 'light'
1457 + }
1458 + ```
1459 +
1460 + ```svelte:App.svelte
1461 + <script>
1462 + import { config, toggleTheme } from './config.svelte'
1463 + </script>
1464 +
1465 + <button onclick={toggleTheme}>
1466 + {config.theme}
1467 + </button>
1468 + ```
1469 +
1470 + You could use a function, or a class for the config:
1471 +
1472 + ```ts:config.svelte.ts
1473 + type Themes = 'light' | 'dark'
1474 +
1475 + class Config {
1476 + theme = $state<Themes>('dark')
1477 +
1478 + toggleTheme() {
1479 + this.theme = this.theme === 'light' ? 'dark' : 'light'
1480 + }
1481 + }
1482 +
1483 + export const config = new Config()
1484 + ```
1485 +
1486 + It doesn't matter if you use functions or classes, as long as you understand how Svelte reactivity works.
1487 +
1488 + ## Why You Should Avoid Effects
1489 +
1490 + 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.
1491 +
1492 + It's unlikely your app would be worse just by using effects — the actual problem is that it's easy to overcomplicate your code with effects, because it seems like the right thing to do.
1493 +
1494 + In this example, we're using the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) to read and write the `counter` value each time it updates. Hey, that's a side-effect! Using an effect seems resonable:
1495 +
1496 + ```ts:counter.svelte.ts
1497 + class Counter {
1498 + constructor(initial: number) {
1499 + this.count = $state(initial)
1500 +
1501 + $effect(() => {
1502 + const savedCount = localStorage.getItem('count')
1503 + if (savedCount) this.count = parseInt(savedCount)
1504 + })
1505 +
1506 + $effect(() => {
1507 + localStorage.setItem('count', this.count.toString())
1508 + })
1509 + }
1510 + }
1511 + ```
1512 +
1513 + The problem only arises if you create the counter outside the component initialization phase (in a separate module, or inside of an event handler):
1514 +
1515 + ```ts:counter.svelte.ts
1516 + export const counter = new Counter(0)
1517 + ```
1518 +
1519 + Oops! Immediately, there's an `effect_orphan` error:
1520 +
1521 + > `$effect` can only be used inside an effect (e.g. during component initialisation).
1522 +
1523 + Your entire app is a root effect with other nested effects, so Svelte can run the teardown logic for them when the component is removed — in this case, you're trying to create an effect outside that root effect, so Svelte can't keep track of it.
1524 +
1525 + Svelte provides an advanced `$effect.root` rune to create your own root effect, but now you have to run the cleanup manually:
1526 +
1527 + ```ts:counter.svelte.ts {2,8-19,22-24}
1528 + class Counter {
1529 + #cleanup
1530 +
1531 + constructor(initial: number) {
1532 + this.count = $state(initial)
1533 +
1534 + // manual cleanup 😮‍💨
1535 + this.cleanup = $effect.root(() => {
1536 + $effect(() => {
1537 + const savedCount = localStorage.getItem('count')
1538 + if (savedCount) this.count = parseInt(savedCount)
1539 + })
1540 +
1541 + $effect(() => {
1542 + localStorage.setItem('count', this.count.toString())
1543 + })
1544 +
1545 + return () => console.log('🧹 cleanup')
1546 + })
1547 + }
1548 +
1549 + destroy() {
1550 + this.#cleanup()
1551 + }
1552 + }
1553 + ```
1554 +
1555 + Awkward! 😄 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 the solution?
1556 +
1557 + ```ts:counter.svelte.ts {5-14}
1558 + class Counter {
1559 + constructor(initial: number) {
1560 + this.count = $state(initial)
1561 +
1562 + if ($effect.tracking()) {
1563 + $effect(() => {
1564 + const savedCount = localStorage.getItem('count')
1565 + if (savedCount) this.count = parseInt(savedCount)
1566 + })
1567 +
1568 + $effect(() => {
1569 + localStorage.setItem('count', this.count.toString())
1570 + })
1571 + }
1572 + }
1573 + }
1574 + ```
1575 +
1576 + 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. 😩
1577 +
1578 + 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:
1579 +
1580 + ```ts:counter.svelte.ts {7-12,17}
1581 + export class Counter {
1582 + constructor(initial: number) {
1583 + this.#count = $state(initial)
1584 + }
1585 +
1586 + get count() {
1587 + if ($effect.tracking()) {
1588 + $effect(() => {
1589 + const savedCount = localStorage.getItem('count')
1590 + if (savedCount) this.#count = parseInt(savedCount)
1591 + })
1592 + }
1593 + return this.#count
1594 + }
1595 +
1596 + set count(v: number) {
1597 + localStorage.setItem('count', v.toString())
1598 + this.#count = v
1599 + }
1600 + }
1601 + ```
1602 +
1603 + There's **another** problem. Each time we read the value, we're creating an effect! 😨 That's a simple fix — we can use a variable to track if we already ran the effect:
1604 +
1605 + ```ts:counter.svelte.ts {2,11,14}
1606 + export class Counter {
1607 + #first = true
1608 +
1609 + constructor(initial: number) {
1610 + this.#count = $state(initial)
1611 + }
1612 +
1613 + get count() {
1614 + if ($effect.tracking()) {
1615 + $effect(() => {
1616 + if (!this.#first) return
1617 + const savedCount = localStorage.getItem('count')
1618 + if (savedCount) this.#count = parseInt(savedCount)
1619 + this.#first = false
1620 + })
1621 + }
1622 + return this.#count
1623 + }
1624 +
1625 + set count(v: number) {
1626 + localStorage.setItem('count', v.toString())
1627 + this.#count = v
1628 + }
1629 + }
1630 + ```
1631 +
1632 + In reality, none of this is necessary — you can make everything simpler by doing side-effects inside event handlers like `onclick` instead of using effects. In fact, we can just remove the effect and everything works:
1633 +
1634 + ```ts:counter.svelte.ts
1635 + export class Counter {
1636 + #first = true
1637 +
1638 + constructor(initial: number) {
1639 + this.#count = $state(initial)
1640 + }
1641 +
1642 + get count() {
1643 + if (this.#first) {
1644 + const savedCount = localStorage.getItem('count')
1645 + if (savedCount) this.#count = parseInt(savedCount)
1646 + this.#first = false
1647 + }
1648 + return this.#count
1649 + }
1650 +
1651 + set count(v: number) {
1652 + localStorage.setItem('count', v.toString())
1653 + this.#count = v
1654 + }
1655 + }
1656 + ```
1657 +
1658 + 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.
1659 +
1660 + ## How Svelte Reactivity Works
1661 +
1662 + I believe that understanding how something works gives you greater enjoyment in life by being more competent at what you do.
1663 +
1664 + 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](https://github.com/tc39/proposal-signals) itself.
1665 +
1666 + So far we learned that reassignments cause updates in Svelte. There's nothing special about `=` though! It just creates a function call to update the value:
1667 +
1668 + ```svelte:example {3}
1669 + <script lang="ts">
1670 + let value = $state('🍎')
1671 + value = '🍌' // set(value, '🍌')
1672 + </script>
1673 +
1674 + <!-- how does this get updated? -->
1675 + {value}
1676 + ```
1677 +
1678 + 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:
1679 +
1680 + ```ts:signals.ts
1681 + function state(value) {
1682 + const signal = { value, subscribers: new Set() }
1683 + return signal
1684 + }
1685 + ```
1686 +
1687 + **You need effects to react to signals** and effects are just functions that run when a signal updates.
1688 +
1689 + That's how Svelte updates the DOM by compiling your template into effects. This is referred to as a **tracking context**:
1690 +
1691 + ```ts:example
1692 + template_effect(() => set_text(text, get(value)))
1693 + ```
1694 +
1695 + Your entire app is a root effect with nested effects inside of it, so Svelte can keep track of your effects for cleanup. When the effect runs, it invokes the callback function and sets it as the active effect in some variable:
1696 +
1697 + ```ts:signals.ts
1698 + let activeEffect = null
1699 +
1700 + function effect(fn) {
1701 + // set active effect
1702 + activeEffect = fn
1703 + // run the effect
1704 + fn()
1705 + }
1706 + ```
1707 +
1708 + The magic happens when you read a signal inside of an effect. When `value` is read, it adds the active effect as a subscriber:
1709 +
1710 + ```ts:signals.ts
1711 + // the active effect
1712 + let activeEffect = fn
1713 +
1714 + function get(signal) {
1715 + // add effect to subscribers
1716 + signal.subscribers.add(activeEffect)
1717 + // return value
1718 + return signal.value
1719 + }
1720 + ```
1721 +
1722 + Later, when you write to `count` it notifies the subscribers and recreates the dependency graph when it reads the signal inside the effect:
1723 +
1724 + ```ts:signals.ts
1725 + function set(signal, value) {
1726 + // update signal
1727 + signal.value = value
1728 + // notify subscribers
1729 + signal.subscribers.forEach(effect => effect())
1730 + }
1731 + ```
1732 +
1733 + You can just update state and it's going to update the UI anywhere where it's used. If you're familiar with the observer pattern, signals are observables on steroids and frameworks like Svelte do a lot of work under the hood to make them performant.
1734 +
1735 + Here's a counter example using our basic signals implementation inside a regular `.html` file:
1736 +
1737 + ```html:example
1738 + <script type="module">
1739 + import { state, set, get, effect } from './signals.js'
1740 +
1741 + // create signal
1742 + const count = state(0)
1743 +
1744 + // hook into DOM elements
1745 + const btn = document.querySelector('button')
1746 + btn.onclick = () => set(count, get(count) + 1)
1747 +
1748 + // create template effect
1749 + effect(() => btn.textContent = get(count))
1750 + </script>
1751 +
1752 + <button>0</button>
1753 + ```
1754 +
1755 + This is oversimplified, but it happens every update and that's why it's called **runtime reactivity**, because it happens as your code runs!
1756 +
1757 + **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.
1758 +
1759 + Deriveds are 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:
1760 +
1761 + ```svelte:example {7,14}
1762 + <script lang="ts">
1763 + let value = $state('🍎')
1764 + let code = $derived(getCodePoint())
1765 +
1766 + function getCodePoint() {
1767 + // `value` is read inside the derived effect
1768 + return value.codePointAt(0).toString(16)
1769 + }
1770 +
1771 + value = '🍌'
1772 + </script>
1773 +
1774 + <!-- `code` is read inside the template effect -->
1775 + {code}
1776 + ```
1777 +
1778 + 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.
1779 +
1780 + Here if `emoji.code` was a regular value and not a getter, the text inside the button would never update:
1781 +
1782 + ```svelte:example {5-6,15}
1783 + <script lang="ts">
1784 + class Emoji {
1785 + constructor(emoji: string) {
1786 + // turned into `get` and `set` methods
1787 + this.current = $state(emoji)
1788 + this.code = $derived(this.current.codePointAt(0).toString(16))
1789 + }
1790 + }
1791 +
1792 + const emoji = new Emoji('🍎')
1793 + </script>
1794 +
1795 + <button onclick={() => emoji.current = '🍌'}>
1796 + <!-- template_effect(() => set_text(text, emoji.code)) -->
1797 + {emoji.code}
1798 + </button>
1799 + ```
1800 +
1801 + As the React people love to say, "it's just JavaScript!" 😄
1802 +
1803 + ## Using Template Logic
1804 +
1805 + HTML doesn't have conditionals or loops, but Svelte has control flow blocks ranging from `{#if ...}`, `{#each ...}` to data loading blocks like `{#await ...}`.
1806 +
1807 + ### Using Conditionals
1808 +
1809 + In Svelte, you can use the `{#if ...}` block to conditionally render content:
1810 +
1811 + ```svelte:App.svelte
1812 + <script lang="ts">
1813 + type Status = 'loading' | 'success' | 'error'
1814 +
1815 + let status = $state<Status>('loading')
1816 + </script>
1817 +
1818 + {#if status === 'loading'}
1819 + <p>Loading...</p>
1820 + {:else if status === 'success'}
1821 + <p>Success!</p>
1822 + {:else if status === 'error'}
1823 + <p>Error</p>
1824 + {:else}
1825 + <p>Impossible state</p>
1826 + {/if}
1827 + ```
1828 +
1829 + ### Looping Over Data
1830 +
1831 + To loop over a list of items, you use the `{#each ...}` block:
1832 +
1833 + ```svelte:App.svelte
1834 + <script lang="ts">
1835 + let todos = $state([
1836 + { id: 1, text: 'Todo 1', done: false },
1837 + { id: 2, text: 'Todo 2', done: false },
1838 + { id: 3, text: 'Todo 3', done: false },
1839 + { id: 4, text: 'Todo 4', done: false }
1840 + ])
1841 + </script>
1842 +
1843 + <ul>
1844 + {#each todos as todo}
1845 + <li>
1846 + <input checked={todo.done} type="checkbox" />
1847 + <span>{todo.text}</span>
1848 + </li>
1849 + {:else}
1850 + <p>No items</p>
1851 + {/each}
1852 + </ul>
1853 + ```
1854 +
1855 + You can [destructure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) the value you're iterating over, get the current item index, and provide a unique key so Svelte can keep track of changes:
1856 +
1857 + ```svelte:App.svelte {2}
1858 + <ul>
1859 + {#each todos as { id, text, done }, i (id)}
1860 + <li>
1861 + <input checked={done} type="checkbox" />
1862 + <span style:color={i % 2 === 0 ? 'orangered' : ''}>{text}</span>
1863 + </li>
1864 + {/each}
1865 + </ul>
1866 + ```
1867 +
1868 + You can omit the `as` part inside the `{#each ...}` block when you just want to loop over an arbitrary amount of items. In this example, we're creating a basic grid:
1869 +
1870 + ```svelte:App.svelte
1871 + <div class="grid">
1872 + {#each Array(10), row}
1873 + {#each Array(10), col}
1874 + <div class="cell">{row},{col}</div>
1875 + {/each}
1876 + {/each}
1877 + </div>
1878 +
1879 + <style>
1880 + .grid {
1881 + max-width: 400px;
1882 + display: grid;
1883 + grid-template-columns: repeat(10, 1fr);
1884 + gap: 0.5rem;
1885 +
1886 + .cell {
1887 + padding: 1rem;
1888 + border: 1px solid #ccc;
1889 + }
1890 + }
1891 + </style>
1892 + ```
1893 +
1894 + You can loop over any iterable that works with `Array.from` from a `Map` and `Set` object, to generators:
1895 +
1896 + ```svelte:App.svelte
1897 + <script lang="ts">
1898 + let itemsMap = new Map([
1899 + ['🍎', 'apple'],
1900 + ['🍌', 'banana'],
1901 + ])
1902 +
1903 + let itemsSet = new Set(['🍎', '🍌'])
1904 +
1905 + function* itemsGenerator() {
1906 + yield '🍎'
1907 + yield '🍌'
1908 + }
1909 + </script>
1910 +
1911 + <ul>
1912 + {#each itemsMap as [key, value]}
1913 + <li>{key}: {value}</li>
1914 + {/each}
1915 + </ul>
1916 +
1917 + <ul>
1918 + {#each itemsSet as item}
1919 + <li>{item}</li>
1920 + {/each}
1921 + </ul>
1922 +
1923 + <ul>
1924 + {#each itemsGenerator() as item}
1925 + <li>{item}</li>
1926 + {/each}
1927 + </ul>
1928 + ```
1929 +
1930 + Svelte even has reactive versions of built-in JavaScript objects, which we're going to look at later.
1931 +
1932 + ### Asynchronous Data Loading
1933 +
1934 + Previously, we fetched some pokemon data inside of an effect, but we haven't handled the loading, error, or success state.
1935 +
1936 + Svelte has an `{#await ...}` block for dealing with promises which handles loading, error, and success states:
1937 +
1938 + ```svelte:App.svelte
1939 + <script lang="ts">
1940 + async function getPokemon(pokemon: string) {
1941 + const baseUrl = 'https://pokeapi.co/api/v2/pokemon'
1942 + const response = await fetch(`${baseUrl}/${pokemon}`)
1943 + if (!response.ok) throw new Error('💣️ oops!')
1944 + let { name, sprites } = await response.json()
1945 + return { name, image: sprites['front_default'] }
1946 + }
1947 + </script>
1948 +
1949 + {#await getPokemon('charizard')}
1950 + <p>loading...</p>
1951 + {:then pokemon}
1952 + <p>{pokemon.name}</p>
1953 + <img src={pokemon.image} alt={pokemon.name} />
1954 + {:catch error}
1955 + <p>{error.message}</p>
1956 + {/await}
1957 + ```
1958 +
1959 + You can omit the `catch` block if you don't care about errors, and the initial block if you only care about the result:
1960 +
1961 + ```svelte:App.svelte
1962 + {#await getPokemon('charizard') then pokemon}
1963 + <p>{pokemon.name}</p>
1964 + <img src={pokemon.image} alt={pokemon.name} />
1965 + {/await}
1966 + ```
1967 +
1968 + ### Asynchronous Svelte Aside
1969 +
1970 + 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](https://github.com/sveltejs/svelte/discussions/15845) in your Svelte config:
1971 +
1972 + ```ts:svelte.config.js
1973 + export default {
1974 + compilerOptions: {
1975 + experimental: {
1976 + async: true
1977 + }
1978 + }
1979 + }
1980 + ```
1981 +
1982 + At the moment you have to create a [boundary](https://svelte.dev/docs/svelte/svelte-boundary) at the root of your app, or where you want to use the `await` keyword:
1983 +
1984 + ```svelte:App.svelte
1985 + <script lang="ts">
1986 + let { children } = $props()
1987 + </script>
1988 +
1989 + <svelte:boundary>
1990 + {#snippet pending()}
1991 + <!-- only shows when the component is added -->
1992 + <p>loading...</p>
1993 + {/snippet}
1994 +
1995 + {@render children?.()}
1996 + </svelte:boundary>
1997 + ```
1998 +
1999 + Then inside of a component, you can use the `await` keyword in the script block, or template:
2000 +
2001 + ```svelte:Pokemon.svelte {5}
2002 + <script lang="ts">
2003 + // same Pokemon API as before
2004 + import { getPokemon } from './pokemon.ts'
2005 +
2006 + let pokemon = await getPokemon('charizard')
2007 + </script>
2008 +
2009 + <p>{pokemon.name}</p>
2010 + <img src={pokemon.image} alt={pokemon.name} />
2011 + ```
2012 +
2013 + You can use the `$effect.pending` rune to show a loading state:
2014 +
2015 + ```svelte:Pokemon.svelte
2016 + <!-- shows when loading new data -->
2017 + {#if $effect.pending()}
2018 + <p>loading...</p>
2019 + {:else}
2020 + <p>{(await pokemon).name}</p>
2021 + <img src={(await pokemon).image} alt={(await pokemon).name} />
2022 + {/if}
2023 + ```
2024 +
2025 + SvelteKit takes this even further with [remote functions](https://svelte.dev/docs/kit/remote-functions) where you can call remote functions like regular functions in the client, with type-safety across the server and client.
2026 +
2027 + ### Recreating Elements
2028 +
2029 + 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:
2030 +
2031 + ```svelte:App.svelte {4,7-9}
2032 + <script lang="ts">
2033 + import { fade } from 'svelte/transition'
2034 +
2035 + let value = $state(0)
2036 + </script>
2037 +
2038 + {#key value}
2039 + <div in:fade>👻</div>
2040 + {/key}
2041 +
2042 + <button onclick={() => value++}>Spook</button>
2043 + ```
2044 +
2045 + ### Local Constants
2046 +
2047 + You can use the `@const` tag to define block-scoped readonly local constants in the Svelte template.
2048 +
2049 + Local constants can only be defined as a child of blocks like `{#if ...}`, `{#else ...}`, `{#await ...}`, and `<Component />`.
2050 +
2051 + In this example, we can destructure `text` and `done` from the `todo` object while keeping the original reference:
2052 +
2053 + ```svelte:App.svelte {3}
2054 + <ul>
2055 + {#each todos as todo}
2056 + {@const { text, done: checked } = todo}
2057 + <li>
2058 + <input {checked} type="checkbox" />
2059 + <span>{text}</span>
2060 + </li>
2061 + {/each}
2062 + </ul>
2063 + ```
2064 +
2065 + In this example, we're creating a SVG grid using local constants to keep everything organized and legible:
2066 +
2067 + ```svelte:App.svelte
2068 + <script lang="ts">
2069 + let size = 800
2070 + let tiles = 8
2071 + </script>
2072 +
2073 + <svg width={size} height={size}>
2074 + {#each Array(tiles), col}
2075 + {#each Array(tiles), row}
2076 + {@const tile = size / tiles}
2077 + {@const x = col * tile}
2078 + {@const y = row * tile}
2079 + {@const width = tile}
2080 + {@const height = tile}
2081 + {@const fill = (col + row) % 2 === 0 ? 'orangered' : 'white'}
2082 + <rect {x} {y} {width} {height} {fill} />
2083 + {/each}
2084 + {/each}
2085 + </svg>
2086 + ```
2087 +
2088 + <Example name="svg-grid" />
2089 +
2090 + ## Listening To Events
2091 +
2092 + 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:
2093 +
2094 + ```svelte:App.svelte
2095 + <script lang="ts">
2096 + function onclick() {
2097 + console.log('clicked')
2098 + }
2099 + </script>
2100 +
2101 + <!-- using an inline function -->
2102 + <button onclick={() => console.log('clicked')}>Click</button>
2103 +
2104 + <!-- passing a function -->
2105 + <button onclick={onclick}>Click</button>
2106 +
2107 + <!-- using the shorthand -->
2108 + <button {onclick}>Click</button>
2109 + ```
2110 +
2111 + You can spread events, since they're just attributes:
2112 +
2113 + ```svelte:App.svelte
2114 + <script lang="ts">
2115 + const events = {
2116 + onclick: () => console.log('clicked'),
2117 + ondblclick: () => console.log('double clicked')
2118 + }
2119 + </script>
2120 +
2121 + <button {...events}>Click</button>
2122 + ```
2123 +
2124 + This example uses the `onmousemove` event to update the mouse position:
2125 +
2126 + ```svelte:App.svelte
2127 + <script lang="ts">
2128 + let mouse = $state({ x: 0, y: 0 })
2129 +
2130 + // the event is automatically passed
2131 + function onmousemove(e: MouseEvent) {
2132 + mouse.x = e.clientX
2133 + mouse.y = e.clientY
2134 + }
2135 + </script>
2136 +
2137 + <div {onmousemove}>
2138 + The mouse position is {mouse.x} x {mouse.y}
2139 + </div>
2140 +
2141 + <style>
2142 + div {
2143 + width: 100%;
2144 + height: 100%;
2145 + }
2146 + </style>
2147 + ```
2148 +
2149 + <Example name="mouse-position" />
2150 +
2151 + 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:
2152 +
2153 + ```svelte:App.svelte {3}
2154 + <script lang="ts">
2155 + function onsubmit(e: SubmitEvent) {
2156 + e.preventDefault()
2157 + const data = new FormData(this)
2158 + const email = data.get('email')
2159 + console.log(email)
2160 + }
2161 + </script>
2162 +
2163 + <form {onsubmit}>
2164 + <input type="email" name="email" />
2165 + <button type="submit">Subscribe</button>
2166 + </form>
2167 + ```
2168 +
2169 + ## Using Data Bindings
2170 +
2171 + 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.
2172 +
2173 + ### Two-Way Data Binding
2174 +
2175 + 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:
2176 +
2177 + ```svelte:App.svelte {3,4,10,11,15}
2178 + <script lang="ts">
2179 + let list = $state(['Angular', 'React', 'Solid', 'Svelte', 'Vue', 'Qwik'])
2180 + let filteredList = $derived(list.filter((item) => item.includes(search)))
2181 + let search = $state('')
2182 + </script>
2183 +
2184 + <input
2185 + type="search"
2186 + placeholder="Search"
2187 + value={search}
2188 + oninput={(e) => search = (e.target as HTMLInputElement).value}
2189 + />
2190 +
2191 + <ul>
2192 + {#each filteredList as item}
2193 + <li>{item}</li>
2194 + {:else}
2195 + <p>No results</p>
2196 + {/each}
2197 + </ul>
2198 + ```
2199 +
2200 + <Example name="input-binding" />
2201 +
2202 + Thankfully, Svelte supports two-way data binding using the `bind:` directive. If you update the value, it updates the input and vice versa:
2203 +
2204 + ```svelte:App.svelte
2205 + <input type="search" bind:value={search} ... />
2206 + ```
2207 +
2208 + One of the more useful bindings is `bind:this` to get a reference to a DOM node such as the `<canvas>` element for example:
2209 +
2210 + ```svelte:App.svelte {3,10,14}
2211 + <script lang="ts">
2212 + // this is `undefined` until the component is added
2213 + let canvas: HTMLCanvasElement
2214 +
2215 + $effect(() => {
2216 + // ⛔️ don't do this
2217 + const canvas = document.querySelector('canvas')!
2218 +
2219 + // 👍️ bind the value instead
2220 + const ctx = canvas.getContext('2d')
2221 + })
2222 + </script>
2223 +
2224 + <canvas bind:this={canvas}></canvas>
2225 + ```
2226 +
2227 + ### Function Bindings
2228 +
2229 + Another useful thing to know about are **function bindings** if you need to validate some input, or link one value to another.
2230 +
2231 + This example transforms the text the user types into the [Mocking SpongeBob](https://knowyourmeme.com/memes/mocking-spongebob) case:
2232 +
2233 + ```svelte:App.svelte
2234 + <script lang="ts">
2235 + let text = $state('I love Svelte')
2236 +
2237 + function toSpongeBobCase(text: string) {
2238 + return text
2239 + .split('')
2240 + .map((c, i) => i % 2 === 1 ? c.toUpperCase() : c.toLowerCase())
2241 + .join('')
2242 + }
2243 + </script>
2244 +
2245 + <textarea
2246 + value={toSpongeBobCase(text)}
2247 + oninput={(e) => {
2248 + text = toSpongeBobCase((e.target as HTMLInputElement).value)
2249 + }}
2250 + ></textarea>
2251 +
2252 + <style>
2253 + textarea {
2254 + width: 600px;
2255 + height: 300px;
2256 + padding: 1rem;
2257 + border-radius: 0.5rem;
2258 + }
2259 + </style>
2260 + ```
2261 +
2262 + <Example name="spongebob-case" />
2263 +
2264 + 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:
2265 +
2266 + ```svelte:App.svelte
2267 + <!-- ... -->
2268 + <textarea
2269 + bind:value={
2270 + () => toSpongeBobCase(text),
2271 + (v: string) => text = toSpongeBobCase(v)
2272 + }
2273 + ></textarea>
2274 + ```
2275 +
2276 + ### Readonly Bindings
2277 +
2278 + Svelte provides two-way bindings, and readonly bindings for different elements you can find in the [Svelte documentation for bind](https://svelte.dev/docs/svelte/bind).
2279 +
2280 + There are media bindings for `<audio>`, `<video>`, and `<img>` elements:
2281 +
2282 + ```svelte:App.svelte {3-5,9}
2283 + <script lang="ts">
2284 + let clip = 'video.mp4'
2285 + let currentTime = $state(0)
2286 + let duration = $state(0)
2287 + let paused = $state(true)
2288 + </script>
2289 +
2290 + <div class="container">
2291 + <video src={clip} bind:currentTime bind:duration bind:paused></video>
2292 +
2293 + <div class="controls">
2294 + <button onclick={() => paused = !paused}>
2295 + {paused ? '▶️' : '⏸️'}
2296 + </button>
2297 + <span>{currentTime.toFixed(1)}/{duration.toFixed(1)}</span>
2298 + <input type="range" bind:value={currentTime} max={duration} step={0.1} />
2299 + </div>
2300 + </div>
2301 +
2302 + <style>
2303 + .container {
2304 + width: 600px;
2305 +
2306 + video {
2307 + width: 100%;
2308 + border-radius: 0.5rem;
2309 + }
2310 +
2311 + .controls {
2312 + display: flex;
2313 + gap: 0.5rem;
2314 +
2315 + input[type="range"] {
2316 + flex-grow: 1;
2317 + }
2318 + }
2319 + }
2320 + </style>
2321 + ```
2322 +
2323 + <Example name="video-bindings" />
2324 +
2325 + There are also readonly bindings for visible elements that use [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) to measure any dimension changes:
2326 +
2327 + ```svelte:App.svelte {2-3,6}
2328 + <script lang="ts">
2329 + let width = $state()
2330 + let height = $state()
2331 + </script>
2332 +
2333 + <div class="container" bind:clientWidth={width} bind:clientHeight={height}>
2334 + <div class="text" contenteditable>Edit this text</div>
2335 + <div class="size">{width} x {height}</div>
2336 + </div>
2337 +
2338 + <style>
2339 + .container {
2340 + position: relative;
2341 + display: inline-block;
2342 + padding: 0.5rem;
2343 + border: 1px solid orangered;
2344 +
2345 + .text {
2346 + font-size: 2rem;
2347 + }
2348 +
2349 + .size {
2350 + position: absolute;
2351 + left: 50%;
2352 + bottom: 0px;
2353 + padding: 0.5rem;
2354 + translate: -50% 100%;
2355 + color: black;
2356 + background-color: orangered;
2357 + font-weight: 700;
2358 + white-space: pre;
2359 + }
2360 + }
2361 + </style>
2362 + ```
2363 +
2364 + <Example name="readonly-bindings" />
2365 +
2366 + 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.
2367 +
2368 + ## Svelte Components
2369 +
2370 + > Frameworks are not tools for organizing your code, they are tools for organizing your mind. — [Rich Harris](https://www.youtube.com/watch?v=AdNJ3fydeao)
2371 +
2372 + 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.
2373 +
2374 + Let's use a basic todo list app as an example:
2375 +
2376 + ```svelte:Todos.svelte
2377 + <script lang="ts">
2378 + import { slide } from 'svelte/transition'
2379 +
2380 + type Todo = { id: string; text: string; completed: boolean }
2381 + type Filter = 'all' | 'active' | 'completed'
2382 +
2383 + let todo = $state('')
2384 + let todos = $state<Todo[]>([])
2385 + let filter = $state<Filter>('all')
2386 + let filteredTodos = $derived(filterTodos())
2387 + let remaining = $derived(remainingTodos())
2388 +
2389 + function addTodo(e: SubmitEvent) {
2390 + e.preventDefault()
2391 + todos.push({
2392 + id: crypto.randomUUID(),
2393 + text: todo,
2394 + completed: false
2395 + })
2396 + todo = ''
2397 + }
2398 +
2399 + function removeTodo(todo: Todo) {
2400 + todos = todos.filter((t) => t.id !== todo.id)
2401 + }
2402 +
2403 + function filterTodos() {
2404 + return todos.filter((todo) => {
2405 + if (filter === 'all') return true
2406 + if (filter === 'active') return !todo.completed
2407 + if (filter === 'completed') return todo.completed
2408 + })
2409 + }
2410 +
2411 + function setFilter(newFilter: Filter) {
2412 + filter = newFilter
2413 + }
2414 +
2415 + function remainingTodos() {
2416 + return todos.filter((todo) => !todo.completed).length
2417 + }
2418 +
2419 + function clearCompleted() {
2420 + todos = todos.filter((todo) => !todo.completed)
2421 + }
2422 + </script>
2423 +
2424 + <form onsubmit={addTodo}>
2425 + <input type="text" placeholder="Add todo" bind:value={todo} />
2426 + </form>
2427 +
2428 + <ul>
2429 + {#each filteredTodos as todo (todo)}
2430 + <li transition:slide>
2431 + <input type="checkbox" bind:checked={todo.completed} />
2432 + <input type="text" bind:value={todo.text} />
2433 + <button onclick={() => removeTodo(todo)}>🗙</button>
2434 + </li>
2435 + {/each}
2436 + </ul>
2437 +
2438 + <div>
2439 + <p>{remaining} {remaining === 1 ? 'item' : 'items'} left</p>
2440 +
2441 + <div class="filters">
2442 + {#each ['all', 'active', 'completed'] as const as filter}
2443 + <button onclick={() => setFilter(filter)}>{filter}</button>
2444 + {/each}
2445 +
2446 + <button onclick={clearCompleted}>Clear completed</button>
2447 + </div>
2448 + </div>
2449 + ```
2450 +
2451 + <Example name="todo-list" />
2452 +
2453 + 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:
2454 +
2455 + ```console:files
2456 + todos/
2457 + ├── Todos.svelte
2458 + ├── AddTodo.svelte
2459 + ├── TodoList.svelte
2460 + ├── TodoItem.svelte
2461 + └── TodoFilter.svelte
2462 + ```
2463 +
2464 + 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.
2465 +
2466 + 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.
2467 +
2468 + To receive the props, we use the `$props` rune:
2469 +
2470 + ```svelte:AddTodo.svelte {7}
2471 + <script lang="ts">
2472 + interface Props {
2473 + todo: string
2474 + addTodo: () => void
2475 + }
2476 +
2477 + let props: Props = $props()
2478 + </script>
2479 +
2480 + <form onsubmit={props.addTodo}>
2481 + <input type="text" placeholder="Add todo" bind:value={props.todo} />
2482 + </form>
2483 + ```
2484 +
2485 + You can destructure props, rename them, set a default value, and spread the rest of the props:
2486 +
2487 + ```svelte:AddTodo.svelte {7}
2488 + <script lang="ts">
2489 + interface Props {
2490 + todo: string
2491 + addTodo: () => void
2492 + }
2493 +
2494 + let { addTodo, todo = 'Fallback', ...props }: Props = $props()
2495 + </script>
2496 +
2497 + <form onsubmit={addTodo} {...props}>
2498 + <input type="text" placeholder="Add todo" bind:value={todo} />
2499 + </form>
2500 + ```
2501 +
2502 + 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:
2503 +
2504 + ```svelte:AddTodo.svelte {7,11}
2505 + <script lang="ts">
2506 + interface Props {
2507 + todo: string
2508 + addTodo: () => void
2509 + }
2510 +
2511 + let { addTodo, todo = $bindable('Fallback') } = $props()
2512 + </script>
2513 +
2514 + <form onsubmit={addTodo}>
2515 + <input type="text" placeholder="Add todo" bind:value={todo} />
2516 + </form>
2517 + ```
2518 +
2519 + You can now safely bind the `todo` prop:
2520 +
2521 + ```svelte:Todos.svelte {4,8}
2522 + <script lang="ts">
2523 + import AddTodo from './AddTodo.svelte'
2524 +
2525 + let todo = $state('')
2526 + // ...
2527 + </script>
2528 +
2529 + <AddTodo {addTodo} bind:todo />
2530 + ```
2531 +
2532 + 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:
2533 +
2534 + ```svelte:Todos.svelte
2535 + <script lang="ts">
2536 + function addTodo(todo: string) {
2537 + todos.push({
2538 + id: crypto.randomUUID(),
2539 + text: todo,
2540 + completed: false
2541 + })
2542 + }
2543 + // ...
2544 + </script>
2545 +
2546 + <AddTodo {addTodo} />
2547 + ```
2548 +
2549 + Let's update the `<AddTodo>` component:
2550 +
2551 + ```svelte:AddTodo.svelte
2552 + <script lang="ts">
2553 + interface Props {
2554 + addTodo: (todo: string) => void
2555 + }
2556 +
2557 + let { addTodo }: Props = $props()
2558 + let todo = $state('')
2559 +
2560 + function onsubmit(e: SubmitEvent) {
2561 + e.preventDefault()
2562 + addTodo(todo)
2563 + todo = ''
2564 + }
2565 + </script>
2566 +
2567 + <form {onsubmit}>
2568 + <input type="text" placeholder="Add todo" bind:value={todo} />
2569 + </form>
2570 + ```
2571 +
2572 + 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.
2573 +
2574 + Let's create the `<TodoList>` component to render the list of todos, and use a Svelte transition to spice it up:
2575 +
2576 + ```svelte:TodoList.svelte
2577 + <script lang="ts">
2578 + import { slide } from 'svelte/transition'
2579 +
2580 + interface Props {
2581 + todos: { id: number; text: string; completed: boolean }[]
2582 + removeTodo: (id: number) => void
2583 + }
2584 +
2585 + let { todos, removeTodo }: Props = $props()
2586 + </script>
2587 +
2588 + <ul>
2589 + {#each todos as todo, i (todo)}
2590 + <li transition:slide>
2591 + <input type="checkbox" bind:checked={todo.completed} />
2592 + <input type="text" bind:value={todo.text} />
2593 + <button onclick={() => removeTodo(todo.id)}>🗙</button>
2594 + </li>
2595 + {/each}
2596 + </ul>
2597 + ```
2598 +
2599 + Let's pass the `filteredTodos` and `removeTodo` props:
2600 +
2601 + ```svelte:Todos.svelte {3,8}
2602 + <script lang="ts">
2603 + import AddTodo from './AddTodo.svelte'
2604 + import TodoList from './TodoList.svelte'
2605 + // ...
2606 + </script>
2607 +
2608 + <AddTodo {todo} {addTodo} />
2609 + <TodoList todos={filteredTodos} {removeTodo} />
2610 + ```
2611 +
2612 + Let's create the `<TodoFilter>` component to filter the todos:
2613 +
2614 + ```svelte:TodoFilter.svelte
2615 + <script lang="ts">
2616 + type Filter = 'all' | 'active' | 'completed'
2617 +
2618 + interface Props {
2619 + remaining: number
2620 + setFilter: (filter: Filter) => void
2621 + clearCompleted: () => void
2622 + }
2623 +
2624 + let { remaining, setFilter, clearCompleted }: Props = $props()
2625 + </script>
2626 +
2627 + <div>
2628 + <p>{remaining} {remaining === 1 ? 'item' : 'items'} left</p>
2629 +
2630 + <div class="filters">
2631 + {#each ['all', 'active', 'completed'] as const as filter}
2632 + <button onclick={() => setFilter(filter)}>{filter}</button>
2633 + {/each}
2634 +
2635 + <button onclick={clearCompleted}>Clear completed</button>
2636 + </div>
2637 + </div>
2638 + ```
2639 +
2640 + Let's pass the `remaining`, `setFilter`, and `clearCompleted` props:
2641 +
2642 + ```svelte:Todos.svelte {4,10}
2643 + <script lang="ts">
2644 + import AddTodo from './AddTodo.svelte'
2645 + import TodoList from './TodoList.svelte'
2646 + import TodoFilter from './TodoFilter.svelte'
2647 + // ...
2648 + </script>
2649 +
2650 + <AddTodo {todo} {addTodo} />
2651 + <TodoList todos={filteredTodos} {removeTodo} />
2652 + <TodoFilter {remaining} {setFilter} {clearCompleted} />
2653 + ```
2654 +
2655 + I left the `<TodoItem>` component for last to show the downside of abusing bindings:
2656 +
2657 + ```svelte:TodoItem.svelte
2658 + <script lang="ts">
2659 + import { slide } from 'svelte/transition'
2660 +
2661 + interface Props {
2662 + todo: { id: number; text: string; completed: boolean }
2663 + removeTodo: (id: number) => void
2664 + }
2665 +
2666 + let { todo = $bindable(), removeTodo }: Props = $props()
2667 + </script>
2668 +
2669 + <li transition:slide>
2670 + <input type="checkbox" bind:checked={todo.completed} />
2671 + <input type="text" bind:value={todo.text} />
2672 + <button onclick={() => removeTodo(todo.id)}>🗙</button>
2673 + </li>
2674 + ```
2675 +
2676 + This works, but you're going to get warnings for mutating `todos` in the parent state if you don't make `todos` bindable:
2677 +
2678 + ```svelte:Todos.svelte {9}
2679 + <script lang="ts">
2680 + import AddTodo from './AddTodo.svelte'
2681 + import TodoList from './TodoList.svelte'
2682 + import TodoFilter from './TodoFilter.svelte'
2683 + // ...
2684 + </script>
2685 +
2686 + <AddTodo {todo} {addTodo} />
2687 + <TodoList bind:todos={filteredTodos} {removeTodo} />
2688 + <TodoFilter {remaining} {setFilter} {clearCompleted} />
2689 + ```
2690 +
2691 + You have to bind each `todo` to the `todos` array:
2692 +
2693 + ```svelte:TodoItem.svelte {4,10}
2694 + <script lang="ts">
2695 + import TodoItem from './TodoItem.svelte'
2696 + // ...
2697 + let { todos = $bindable(), removeTodo }: Props = $props()
2698 + </script>
2699 +
2700 + <ul>
2701 + {#each todos as todo, i (todo)}
2702 + <li transition:slide>
2703 + <TodoItem bind:todo={todos[i]} {removeTodo} />
2704 + </li>
2705 + {/each}
2706 + </ul>
2707 + ```
2708 +
2709 + 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.
2710 +
2711 + Let's update the `<Todos>` component to use callback props:
2712 +
2713 + ```svelte:Todos.svelte
2714 + <script lang="ts">
2715 + function addTodo(e: SubmitEvent) {
2716 + e.preventDefault()
2717 + const formData = new FormData(this)
2718 + todos.push({
2719 + id: crypto.randomUUID(),
2720 + text: formData.get('todo'),
2721 + completed: false
2722 + })
2723 + this.reset()
2724 + }
2725 +
2726 + function toggleTodo(todo: Todo) {
2727 + const index = todos.findIndex((t) => t.id === todo.id)
2728 + todos[index].completed = !todos[index].completed
2729 + }
2730 +
2731 + function updateTodo(todo: Todo) {
2732 + const index = todos.findIndex((t) => t.id === todo.id)
2733 + todos[index].text = todo.text
2734 + }
2735 +
2736 + // ...
2737 + </script>
2738 +
2739 + <AddTodo {addTodo} />
2740 + <TodoList todos={filteredTodos} {toggleTodo} {updateTodo} {removeTodo} />
2741 + <TodoFilter {remaining} {setFilter} {clearCompleted} />
2742 + ```
2743 +
2744 + The last thing to do is to update the rest of the components to accept callback props:
2745 +
2746 + ```svelte:AddTodo.svelte {3,6}
2747 + <script lang="ts">
2748 + // ...
2749 + let { addTodo }: Props = $props()
2750 + </script>
2751 +
2752 + <form onsubmit={addTodo}>
2753 + <input type="text" placeholder="Add todo" name="todo" />
2754 + </form>
2755 + ```
2756 +
2757 + ```svelte:TodoList.svelte {4,9}
2758 + <script lang="ts">
2759 + import TodoItem from './TodoItem.svelte'
2760 + // ...
2761 + let { todos, toggleTodo, updateTodo, removeTodo }: Props = $props()
2762 + </script>
2763 +
2764 + <ul>
2765 + {#each todos as todo (todo)}
2766 + <TodoItem {todo} {toggleTodo} {updateTodo} {removeTodo} />
2767 + {/each}
2768 + </ul>
2769 + ```
2770 +
2771 + ```svelte:TodoItem.svelte {4,10,15,18}
2772 + <script lang="ts">
2773 + import { slide } from 'svelte/transition'
2774 + // ...
2775 + let { todo, toggleTodo, updateTodo, removeTodo }: Props = $props()
2776 + </script>
2777 +
2778 + <li transition:slide>
2779 + <input
2780 + type="checkbox"
2781 + checked={todo.completed}
2782 + onchange={() => toggleTodo(todo)}
2783 + />
2784 + <input
2785 + type="text"
2786 + value={todo.text}
2787 + oninput={() => updateTodo(todo)}
2788 + />
2789 + <button onclick={() => removeTodo(todo)}>🗙</button>
2790 + </li>
2791 + ```
2792 +
2793 + **You should avoid creating components** if you're not sure what to turn into a component. Instead, write everything inside a single component until it gets complicated, or the reusable parts become obvious.
2794 +
2795 + Later we're going to learn how to talk between components without props, using the context API.
2796 +
2797 + ## Component Composition
2798 +
2799 + 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.
2800 +
2801 + ### Component Nesting
2802 +
2803 + In HTML, you can nest elements inside other elements:
2804 +
2805 + ```html:index.html
2806 + <div class="accordion">
2807 + <div class="accordion-item">
2808 + <button>
2809 + <div>Item A</div>
2810 + <div class="accordion-trigger">👈️</div>
2811 + </button>
2812 + <div class="accordion-content">Content</div>
2813 + </div>
2814 + </div>
2815 + ```
2816 +
2817 + The fun part of using a framework like Svelte is that you get to decide how you want to compose components. To show component composition in Svelte, let's create an accordion component that can have many accordion items.
2818 +
2819 + Let's create these files inside an `accordion` folder:
2820 +
2821 + ```console:files
2822 + accordion/
2823 + ├── Accordion.svelte
2824 + ├── AccordionItem.svelte
2825 + └── index.ts
2826 + ```
2827 +
2828 + Let's export the accordion components from the `index.ts` file:
2829 +
2830 + ```ts:index.ts
2831 + export { default as Accordion } from './Accordion.svelte'
2832 + export { default as AccordionItem } from './AccordionItem.svelte'
2833 + ```
2834 +
2835 + Components accept attributes like regular HTML elements called properties, or **props\*** for short. Any content inside the component tags becomes part of the implicit `children` prop:
2836 +
2837 + ```svelte:App.svelte {6-7}
2838 + <script lang="ts">
2839 + import { Accordion, AccordionItem } from './accordion'
2840 + </script>
2841 +
2842 + <Accordion>
2843 + <!-- children -->
2844 + <p>Accordion item</p>
2845 + </Accordion>
2846 + ```
2847 +
2848 + The `children` prop is actually a snippet which you can define yourself:
2849 +
2850 + ```svelte:App.svelte {6-8}
2851 + <script lang="ts">
2852 + import { Accordion, AccordionItem } from './accordion'
2853 + </script>
2854 +
2855 + <Accordion>
2856 + {#snippet children()}
2857 + <p>Accordion item</p>
2858 + {/snippet}
2859 + </Accordion>
2860 + ```
2861 +
2862 + Snippets are great for reusing markup, or delegating rendering to a child component.
2863 +
2864 + You can get the `children` from the `$props` rune, and render them using the `@render` tag using an `{#if ...}` block with a fallback, or optional chaining:
2865 +
2866 + ```svelte:Accordion.svelte {7-11,14}
2867 + <script lang="ts">
2868 + let { children } = $props()
2869 + </script>
2870 +
2871 + <div class="accordion">
2872 + <!-- using a conditional with a fallback -->
2873 + {#if children}
2874 + {@render children()}
2875 + {:else}
2876 + <p>Fallback content</p>
2877 + {/if}
2878 +
2879 + <!-- using optional chaining -->
2880 + {@render children?.()}
2881 + </div>
2882 + ```
2883 +
2884 + The `<AccordionItem>` accepts a `title` 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:
2885 +
2886 + ```svelte:AccordionItem.svelte {10,21,27}
2887 + <script lang="ts">
2888 + import { slide } from 'svelte/transition'
2889 + import type { Snippet } from 'svelte'
2890 +
2891 + interface Props {
2892 + title: string
2893 + children: Snippet
2894 + }
2895 +
2896 + let { title, children }: Props = $props()
2897 +
2898 + let open = $state(false)
2899 +
2900 + function toggle() {
2901 + open = !open
2902 + }
2903 + </script>
2904 +
2905 + <div class="accordion-item">
2906 + <button onclick={toggle} class="accordion-heading">
2907 + <div>{title}</div>
2908 + <div class="accordion-trigger" class:open>👈️</div>
2909 + </button>
2910 +
2911 + {#if open}
2912 + <div transition:slide class="accordion-content">
2913 + {@render children?.()}
2914 + </div>
2915 + {/if}
2916 + </div>
2917 +
2918 + <style>
2919 + .accordion-item {
2920 + &:not(:last-child) {
2921 + margin-bottom: var(--spacing-24);
2922 + }
2923 +
2924 + .accordion-heading {
2925 + display: flex;
2926 + gap: 2rem;
2927 + padding: 0;
2928 + border: none;
2929 + }
2930 +
2931 + .accordion-trigger {
2932 + transition: rotate 0.2s ease;
2933 +
2934 + &.open {
2935 + rotate: -90deg;
2936 + }
2937 + }
2938 +
2939 + .accordion-content {
2940 + margin-top: 0.5rem;
2941 + }
2942 + }
2943 + </style>
2944 + ```
2945 +
2946 + <Example name="accordion" />
2947 +
2948 + That's it! 😄 You can now use the `<Accordion>` component in your app.
2949 +
2950 + That being said, this has limited composability if you want to change the icon, or position of the individual accordion elements.
2951 +
2952 + Before you know it, you end up with an explosion of props:
2953 +
2954 + ```svelte:App.svelte {7-10}
2955 + <script lang="ts">
2956 + import { Accordion, AccordionItem } from './accordion'
2957 + </script>
2958 +
2959 + <Accordion>
2960 + <AccordionItem
2961 + title="Item A"
2962 + icon="👈️"
2963 + iconPosition="left"
2964 + ...
2965 + >
2966 + Content
2967 + </AccordionItem>
2968 + </Accordion>
2969 + ```
2970 +
2971 + That's not a way to live your life! Instead, you can use [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) so the user can render the accordion item however they want.
2972 +
2973 + ### Snippets
2974 +
2975 + 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:
2976 +
2977 + ```svelte:AccordionItem.svelte {8,9,11-13,17}
2978 + <script lang="ts">
2979 + import type { Snippet } from 'svelte'
2980 +
2981 + interface Props {
2982 + accordionItem?: Snippet<[accordionItem: { open: boolean; toggle: () => void }]>
2983 + }
2984 +
2985 + let { accordionItem }: Props = $props()
2986 + let open = $state(false)
2987 +
2988 + function toggle() {
2989 + open = !open
2990 + }
2991 + </script>
2992 +
2993 + <div class="accordion-item">
2994 + {@render accordionItem?.({ open, toggle })}
2995 + </div>
2996 + ```
2997 +
2998 + 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:
2999 +
3000 + ```svelte:App.svelte {6-17,20}
3001 + <script lang="ts">
3002 + import { slide } from 'svelte/transition'
3003 + import { Accordion, AccordionItem } from './accordion'
3004 + </script>
3005 +
3006 + {#snippet accordionItem({ open, toggle })}
3007 + <button onclick={toggle} class="accordion-heading">
3008 + <div>Item A</div>
3009 + <div class="accordion-trigger" class:open>👈️</div>
3010 + </button>
3011 +
3012 + {#if open}
3013 + <div transition:slide class="accordion-content">
3014 + Content
3015 + </div>
3016 + {/if}
3017 + {/snippet}
3018 +
3019 + <Accordion>
3020 + <AccordionItem {accordionItem} />
3021 + </Accordion>
3022 + ```
3023 +
3024 + 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:
3025 +
3026 + ```svelte:App.svelte {8-19}
3027 + <script lang="ts">
3028 + import { slide } from 'svelte/transition'
3029 + import { Accordion, AccordionItem } from './accordion'
3030 + </script>
3031 +
3032 + <Accordion>
3033 + <AccordionItem>
3034 + {#snippet accordionItem({ open, toggle })}
3035 + <button onclick={toggle} class="accordion-heading">
3036 + <div>Item A</div>
3037 + <div class"accordion-trigger" class:open>👈️</div>
3038 + </button>
3039 +
3040 + {#if open}
3041 + <div transition:slide class="accordion-content">
3042 + Content
3043 + </div>
3044 + {/if}
3045 + {/snippet}
3046 + </AccordionItem>
3047 + </Accordion>
3048 + ```
3049 +
3050 + <Card type="info">
3051 + You can export snippets from <code>&lt;script module&gt;</code> if they don't reference any state in a <code>script</code> block, and use them in other components.
3052 + </Card>
3053 +
3054 + This gives you complete control how the accordion item is rendered.
3055 +
3056 + ### The Context API
3057 +
3058 + 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?
3059 +
3060 + You might bind the `open` prop from the `<Accordion>` component, but then you have an extra prop:
3061 +
3062 + ```svelte:App.svelte {5,12,14}
3063 + <script lang="ts">
3064 + import { slide } from 'svelte/transition'
3065 + import { Accordion, AccordionItem } from './accordion'
3066 +
3067 + let open = $state(false)
3068 + </script>
3069 +
3070 + <button onclick={() => (open = !open)}>
3071 + {open ? 'Close' : 'Open'}
3072 + </button>
3073 +
3074 + <Accordion bind:open>
3075 + <!-- extra prop -->
3076 + <AccordionItem {open} />
3077 + </Accordion>
3078 + ```
3079 +
3080 + Instead of using props, you can use the context API. The context API is just a JavaScript [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object that holds key-value pairs.
3081 +
3082 + You can set the context in the parent component by using the `setContext` function, which accepts a key and a value:
3083 +
3084 + ```svelte:Accordion.svelte {10,12-14}
3085 + <script lang="ts">
3086 + import { setContext } from 'svelte'
3087 + import type { Snippet } from 'svelte'
3088 +
3089 + interface Props {
3090 + open: boolean
3091 + children: Snippet
3092 + }
3093 +
3094 + let { open = $bindable(), children }: Props = $props()
3095 +
3096 + setContext('accordion', {
3097 + get open() { return open }
3098 + })
3099 + </script>
3100 +
3101 + <div class="accordion">
3102 + {@render children?.()}
3103 + </div>
3104 + ```
3105 +
3106 + 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:
3107 +
3108 + ```svelte:AccordionItem.svelte {2,7,8}
3109 + <script lang="ts">
3110 + import { getContext } from 'svelte'
3111 +
3112 + // ...
3113 + let { accordionItem }: Props = $props()
3114 +
3115 + const accordion = getContext('accordion')
3116 + let open = $derived(accordion.open)
3117 +
3118 + function toggle() {
3119 + open = !open
3120 + }
3121 + </script>
3122 +
3123 + <div>
3124 + {@render accordionItem?.({ open, toggle })}
3125 + </div>
3126 + ```
3127 +
3128 + That's it! 😄
3129 +
3130 + ### Type-Safe Context
3131 +
3132 + You can make the context API more type-safe by creating a context file with helper functions:
3133 +
3134 + ```ts:context.ts
3135 + import { getContext, setContext } from 'svelte'
3136 +
3137 + interface Accordion {
3138 + open: boolean
3139 + }
3140 +
3141 + // unique key
3142 + const key = {}
3143 +
3144 + export function setAccordionContext(open: Accordion) {
3145 + setContext(key, open)
3146 + }
3147 +
3148 + export function getAccordionContext() {
3149 + return getContext(key) as Accordion
3150 + }
3151 + ```
3152 +
3153 + ### Passing State Into Context
3154 +
3155 + You've probably noticed how you can store reactive state in context. Let's take a step back, and explain how this works:
3156 +
3157 + ```ts:example
3158 + // why this?
3159 + setContext('accordion', {
3160 + get open() { return open }
3161 + })
3162 +
3163 + // ...and not this?
3164 + setContext('accordion', { open })
3165 + ```
3166 +
3167 + 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.
3168 +
3169 + Let's say this is the context API:
3170 +
3171 + ```ts:example
3172 + const context = new Map()
3173 +
3174 + function setContext(key, value) {
3175 + context.set(key, value)
3176 + }
3177 +
3178 + function getContext(key) {
3179 + return context.get(key)
3180 + }
3181 + ```
3182 +
3183 + The value passed to context is not a reference to the `value` variable, but the `🍌` value itself.
3184 +
3185 + If `value` changes after the context is set, it won't update:
3186 +
3187 + ```ts:example
3188 + let emoji = '🍌'
3189 +
3190 + setContext('ctx', { emoji })
3191 +
3192 + const ctx = getContext('ctx')
3193 + console.log(ctx.emoji) // 🍌
3194 +
3195 + emoji = '🍎'
3196 + console.log(ctx.emoji) // 🍌
3197 + ```
3198 +
3199 + Svelte doesn't change how JavaScript works — you need a mechanism which returns the latest value:
3200 +
3201 + ```ts:example
3202 + let emoji = '🍌'
3203 +
3204 + setContext('ctx', {
3205 + getLatestValue() { return emoji }
3206 + })
3207 +
3208 + const ctx = getContext('ctx')
3209 + console.log(ctx.getLatestValue()) // 🍌
3210 +
3211 + emoji = '🍎'
3212 + console.log(ctx.getLatestValue()) // 🍎
3213 + ```
3214 +
3215 + Same as before, you can use a **function**, **class**, **accessor**, or **proxied state** to get and set the value:
3216 +
3217 + ```ts:example
3218 + import { setContext } from 'svelte'
3219 +
3220 + let emoji = $state('🍌')
3221 +
3222 + // 👍 function
3223 + setContext('ctx', {
3224 + getEmoji() { return emoji },
3225 + updateEmoji(v) { emoji = v },
3226 + })
3227 +
3228 + const ctx = getContext('ctx')
3229 + ctx.getEmoji()
3230 + ctx.updateEmoji('🍎')
3231 +
3232 + // 👍 class
3233 + class Emoji {
3234 + current = $state('🍌')
3235 + }
3236 + setContext('ctx', { emoji: new Emoji() })
3237 +
3238 + const ctx = getContext('ctx')
3239 + ctx.emoji.current
3240 + ctx.emoji.current = '🍎'
3241 +
3242 + // 👍 property accesors
3243 + setContext('ctx', {
3244 + get emoji() { return emoji },
3245 + set emoji(v) { emoji = v },
3246 + })
3247 +
3248 + const ctx = getContext('ctx')
3249 + ctx.emoji
3250 + ctx.emoji = '🍎'
3251 +
3252 + // 👍 proxied state
3253 + let emoji = $state({ current: '🍌'})
3254 + setContext('ctx', { emoji })
3255 +
3256 + const ctx = getContext('ctx')
3257 + ctx.emoji.current
3258 + ctx.emoji.current = '🍎'
3259 + ```
3260 +
3261 + ### Module Context
3262 +
3263 + There's one more trick you should know when it comes to component composition, and it's the `module` script block.
3264 +
3265 + So far, we used the regular script block for component logic that's unique for every instance:
3266 +
3267 + ```svelte:Counter.svelte {3}
3268 + <script lang="ts">
3269 + // unique for every instance
3270 + let id = crypto.randomUUID()
3271 + </script>
3272 +
3273 + <p>{id}</p>
3274 + ```
3275 +
3276 + If you want to share code across component instances, you can use the `module` script block:
3277 +
3278 + ```svelte:Counter.svelte {1,3}
3279 + <script lang="ts" module>
3280 + // same for every instance
3281 + let id = crypto.randomUUID()
3282 + </script>
3283 +
3284 + <p>{id}</p>
3285 + ```
3286 +
3287 + <Card type="info">
3288 + If you need a unique identifier for a component instance, Svelte provides one through <code>$props.id</code>.
3289 + </Card>
3290 +
3291 + You can also share state between instances:
3292 +
3293 + ```svelte:Counter.svelte {5}
3294 + <script lang="ts" module>
3295 + // same for every instance
3296 + let id = crypto.randomUUID()
3297 + // state
3298 + let count = $state(0)
3299 + </script>
3300 +
3301 + <p>{id}</p>
3302 + <button onclick={() => count++}>{count}</button>
3303 + ```
3304 +
3305 + You can control media playback across component instances, or export functions and snippets from the module:
3306 +
3307 + ```svelte:Counter.svelte {7-9,11}
3308 + <script lang="ts" module>
3309 + // outputs different random number for every instance
3310 + let id = crypto.randomUUID()
3311 + // state
3312 + let count = $state(0)
3313 + // exporting functions
3314 + export function reset() {
3315 + count = 0
3316 + }
3317 + // exporting snippets
3318 + export { icon }
3319 + </script>
3320 +
3321 + <p>{id}</p>
3322 + <button onclick={() => count++}>{count}</button>
3323 +
3324 + {#snippet icon(width = 24, height = 24)}
3325 + <svg xmlns="http://www.w3.org/2000/svg" {width} {height} viewBox="0 0 24 24">
3326 + <circle cx="50%" cy="50%" r="50%" fill="orangered" />
3327 + </svg>
3328 + {/snippet}
3329 + ```
3330 +
3331 + This works great for sharing state across component instances, or just exporting some functions from the module:
3332 +
3333 + ```svelte:App.svelte
3334 + <script>
3335 + import Counter, { icon, reset } from './Counter.svelte'
3336 + </script>
3337 +
3338 + <Counter />
3339 + <Counter />
3340 + <Counter />
3341 + <Counter />
3342 + <button onclick={() => reset()}>Reset</button>
3343 +
3344 + {@render icon()}
3345 + {@render icon(50, 50)}
3346 + {@render icon(100, 100)}
3347 + {@render icon(200, 200)}
3348 + ```
3349 +
3350 + You can also have a regular script block, and a `module` script block in the same component.
3351 +
3352 + ## Transitions And Animations
3353 +
3354 + 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.
3355 +
3356 + ### Transitions
3357 +
3358 + To use a transition, you use the `transition:` directive on an element. Transitions play when the element is added to the DOM, and play in reverse when the element is removed from the DOM.
3359 +
3360 + 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:
3361 +
3362 + ```svelte:App.svelte {2,11-12}
3363 + <script lang="ts">
3364 + import { fade } from 'svelte/transition'
3365 +
3366 + let play = $state(false)
3367 +
3368 + setInterval(() => (play = !play), 2000)
3369 + </script>
3370 +
3371 + {#if play}
3372 + <div class="message">
3373 + <span transition:fade={{ duration: 600 }}>Hello</span>
3374 + <span transition:fade={{ delay: 600 }}>World</span>
3375 + </div>
3376 + {/if}
3377 +
3378 + <style>
3379 + .message {
3380 + font-size: 4rem;
3381 + }
3382 + </style>
3383 + ```
3384 +
3385 + <Example name="fade-transition" />
3386 +
3387 + You can have separate intro and outro transitions using the `in:` and `out:` directives:
3388 +
3389 + ```svelte:App.svelte {2,13-14,19-20}
3390 + <script lang="ts">
3391 + import { fade, fly } from 'svelte/transition'
3392 + import { cubicInOut } from 'svelte/easing'
3393 +
3394 + let play = $state(false)
3395 +
3396 + setInterval(() => (play = !play), 2000)
3397 + </script>
3398 +
3399 + {#if play}
3400 + <div class="message">
3401 + <span
3402 + in:fly={{ x: -10, duration: 600, easing: cubicInOut }}
3403 + out:fade
3404 + >
3405 + Hello
3406 + </span>
3407 + <span
3408 + in:fly={{ x: 10, delay: 600, easing: cubicInOut }}
3409 + out:fade
3410 + >
3411 + World
3412 + </span>
3413 + </div>
3414 + {/if}
3415 +
3416 + <style>
3417 + .message {
3418 + font-size: 4rem;
3419 + }
3420 + </style>
3421 + ```
3422 +
3423 + Svelte also has a lot of [built-in easing functions](https://svelte.dev/docs/svelte/svelte-easing) you can use to make a transition feel more natural, or give it more character.
3424 +
3425 + There's also a bunch of transition events you can listen to, including `introstart`, `introend`, `outrostart`, and `outroend`.
3426 +
3427 + ### Local And Global Transitions
3428 +
3429 + Let's say you have an `{#each ...}` block that renders a list of items using a staggered transition inside of an `{#if ...}` block:
3430 +
3431 + ```svelte:App.svelte {9-13}
3432 + <script lang="ts">
3433 + import { fade } from 'svelte/transition'
3434 +
3435 + let play = $state(false)
3436 + </script>
3437 +
3438 + {#if play}
3439 + <div class="items">
3440 + {#each Array(50), i}
3441 + <div in:fade={{ delay: i * 100 }}>
3442 + {i + 1}
3443 + </div>
3444 + {/each}
3445 + </div>
3446 + {/if}
3447 +
3448 + <style>
3449 + .items {
3450 + width: 400px;
3451 + display: grid;
3452 + grid-template-columns: repeat(10, 1fr);
3453 + gap: 1rem;
3454 + }
3455 + </style>
3456 + ```
3457 +
3458 + It doesn't work, but why?
3459 +
3460 + **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.
3461 +
3462 + The solution is to use the `global` modifier:
3463 +
3464 + ```svelte:App.svelte
3465 + <div in:fade|global={{ delay: i * 100 }}>
3466 + {i + 1}
3467 + </div>
3468 + ```
3469 +
3470 + <Example name="global-transitions" />
3471 +
3472 + Transitions were global by default in older versions of Svelte, so keep that in mind if you come across older Svelte code.
3473 +
3474 + ### Autoplaying Transitions Aside
3475 +
3476 + If you're using SvelteKit, transitions might not play immediately since the first page load is server-side rendered.
3477 +
3478 + If you want that behavior, you can create a component with an effect to trigger the transition when it's added to the DOM:
3479 +
3480 + ```svelte:Fade.svelte {11,13-15}
3481 + <script lang="ts">
3482 + import { fade, type FadeParams } from 'svelte/transition'
3483 + import type { Snippet } from 'svelte'
3484 +
3485 + interface Props {
3486 + children: Snippet
3487 + options?: FadeParams
3488 + }
3489 +
3490 + let { children, options }: Props = $props()
3491 + let play = $state(false)
3492 +
3493 + $effect(() => {
3494 + play = true
3495 + })
3496 + </script>
3497 +
3498 + {#if play}
3499 + <div transition:fade|global={options}>
3500 + {@render children?.()}
3501 + </div>
3502 + {/if}
3503 + ```
3504 +
3505 + Now you can use the `<Fade>` component in your app:
3506 +
3507 + ```svelte:Example.svelte
3508 + <script lang="ts">
3509 + import { Fade } from './transitions'
3510 + </script>
3511 +
3512 + <Fade options={{ duration: 2000 }}>
3513 + Boo! 👻
3514 + </Fade>
3515 + ```
3516 +
3517 + You can create a generic `<Transition>` component to conditionally render a transition, and use a prop to autoplay the transition:
3518 +
3519 + ```svelte:example
3520 + <Transition type="fade" play>
3521 + Boo! 👻
3522 + </Transition>
3523 + ```
3524 +
3525 + ### Custom Transitions
3526 +
3527 + You can find more built-in transitions in the [Svelte documentation](https://svelte.dev/docs/svelte/svelte-transition). If that isn't enough, you can also create custom transitions.
3528 +
3529 + Custom transitions are regular function which have to return an object with the transition options and a `css`, or `tick` function:
3530 +
3531 + ```svelte:App.svelte {5-17,25}
3532 + <script lang="ts">
3533 + import { elasticOut } from 'svelte/easing'
3534 + import type { TransitionConfig } from 'svelte/transition'
3535 +
3536 + function customTransition(node: HTMLElement, options?: TransitionConfig) {
3537 + const { duration = 2000, delay = 0, easing = elasticOut } = options
3538 +
3539 + return {
3540 + duration,
3541 + delay,
3542 + easing,
3543 + css: (t: number) => `
3544 + color: hsl(${360 * t} , 100%, 80%);
3545 + transform: scale(${t});
3546 + `,
3547 + }
3548 + }
3549 +
3550 + let play = $state(false)
3551 + </script>
3552 +
3553 + <button onclick={() => (play = !play)}>Play</button>
3554 +
3555 + {#if play}
3556 + <div in:customTransition>Whoooo!</div>
3557 + {/if}
3558 + ```
3559 +
3560 + <Example name="custom-transition" />
3561 +
3562 + You should always return a `css` function, because Svelte is going to create keyframes using the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) which is always more performant.
3563 +
3564 + 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.
3565 +
3566 + 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.
3567 +
3568 + Alternatively, you can return a `tick` function when you need to use JavaScript for a transition and Svelte is going to use the [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API:
3569 +
3570 + ```svelte:App.svelte {10-29,35}
3571 + <script lang="ts">
3572 + import { TransitionConfig } from 'svelte/transition'
3573 +
3574 + const chars = '!@#$%&*1234567890-=_+[]{}|;:,.<>/?'
3575 +
3576 + function getRandomCharacter() {
3577 + return chars[Math.floor(Math.random() * chars.length)]
3578 + }
3579 +
3580 + function scrambleText(node: HTMLElement, options?: TransitionConfig) {
3581 + const { duration = 4000 } = options
3582 + const finalText = node.textContent
3583 + const length = finalText.length
3584 +
3585 + return {
3586 + duration,
3587 + tick: (t: number) => {
3588 + let output = ''
3589 + for (let i = 0; i < length; i++) {
3590 + if (t > i / length) {
3591 + output += finalText[i]
3592 + } else {
3593 + output += getRandomCharacter()
3594 + }
3595 + }
3596 + node.textContent = output
3597 + },
3598 + }
3599 + }
3600 +
3601 + let play = $state(false)
3602 + </script>
3603 +
3604 + {#key play}
3605 + <p in:scrambleText>Scrambling Text Effect</p>
3606 + {/key}
3607 +
3608 + <button onclick={() => (play = !play)}>Scramble text</button>
3609 +
3610 + <style>
3611 + p {
3612 + font-family: monospace;
3613 + }
3614 + </style>
3615 + ```
3616 +
3617 + <Example name="scramble-text" />
3618 +
3619 + You can define custom transitions in a separate file and import them in your app.
3620 +
3621 + ### Coordinating Transitions Between Different Elements
3622 +
3623 + Sometimes you want to coordinate a transition between different elements when the DOM changes.
3624 +
3625 + In this example, we have a section for published posts and archived posts where you can archive and unarchive post:
3626 +
3627 + ```svelte:App.svelte
3628 + <script lang="ts">
3629 + interface Post {
3630 + id: number
3631 + title: string
3632 + description: string
3633 + published: boolean
3634 + }
3635 +
3636 + let posts = $state<Post[]>([
3637 + {
3638 + id: 1,
3639 + title: 'Post',
3640 + description: 'Content',
3641 + published: true,
3642 + },
3643 + // ...
3644 + ])
3645 +
3646 + function togglePublished(post: Post) {
3647 + const index = posts.findIndex((p) => p.id === post.id)
3648 + posts[index].published = !posts[index].published
3649 + }
3650 +
3651 + function removePost(post: Post) {
3652 + const index = posts.findIndex((p) => p.id === post.id)
3653 + posts.splice(index, 1)
3654 + }
3655 + </script>
3656 +
3657 + <div class="posts">
3658 + <h2>Posts</h2>
3659 + <section>
3660 + {#each posts.filter((posts) => posts.published) as post (post)}
3661 + <article>
3662 + <h3>{post.title}</h3>
3663 + <p >{post.description}</p>
3664 + <div>
3665 + <button onclick={() => togglePublished(post)}>💾</button>
3666 + <button onclick={() => removePost(post)}>❌</button>
3667 + </div>
3668 + </article>
3669 + {:else}
3670 + <p>There are no posts.</p>
3671 + {/each}
3672 + </section>
3673 + </div>
3674 +
3675 + <div class="archive">
3676 + <h2>Archive</h2>
3677 + <section>
3678 + {#each posts.filter((posts) => !posts.published) as post (post)}
3679 + <article>
3680 + <h3>{post.title}</h3>
3681 + <div>
3682 + <button onclick={() => togglePublished(post)}>♻️</button>
3683 + </div>
3684 + </article>
3685 + {:else}
3686 + <p>Archived items go here.</p>
3687 + {/each}
3688 + </section>
3689 + </div>
3690 +
3691 + <style>
3692 + .posts {
3693 + display: flex;
3694 +
3695 + section {
3696 + display: grid;
3697 + grid-template-columns: repeat(2, 240px);
3698 + gap: 2rem;
3699 + }
3700 + }
3701 +
3702 + .archive {
3703 + section {
3704 + display: block;
3705 + width: 200px;
3706 + margin-top: 1rem;
3707 +
3708 + article:not(:last-child) {
3709 + margin-bottom: 1rem;
3710 + }
3711 + }
3712 + }
3713 +
3714 + p {
3715 + margin-bottom: 0.5rem;
3716 + }
3717 + </style>
3718 + ```
3719 +
3720 + This works, but the user experience is not great!
3721 +
3722 + 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.
3723 +
3724 + The `crossfade` transition creates two transitions named `send` and `receive` which accept a unique key to know what to transition:
3725 +
3726 + ```svelte:App.svelte {2,4,10-11,17-18}
3727 + <script lang="ts">
3728 + import { crossfade } from 'svelte/transition'
3729 +
3730 + const [send, receive] = crossfade({})
3731 + // ...
3732 + </script>
3733 +
3734 + <!-- published posts -->
3735 + <article
3736 + in:receive={{ key: post }}
3737 + out:send={{ key: post }}
3738 + >
3739 + <!-- ... -->
3740 +
3741 + <!-- archived posts -->
3742 + <article
3743 + in:receive={{ key: post }}
3744 + out:send={{ key: post }}
3745 + >
3746 + <!-- ... -->
3747 + ```
3748 +
3749 + <Example name="crossfade" />
3750 +
3751 + You can also pass `duration` and a custom `fallback` transition options when there are no matching transitions:
3752 +
3753 + ```ts:App.svelte
3754 + const [send, receive] = crossfade({
3755 + // the duration is based on the distance
3756 + duration: (d) => Math.sqrt(d * 200),
3757 + // custom transition
3758 + fallback(node, params) {
3759 + return {
3760 + css: (t) => `
3761 + transform: scale(${t});
3762 + opacity: ${t};
3763 + `
3764 + }
3765 + }
3766 + })
3767 + ```
3768 +
3769 + That's it! 😄
3770 +
3771 + These days there are web APIs to transition view changes like the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API), but they can be limiting and might not be supported in all browsers.
3772 +
3773 + ### FLIP Animations
3774 +
3775 + In the previous example, we used the `crossfade` transition to coordinate transitions between different elements, but it's not perfect. When a post is archived or published, all the items "wait" for the transition to end before they "snap" into their new position.
3776 +
3777 + 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:
3778 +
3779 + ```svelte:App.svelte {2,11,19}
3780 + <script lang="ts">
3781 + import { flip } from 'svelte/animate'
3782 + import { crossfade } from 'svelte/transition'
3783 +
3784 + const [send, receive] = crossfade({})
3785 + // ...
3786 + </script>
3787 +
3788 + <!-- published posts -->
3789 + <article
3790 + animate:flip={{ duration: 200 }}
3791 + in:receive={{ key: post }}
3792 + out:send={{ key: post }}
3793 + >
3794 + <!-- ... -->
3795 +
3796 + <!-- archived posts -->
3797 + <article
3798 + animate:flip={{ duration: 200 }}
3799 + in:receive={{ key: post }}
3800 + out:send={{ key: post }}
3801 + >
3802 + <!-- ... -->
3803 + ```
3804 +
3805 + <Example name="crossfade-flip" />
3806 +
3807 + Isn't that magical? 🤩🪄
3808 +
3809 + [FLIP](https://aerotwist.com/blog/flip-your-animations/) 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.
3810 +
3811 + 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](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect#Properties) which has the size and position of the element before and after the change and `parameters`.
3812 +
3813 + Here's a custom FLIP animation I _yoinked_ from the Svelte source code:
3814 +
3815 + ```ts:animations.ts
3816 + interface Options {
3817 + duration?: number
3818 + }
3819 +
3820 + function flip(
3821 + node: HTMLElement,
3822 + { from, to }: { from: DOMRect; to: DOMRect },
3823 + options: Options = {}
3824 + ) {
3825 + const { duration = 2000 } = options
3826 +
3827 + const dx = from.left - to.left
3828 + const dy = from.top - to.top
3829 + const dsx = from.width / to.width
3830 + const dsy = from.height / to.height
3831 +
3832 + return {
3833 + duration,
3834 + css: (t: number, u: number) => {
3835 + const x = dx * u
3836 + const y = dy * u
3837 + const sx = dsx + (1 - dsx) * t
3838 + const sy = dsy + (1 - dsy) * t
3839 + return `transform: translate(${x}px, ${y}px) scale(${sx}, ${sy})`
3840 + }
3841 + }
3842 + }
3843 + ```
3844 +
3845 + 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.
3846 +
3847 + ### Tweened Values And Springs
3848 +
3849 + 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.
3850 +
3851 + 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:
3852 +
3853 + ```svelte:App.svelte {2,5,8,12,22}
3854 + <script lang="ts">
3855 + import { Tween } from 'svelte/motion'
3856 + import { cubicInOut } from 'svelte/easing'
3857 +
3858 + const size = new Tween(50, { duration: 300, easing: cubicInOut })
3859 +
3860 + function onmousedown() {
3861 + size.target = 150
3862 + }
3863 +
3864 + function onmouseup() {
3865 + size.target = 50
3866 + }
3867 + </script>
3868 +
3869 + <svg width="400" height="400" viewBox="0 0 400 400">
3870 + <circle
3871 + {onmousedown}
3872 + {onmouseup}
3873 + cx="200"
3874 + cy="200"
3875 + r={size.current}
3876 + fill="orangered"
3877 + />
3878 + </svg>
3879 + ```
3880 +
3881 + <Example name="tween" />
3882 +
3883 + The `Spring` 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:
3884 +
3885 + ```svelte:App.svelte {2,4,7,11,21}
3886 + <script lang="ts">
3887 + import { Spring } from 'svelte/motion'
3888 +
3889 + const size = new Spring(50, { stiffness: 0.1, damping: 0.25, precision: 0.1 })
3890 +
3891 + function onmousedown() {
3892 + size.target = 150
3893 + }
3894 +
3895 + function onmouseup() {
3896 + size.target = 50
3897 + }
3898 + </script>
3899 +
3900 + <svg width="400" height="400" viewBox="0 0 400 400">
3901 + <circle
3902 + {onmousedown}
3903 + {onmouseup}
3904 + cx="200"
3905 + cy="200"
3906 + r={size.current}
3907 + fill="orangered"
3908 + />
3909 + </svg>
3910 + ```
3911 +
3912 + <Example name="spring" />
3913 +
3914 + They both have a `set` function, which returns a promise and lets you override the options:
3915 +
3916 + ```ts:App.svelte
3917 + async function onmousedown() {
3918 + // using `target` to update the value
3919 + size.target = 150
3920 + // using `set` to update the value
3921 + await size.set(150, { duration: 200 })
3922 + }
3923 + ```
3924 +
3925 + If you want to update the `Tween` or `Spring` value when a reactive value changes, you can use the `of` method:
3926 +
3927 + ```svelte:App.svelte
3928 + <script lang="ts">
3929 + import { Spring, Tween } from 'svelte/motion'
3930 +
3931 + let { value, options } = $props()
3932 +
3933 + Tween.of(() => value, options)
3934 + Spring.of(() => value, options)
3935 + </script>
3936 + ```
3937 +
3938 + ## Using Third Party Libraries
3939 +
3940 + 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.
3941 +
3942 + ### Component Lifecycle Functions
3943 +
3944 + 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.
3945 +
3946 + Let's look at how we can use the popular [GSAP](https://gsap.com/) 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).
3947 +
3948 + Here's a basic GSAP example for creating a tween animation:
3949 +
3950 + ```svelte:App.svelte
3951 + <script lang="ts">
3952 + import gsap from 'gsap'
3953 +
3954 + gsap.to('.box', { rotation: 360, x: 200, duration: 2 })
3955 + </script>
3956 +
3957 + <div class="box"></div>
3958 +
3959 + <style>
3960 + .box {
3961 + width: 100px;
3962 + height: 100px;
3963 + background-color: orangered;
3964 + border-radius: 1rem;
3965 + }
3966 + </style>
3967 + ```
3968 +
3969 + If you try this example in Svelte, you 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.
3970 +
3971 + For this reason, Svelte provides an `onMount` lifecycle function. The "lifecyle" part refers to the life of the component, since it runs when the component is added and removed if you return a cleanup function:
3972 +
3973 + ```svelte:App.svelte {2,5-7}
3974 + <script lang="ts">
3975 + import { onMount } from 'svelte'
3976 + import gsap from 'gsap'
3977 +
3978 + onMount(() => {
3979 + gsap.to('.box', { rotation: 360, x: 200, duration: 2 })
3980 + })
3981 + </script>
3982 +
3983 + <div class="box"></div>
3984 +
3985 + <style>
3986 + .box {
3987 + width: 100px;
3988 + height: 100px;
3989 + background-color: orangered;
3990 + border-radius: 1rem;
3991 + }
3992 + </style>
3993 + ```
3994 +
3995 + <Example name="gsap-box" />
3996 +
3997 + This works! That being said, it's not ideal that we query any element with a `.box` class on the page.
3998 +
3999 + Using Svelte, we should use a reference to the element instead. You can also return a cleanup function from `onMount`, or use the `onDestroy` lifecycle function for any cleanup when the component is removed:
4000 +
4001 + ```svelte:App.svelte {2,5,10,13-16,19}
4002 + <script lang="ts">
4003 + import { onDestroy, onMount } from 'svelte'
4004 + import gsap from 'gsap'
4005 +
4006 + let tween: gsap.core.Tween
4007 + let target: HTMLElement
4008 +
4009 + onMount(() => {
4010 + tween = gsap.to(target, { rotation: 180, x: 100, duration: 1 })
4011 + return () => tween.kill()
4012 + })
4013 +
4014 + // alternative cleanup
4015 + onDestroy(() => {
4016 + tween.kill()
4017 + })
4018 + </script>
4019 +
4020 + <div class="box" bind:this={target}></div>
4021 +
4022 + <style>
4023 + .box {
4024 + width: 100px;
4025 + height: 100px;
4026 + background-color: orangered;
4027 + border-radius: 1rem;
4028 + }
4029 + </style>
4030 + ```
4031 +
4032 + ### Effects Versus Lifecycle Functions
4033 +
4034 + You can also use effects to achieve the same thing:
4035 +
4036 + ```svelte:App.svelte {5,7-10,13}
4037 + <script lang="ts">
4038 + import gsap from 'gsap'
4039 +
4040 + let tween: gsap.core.Tween
4041 + let target: HTMLElement
4042 +
4043 + $effect(() => {
4044 + tween = gsap.to(target, { rotation: 360, x: 200, duration: 2 })
4045 + return () => tween.kill()
4046 + })
4047 + </script>
4048 +
4049 + <div class="box" bind:this={target}></div>
4050 +
4051 + <style>
4052 + .box {
4053 + width: 100px;
4054 + height: 100px;
4055 + background-color: orangered;
4056 + border-radius: 1rem;
4057 + }
4058 + </style>
4059 + ```
4060 +
4061 + So why do both of them exist?
4062 +
4063 + Effects aren't component lifecycle functions, because their "lifecycle" depends on the value inside of them updating.
4064 +
4065 + Using `onMount` makes more sense if you don't care about tracking state — you might track state inside of the effect on accident, and then have to [untrack](https://svelte.dev/docs/svelte/svelte#untrack) the value:
4066 +
4067 + ```ts:example
4068 + import { untrack } from 'svelte'
4069 +
4070 + let value_you_dont_want_to_track = $state('')
4071 + let value_you_want_to_track = $state('')
4072 +
4073 + $effect(() => {
4074 + untrack(() => value_you_dont_want_to_track) // 🙈 oops!
4075 + value_you_want_to_track
4076 + })
4077 + ```
4078 +
4079 + It's your choice, of course! If you understand how `$effect` works, you won't get unexpected surprises.
4080 +
4081 + Alright, our code works! Let's go a step further and create a `<Tween>` component which accepts `tween`, `vars` and `children` as props:
4082 +
4083 + ```svelte:Tween.svelte
4084 + <script lang="ts">
4085 + import gsap from 'gsap'
4086 + import type { Snippet } from 'svelte'
4087 +
4088 + interface Props = {
4089 + tween: gsap.core.Tween
4090 + vars: gsap.TweenVars
4091 + children: Snippet
4092 + }
4093 +
4094 + let { tween = $bindable(), vars, children }: Props = $props()
4095 + let target: HTMLElement
4096 +
4097 + $effect(() => {
4098 + tween = gsap.to(target, vars)
4099 + return () => tween.kill()
4100 + })
4101 + </script>
4102 +
4103 + <div bind:this={target}>
4104 + {@render children?.()}
4105 + </div>
4106 + ```
4107 +
4108 + This gives us a generic animation component we can pass any element to, and bind the `tween` prop to get the animation controls:
4109 +
4110 + ```svelte:App.svelte
4111 + <script lang="ts">
4112 + import Tween from './Tween.svelte'
4113 +
4114 + let animation: gsap.core.Tween
4115 + </script>
4116 +
4117 + <Tween bind:tween={animation} vars={{ rotation: 360, x: 200, duration: 2 }}>
4118 + <button onclick={() => animation.restart()} class="box">Play</button>
4119 + </Tween>
4120 +
4121 + <style>
4122 + .box {
4123 + width: 100px;
4124 + height: 100px;
4125 + background-color: orangered;
4126 + border-radius: 1rem;
4127 + }
4128 + </style>
4129 + ```
4130 +
4131 + ### Element Lifecycle Functions Using Attachments
4132 +
4133 + So far, we learned how we can use `onMount` to get a reference to an element when the component is added.
4134 +
4135 + What if you had `onMount` for elements instead of components? You would have attachments.
4136 +
4137 + 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:
4138 +
4139 + ```svelte:example {7-9}
4140 + <script lang="ts">
4141 + import gsap from 'gsap'
4142 + </script>
4143 +
4144 + <div
4145 + class="box"
4146 + {@attach (box) => {
4147 + gsap.to(box, { rotation: 360, x: 200, duration: 2 })
4148 + }}
4149 + ></div>
4150 +
4151 + <style>
4152 + .box {
4153 + width: 100px;
4154 + height: 100px;
4155 + background-color: orangered;
4156 + border-radius: 1rem;
4157 + }
4158 + </style>
4159 + ```
4160 +
4161 + <Example name="attachment" />
4162 +
4163 + Instead of the animation component, we can create an attachment function which can be used on any element.
4164 +
4165 + In this example, the `tween` function accept the animations options and an optional callback to get a reference to the tween:
4166 +
4167 + ```svelte:App.svelte {4-12,18-21}
4168 + <script lang="ts">
4169 + import { gsap } from 'gsap'
4170 +
4171 + function tween(vars, ref) {
4172 + let tween: gsap.core.Tween
4173 +
4174 + return (target: HTMLElement) => {
4175 + tween = gsap.to(target, vars)
4176 + ref?.(tween)
4177 + return () => tween.kill()
4178 + }
4179 + }
4180 +
4181 + let animation: gsap.core.Tween
4182 + </script>
4183 +
4184 + <button
4185 + class="box"
4186 + {@attach tween(
4187 + { rotation: 360, x: 200, duration: 2 },
4188 + (tween) => animation = tween
4189 + )}
4190 + onclick={() => animation.restart()}
4191 + >
4192 + Play
4193 + </button>
4194 + ```
4195 +
4196 + 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(...)}`.
4197 +
4198 + ## Reactive Data Structures And Utilities
4199 +
4200 + Svelte has reactive versions of JavaScript built-in objects like `Map`, `Set`, `Date`, and `URL`.
4201 +
4202 + In this example, we use the reactive version of the built-in `Map` object in JavaScript to cache the pokemon data:
4203 +
4204 + ```svelte:App.svelte {3,8,12,22}
4205 + <script lang="ts">
4206 + import { getAbortSignal } from 'svelte'
4207 + import { SvelteMap } from 'svelte/reactivity'
4208 +
4209 + let name = $state('')
4210 +
4211 + // pokemon cache
4212 + const pokemon = new SvelteMap<string, unknown>()
4213 +
4214 + async function getPokemon() {
4215 + // hits the cache
4216 + if (!name || pokemon.has(name)) return
4217 +
4218 + const baseUrl = 'https://pokeapi.co/api/v2/pokemon'
4219 + const response = await fetch(`${baseUrl}/${name}`, {
4220 + signal: getAbortSignal()
4221 + })
4222 + if (!response.ok) throw new Error('💣️ oops!')
4223 + const data = await response.json()
4224 +
4225 + // add to cache
4226 + pokemon.set(name, data)
4227 + }
4228 +
4229 + $effect(() => {
4230 + getPokemon()
4231 + })
4232 + </script>
4233 +
4234 + <div class="container">
4235 + <input type="search" bind:value={name} placeholder="Enter Pokemon name" />
4236 +
4237 + <div class="pokemon">
4238 + {#each pokemon as [name, details]}
4239 + <details>
4240 + <summary>{name}</summary>
4241 + <div class="data">
4242 + <pre>{JSON.stringify(details, null, 2)}</pre>
4243 + </div>
4244 + </details>
4245 + {/each}
4246 + </div>
4247 +
4248 + <button onclick={() => pokemon.clear()}>🧹 Clear</button>
4249 + </div>
4250 +
4251 + <style>
4252 + .container {
4253 + width: 400px;
4254 + }
4255 +
4256 + input,
4257 + button {
4258 + width: 100%;
4259 + }
4260 +
4261 + input {
4262 + margin-bottom: 2rem;
4263 + padding: 1rem;
4264 + color: #000;
4265 + border-radius: 1rem;
4266 + }
4267 +
4268 + summary {
4269 + text-transform: capitalize;
4270 + }
4271 +
4272 + details {
4273 + overflow: hidden;
4274 + margin-bottom: 2rem;
4275 +
4276 + .data {
4277 + height: 200px;
4278 + }
4279 + }
4280 + </style>
4281 + ```
4282 +
4283 + <Example name="reactive-map" />
4284 +
4285 + You can find more [reactive built-ins](https://svelte.dev/docs/svelte/svelte-reactivity) like `MediaQuery` and `prefersReducedMotion` with examples in the Svelte documentation.
4286 +
4287 + Svelte also provides a convenient way to make external APIs reactive, which we're going to learn about in the next section.
4288 +
4289 + ## Reactive Events
4290 +
4291 + 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.
4292 +
4293 + ### Web Storage API Example
4294 +
4295 + Let's look at the counter example from before:
4296 +
4297 + ```ts:counter.svelte.ts
4298 + export class Counter {
4299 + #first = true
4300 +
4301 + constructor(initial: number) {
4302 + this.#count = $state(initial)
4303 + }
4304 +
4305 + get count() {
4306 + if (this.#first) {
4307 + const savedCount = localStorage.getItem('count')
4308 + if (savedCount) this.#count = parseInt(savedCount)
4309 + this.#first = false
4310 + }
4311 + return this.#count
4312 + }
4313 +
4314 + set count(v: number) {
4315 + localStorage.setItem('count', v.toString())
4316 + this.#count = v
4317 + }
4318 + }
4319 + ```
4320 +
4321 + This can be made simpler using the [createSubscriber](https://svelte.dev/docs/svelte/svelte-reactivity#createSubscriber) function from Svelte.
4322 +
4323 + Instead of using state, you only have to listen for the `storage` event on the `window` and run `update` when it changes to notify subscribers:
4324 +
4325 + ```ts:counter.svelte.ts {5,8-14,17-21,23-25}
4326 + import { createSubscriber } from 'svelte/reactivity'
4327 + import { on } from 'svelte/events'
4328 +
4329 + class Counter {
4330 + #subscribe
4331 +
4332 + constructor(initial: number) {
4333 + this.#subscribe = createSubscriber((update) => {
4334 + if (!localStorage.getItem('count')) {
4335 + localStorage.setItem('count', initial.toString())
4336 + }
4337 + const off = on(window, 'storage', update)
4338 + return () => off()
4339 + })
4340 + }
4341 +
4342 + get count() {
4343 + // makes it reactive when read inside an effect
4344 + this.#subscribe()
4345 + return parseInt(localStorage.getItem('count') ?? '0')
4346 + }
4347 +
4348 + set count(v: number) {
4349 + localStorage.setItem('count', v.toString())
4350 + }
4351 + }
4352 + ```
4353 +
4354 + 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.
4355 +
4356 + 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.
4357 +
4358 + ### GSAP Animation Timeline Example
4359 +
4360 + Let's say you have a GSAP animation timeline you want to be able to control:
4361 +
4362 + ```svelte:App.svelte
4363 + <script lang="ts">
4364 + import { onMount } from 'svelte'
4365 + import gsap from 'gsap'
4366 +
4367 + type Tween = [string | HTMLElement, gsap.TweenVars]
4368 +
4369 + class Timeline {
4370 + #timeline = gsap.timeline()
4371 +
4372 + constructor(tweens: Tween[]) {
4373 + this.populateTimeline(tweens)
4374 + }
4375 +
4376 + populateTimeline(tweens: Tween[]) {
4377 + onMount(() => {
4378 + tweens.forEach(([element, vars]) => {
4379 + this.#timeline.to(element, vars)
4380 + })
4381 + })
4382 + }
4383 + }
4384 +
4385 + const tl = new Timeline([
4386 + ['.box1', { x: 200, duration: 1 }],
4387 + ['.box2', { x: 200, duration: 1 }]
4388 + ])
4389 + </script>
4390 +
4391 + <div class="box box1"></div>
4392 + <div class="box box2"></div>
4393 +
4394 + <style>
4395 + .box {
4396 + aspect-ratio: 1;
4397 + width: 100px;
4398 + background-color: orangered;
4399 + border-radius: 1rem;
4400 + }
4401 + </style>
4402 + ```
4403 +
4404 + Let's use `eventCallback` from GSAP to subscribe to updates and use an effect to update the playhead when `time` updates:
4405 +
4406 + ```svelte:App.svelte {9,14-16,18-20,31-33,35-37,48}
4407 + <script lang="ts">
4408 + import { onMount } from 'svelte'
4409 + import gsap from 'gsap'
4410 +
4411 + type Tween = [string | HTMLElement, gsap.TweenVars]
4412 +
4413 + class Timeline {
4414 + #timeline = gsap.timeline()
4415 + #time = $state(0)
4416 +
4417 + constructor(tweens: Tween[]) {
4418 + this.populateTimeline(tweens)
4419 +
4420 + $effect(() => {
4421 + this.#timeline.seek(this.#time)
4422 + })
4423 +
4424 + this.#timeline.eventCallback('onUpdate', () => {
4425 + this.#time = this.#timeline.time()
4426 + })
4427 + }
4428 +
4429 + populateTimeline(tweens: Tween[]) {
4430 + onMount(() => {
4431 + tweens.forEach(([element, vars]) => {
4432 + this.#timeline.to(element, vars)
4433 + })
4434 + })
4435 + }
4436 +
4437 + get time() {
4438 + return this.#time
4439 + }
4440 +
4441 + set time(v) {
4442 + this.#time = v
4443 + }
4444 + }
4445 +
4446 + const tl = new Timeline([
4447 + ['.box1', { x: 200, duration: 1 }],
4448 + ['.box2', { x: 200, duration: 1 }]
4449 + ])
4450 + </script>
4451 +
4452 + <label>
4453 + <p>Time:</p>
4454 + <input bind:value={tl.time} type="range" min={0} max={2} step={0.01} />
4455 + </label>
4456 +
4457 + <div class="box box1"></div>
4458 + <div class="box box2"></div>
4459 +
4460 + <style>
4461 + .box {
4462 + aspect-ratio: 1;
4463 + width: 100px;
4464 + background-color: orangered;
4465 + border-radius: 1rem;
4466 + }
4467 + </style>
4468 + ```
4469 +
4470 + This can also be made simpler with `createSubscriber` and using the `update` function to rerun the subscriber. In this example, the subscriber is the `time` method:
4471 +
4472 + ```svelte:App.svelte {10,14-17,28-32,34-36}
4473 + <script lang="ts">
4474 + import { onMount } from 'svelte'
4475 + import { createSubscriber } from 'svelte/reactivity'
4476 + import gsap from 'gsap'
4477 +
4478 + type Tween = [string | HTMLElement, gsap.TweenVars]
4479 +
4480 + class Timeline {
4481 + #timeline = gsap.timeline()
4482 + #subscribe
4483 +
4484 + constructor(tweens: Tween[]) {
4485 + this.populateTimeline(tweens)
4486 + this.#subscribe = createSubscriber((update) => {
4487 + this.#timeline.eventCallback('onUpdate', update)
4488 + return () => this.#timeline.eventCallback('onUpdate', null)
4489 + })
4490 + }
4491 +
4492 + populateTimeline(tweens: Tween[]) {
4493 + onMount(() => {
4494 + tweens.forEach(([element, vars]) => {
4495 + this.#timeline.to(element, vars)
4496 + })
4497 + })
4498 + }
4499 +
4500 + get time() {
4501 + // makes it reactive when read inside an effect
4502 + this.#subscribe()
4503 + return this.#timeline.time()
4504 + }
4505 +
4506 + set time(v) {
4507 + this.#timeline.seek(v)
4508 + }
4509 + }
4510 +
4511 + const tl = new Timeline([
4512 + ['.box1', { x: 200, duration: 1 }],
4513 + ['.box2', { x: 200, duration: 1 }]
4514 + ])
4515 + </script>
4516 +
4517 + <div class="box box1"></div>
4518 + <div class="box box2"></div>
4519 +
4520 + <label>
4521 + <p>Time:</p>
4522 + <input bind:value={tl.time} type="range" min={0} max={2} step={0.01} />
4523 + </label>
4524 +
4525 + <style>
4526 + .box {
4527 + aspect-ratio: 1;
4528 + width: 100px;
4529 + margin-bottom: 0.5rem;
4530 + background-color: orangered;
4531 + border-radius: 1rem;
4532 + }
4533 +
4534 + label {
4535 + display: flex;
4536 + gap: 0.5rem;
4537 + margin-top: 1rem;
4538 + }
4539 + </style>
4540 + ```
4541 +
4542 + <Example name="reactive-events" />
4543 +
4544 + Using `createSubscriber` can make your code much simpler, and the `update` function also gives you control **when** to run the update.
4545 +
4546 + ## Special Elements
4547 +
4548 + 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.
4549 +
4550 + 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:
4551 +
4552 + ```svelte:App.svelte {6-8,11-12}
4553 + <script lang="ts">
4554 + import { onMount } from 'svelte'
4555 +
4556 + let scrollY = $state(0)
4557 +
4558 + function handleScroll() {
4559 + scrollY = window.scrollY
4560 + }
4561 +
4562 + onMount(() => {
4563 + window.addEventListener('scroll', handleScroll)
4564 + return () => window.removeEventListener('scroll', handleScroll)
4565 + })
4566 + </script>
4567 +
4568 + <div>{scrollY}px</div>
4569 +
4570 + <style>
4571 + :global(body) {
4572 + height: 8000px;
4573 + }
4574 +
4575 + div {
4576 + position: fixed;
4577 + top: 50%;
4578 + left: 50%;
4579 + translate: -50% -50%;
4580 + font-size: 8vw;
4581 + font-weight: 700;
4582 + }
4583 + </style>
4584 + ```
4585 +
4586 + Thankfully, Svelte makes this easy with special elements like `<svelte:window>` and it does the cleanup for you:
4587 +
4588 + ```svelte:App.svelte {9}
4589 + <script lang="ts">
4590 + let scrollY = $state(0)
4591 +
4592 + function handleScroll() {
4593 + scrollY = window.scrollY
4594 + }
4595 + </script>
4596 +
4597 + <svelte:window onscroll={handleScroll} />
4598 +
4599 + <div>{scrollY}px</div>
4600 + ```
4601 +
4602 + There are also bindings for properties like the scroll position:
4603 +
4604 + ```svelte:App.svelte {2,5}
4605 + <script lang="ts">
4606 + let scrollY = $state(0)
4607 + </script>
4608 +
4609 + <svelte:window bind:scrollY />
4610 + ```
4611 +
4612 + 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:
4613 +
4614 + ```svelte:App.svelte
4615 + <script lang="ts">
4616 + import { scrollY } from 'svelte/reactivity/window'
4617 + </script>
4618 +
4619 + <div>{scrollY.current}px</div>
4620 + ```
4621 +
4622 + ## Legacy Svelte
4623 +
4624 + 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](https://svelte.dev/docs/svelte/legacy-overview) in the Svelte documentation.
4625 +
4626 + 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**.
4627 +
4628 + 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.
4629 +
4630 + 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:
4631 +
4632 + ```svelte:Component.svelte
4633 + <svelte:options runes={true} />
4634 + ```
Próximo Anterior