A Svelte component

Why:A component is a piece of interface with its own data, markup and styling. Afterwards you’ll read a .svelte file, understand the runes $state, $derived and $props, and move a block of your page into components: a card and a list with search.

30 minutes

Read by speech synthesis (Qwen3-TTS); the voice is not a person’s.

A Svelte component is a .svelte file with the code, markup and styles of one piece of interface. The runes $state, $derived and $props set its state and inputs.

A file where a piece of interface lives

In the last lesson you saw that frameworks offer two ideas: the view is derived from state, and an interface is assembled from components. Now let’s look at how that looks in Svelte. The choice isn’t random: this textbook is itself written in Svelte 5, and every diagram on its pages is a component like the ones below.

A Svelte component lives in a file with the extension .svelte and has three parts you already know: code (<script>), markup (ordinary HTML with a few additions) and styling (<style>, optional). Here is an offer card in full:

<script>
  let { item } = $props();
  let open = $state(false);
</script>

<article class="card">
  <h3>{item.title}</h3>
  <p class="price">{item.price}</p>
  <button type="button" aria-expanded={open} onclick={() => (open = !open)}>More</button>
  {#if open}
    <p>{item.text}</p>
  {/if}
</article>

Nothing unusual for a person who has written HTML. Three things are unusual: words with a dollar sign, curly braces in the markup, and the fact that the page changes after the button is pressed, although we never wrote “find the element and toggle it”. Let’s take them in order.

Runes: words the compiler understands

The words with a dollar sign are called runes. They are special words of Svelte 5: the compiler sees them and turns ordinary code into reactive code.

  • $props() receives data from the parent. The line let { item } = $props() reads: “I’ll be given a property called item”. The parent writes <ItemCard {item} />. Data flows down, from parent to child.
  • $state(value) declares state: a value whose change updates the screen. In let open = $state(false), while open is false the block with the description is hidden. Press the button, open = !open runs, and Svelte shows the block by itself. Notice that this is an ordinary assignment, with no setOpen.
  • $derived(expression) declares a value that is computed from others and recalculates itself. If you have query (what is typed in the search) and the list items, then let shown = $derived(items.filter(...)) always answers the current query.
  • $effect(() => ...) runs a side effect after the state has changed, such as writing a value to localStorage. Reach for it last: most tasks are solved by the first three runes.

Here is a list with search: it receives all the items, keeps the query and shows the filtered result.

<script>
  import ItemCard from './ItemCard.svelte';

  let { items } = $props();
  let query = $state('');
  let shown = $derived(
    items.filter((item) => item.title.toLowerCase().includes(query.trim().toLowerCase()))
  );
</script>

<label>Search the list <input type="search" bind:value={query}></label>
<p role="status">Shown: {shown.length} of {items.length}</p>

<div class="cards">
  {#each shown as item (item.title)}
    <ItemCard {item} />
  {:else}
    <p>Nothing found.</p>
  {/each}
</div>

Compare it with the events lesson: there you caught input by hand, looped over the cards, wrote card.hidden and updated the counter separately. Here the counter, the list and the empty screen are derived from two values: items and query. You can’t forget to update something.

Curly braces and blocks

In the markup, curly braces mean “JavaScript here”:

  • {expression} inserts a value. It is text, not markup: tags in the string stay letters, as with textContent. That is why {item.title} is safe for other people’s data. Only {@html string} is dangerous, because it inserts markup, like innerHTML: use it only for code you wrote entirely yourself;
  • {#if condition} ... {:else} ... {/if} shows a block on a condition;
  • {#each list as element (key)} ... {:else} ... {/each} repeats a block for every element of a list. The key in brackets helps Svelte understand which card belongs to which item when the list is filtered or reordered, so that state (such as whether “More” is open) isn’t lost. The {:else} branch is shown for an empty list: a ready-made “nothing found”;
  • onclick={function} attaches a handler, and bind:value={query} ties a field to state in both directions: type in the field and query changes; change query and the field shows the new value.

One more thing that is easy to miss: the styles in a component’s <style> apply by default only to that component. A card won’t spoil the styling of the rest of the page, and a rule that matches nothing is thrown out of the result.

What really happens

The browser doesn’t know what Svelte is. At build time the compiler turns every .svelte file into plain JavaScript: for each place where {item.title} depends on state, it writes a short command, “if this value changed, update this text node”. So almost no middleman library travels to the browser, and there is no comparing of the “old and new” tree as in React: exactly what depends on the changed value is updated.

Check it yourself. On the right is a live component on the same runes, and under it a log of what exactly changed in the page tree after each of your actions.

A component and what it updated
Code
let query = $state('');
let shown = $derived(
  items.filter((i) => i.title.includes(query))
);

<input bind:value={query}>
<p>Shown: {shown.length}</p>
{#each shown as item (item.title)}
  <article>…</article>
{/each}
Live result

Shown: 3 of 3

First offer
Second offer
Third offer
What changed in the page tree: 0 in all
  1. nothing yet: type a word or press “More”
Check yourself: how does $derived differ from $state?

$state holds a value that you change yourself. $derived is computed from other values and recalculates automatically, so you never assign to it by hand.

How to run it yourself

In our sandbox components aren’t built, because that needs a compiler. There are two simple ways to try them.

  • Open svelte.dev/playground, paste in the files App.svelte, ItemList.svelte and ItemCard.svelte from the practice, and you’ll see the page at once.
  • Create a project with the command npx sv create my-app (you’ll need Node.js). It sets up SvelteKit, which is Svelte together with routes by folders, preparing pages on the server and the Vite bundler. Then npm install and npm run dev.

One difference worth knowing in advance: in a plain project of Svelte and Vite alone, the page is empty until the script loads. In SvelteKit the page is rendered on the server and arrives in the browser as ready HTML, and the script then “brings it to life”. For a visitor without the script the difference is huge, and the next lesson is about it.

Practice

You’re on a 2019 site. Move the block of offers into components: the card ItemCard, the list with search ItemList and the assembly point App. The code is written as text in the fields of the block “The page’s components”: inside them you can use the < sign as it is.

The checks read the text of your components: whether there are $props, $state, $derived, a key in {#each} and an {:else} branch. Whether they work is best checked in the Svelte playground. While you’re at it, count honestly how many lines of code it took by hand (your four scripts on the page) and in components, and write what a visitor sees whose JavaScript didn’t load. It’s a very useful thought for the next lesson.

What was wrong before

Before version 5, Svelte was written differently: state was declared with an ordinary let, and reactivity was implied (a special $: label for computed values and special rules for assignment). It was short, but the magic sometimes failed: it worked only inside a component and didn’t carry over into ordinary files, and a newcomer found it hard to see why an assignment in one place is reactive and in another is not. In 2024 Svelte 5 arrived with runes: the same abilities, but written explicitly.

The idea that the view is derived from state and that changes travel along a chain of dependencies (it is called signals) came to almost every framework in the 2020s: Solid, Preact, Angular and Vue (where it is ref). Learn it in Svelte and you’ll read the others easily.

Practice

Preparing your page…

Test

Question 1 of 6

What does the line let count = $state(0) do in a Svelte 5 component?

One answer

Cards

These cards go into your review once you finish the lesson.

  • Which runes does Svelte 5 have, and what do they do?

    Answer

    $state: a reactive value. $derived: a value computed from others. $props: data from the parent. $effect: a side effect after a change. They are words the compiler understands.

  • How do components exchange data?

    Answer

    A parent passes data to its child through props (<Card {item} />), and the child takes it with $props(). Data flows down, and events flow up through handlers.

  • How do you show a list in Svelte, and why a key?

    Answer

    {#each items as item (item.id)} … {:else} empty {/each}. The key in brackets helps Svelte match elements with items when the list is filtered or reordered.

  • How does Svelte differ from React and Vue?

    Answer

    It is a compiler: at build time it turns a component into plain JavaScript that changes the right nodes directly. No heavy library travels to the browser, and there is no virtual DOM.

  • What is SvelteKit?

    Answer

    A meta-framework over Svelte: routes by folders, server rendering and pages prepared in advance, and a build on Vite. This textbook is built on it.

Sources

Videos

Runes in Svelte 5: reactivity with $state, $effect and $derived (Code Hub)

The video loads from youtube-nocookie.com only when you press play.

Open on the site

Next

You now know: A Svelte component.