LCM Logo
Web Development

Svelte

A concept-first tour of Svelte — components, props, state, derived values, and effects — each idea explained before the code.

Svelte is a tool for building user interfaces out of components: small, reusable pieces of UI that describe what the screen should look like for a given set of data. Unlike most frameworks, Svelte is a compiler — it turns your components into small, fast vanilla JavaScript at build time, so there's no virtual DOM and very little runtime. The examples here use Svelte 5 and its modern reactivity system, runes.

Svelte on its own handles the UI layer. For routing, data fetching, and a production build, it's usually paired with a framework — see the SvelteKit guide, which builds directly on these concepts.

Coming from React?

Most concepts map across, but the spelling differs:

  • useState$state, and you assign (count++) instead of calling a setter.
  • useMemo / computed values → $derived.
  • useEffect$effect, with no dependency array (Svelte tracks dependencies for you).
  • Props (function C({ name })) → let { name } = $props().
  • JSX {cond ? <A/> : <B/>} and list.map(...){#if} and {#each} template blocks.
  • Custom hooks → plain functions in a .svelte.ts module.
  • Controlled inputs (value + onChange) → bind:value.

The biggest shift: there's no virtual DOM and no re-running of the whole component. Svelte compiles your assignments into targeted DOM updates, so plain = is the whole reactivity story.

Components and markup

A component lives in a .svelte file and is mostly just HTML. A <script> block holds the component's logic, and the markup below it is plain HTML with {expressions} in curly braces. There's no return, no JSX — what you write is close to what the browser gets.

Greeting.svelte
<script lang="ts">
  let name = "world";
</script>

<h1>Hello, {name}</h1>
App.svelte
<script lang="ts">
  import Greeting from "./Greeting.svelte";
</script>

<Greeting />

Props: passing data in

Props are the inputs to a component — values passed from parent to child, just like attributes on an HTML element. In Svelte 5 you declare them with the $props rune, destructuring the fields you expect.

Greeting.svelte
<script lang="ts">
  let { name }: { name: string } = $props();
</script>

<h1>Hello, {name}</h1>
App.svelte
<Greeting name="Ada" /> <!-- renders "Hello, Ada" -->

That declaration packs two jobs into one line:

  • { name } is JavaScript destructuring. $props() returns an object of all the props passed in — here { name: "Ada" } — and { name } reaches into it and pulls the name field out into its own variable. To give a prop a default, write { name = "world" }.
  • : { name: string } is a TypeScript type annotation. It isn't code that runs; it tells the type checker "this component expects a prop called name, and it must be a string." In plain JavaScript you'd drop it and write let { name } = $props();.

State: data that changes over time

State is data a component owns and can change in response to interaction. The $state rune makes a variable reactive: assign to it with plain =, and Svelte updates every part of the UI that reads it. There's no setter function — assignment is the update.

Counter.svelte
<script lang="ts">
  let count = $state(0);
</script>

<button onclick={() => count++}>
  Clicked {count} times
</button>

This is the big difference from React: count++ is the state update. Svelte's compiler rewrites that assignment into the code that re-renders the button, so you work with ordinary variables instead of a [value, setValue] pair. The same goes for objects and arrays — push to a reactive array or set a property and the UI follows.

Handling events

Event handlers are functions you attach with on-prefixed attributes like onclick or oninput. They receive a normal DOM event and typically update state, which re-renders the affected markup.

Toggle.svelte
<script lang="ts">
  let on = $state(false);
</script>

<button onclick={() => (on = !on)}>{on ? "ON" : "OFF"}</button>

The on = !on part is worth a beginner's pause: !on is JavaScript's "not" operator flipping the boolean, so true becomes false and back again. Because on is $state, the plain assignment is all Svelte needs to update the button text — no function form, no setter.

Rendering lists

To render a collection, use an {#each} block. Give each item a unique key in parentheses — a real id where you can — so Svelte can track items efficiently across updates.

PenguinList.svelte
<script lang="ts">
  let { penguins }: { penguins: { id: number; species: string }[] } = $props();
</script>

<ul>
  {#each penguins as p (p.id)}
    <li>{p.species}</li>
  {/each}
</ul>

Conditional rendering

Svelte uses {#if} blocks for conditional markup, with optional {:else if} and {:else} branches. Unlike React's ternaries, this is real template syntax, so the markup stays readable.

Status.svelte
<script lang="ts">
  let { loading, count }: { loading: boolean; count: number } = $props();
</script>

{#if loading}
  <p>Loading…</p>
{:else}
  <p>{count} results</p>
{/if}

{#if count === 0}
  <p>No matches.</p>
{/if}

Derived values

When a value can be computed from other state, don't store it separately — derive it. The $derived rune recalculates automatically whenever the state it reads changes, so it's always in sync and never goes stale.

Doubler.svelte
<script lang="ts">
  let count = $state(2);
  let doubled = $derived(count * 2);
</script>

<button onclick={() => count++}>
  {count} doubled is {doubled}
</button>

doubled is read-only — you never assign to it. Each time count changes, Svelte re-runs the expression. This is the rune you'll reach for far more often than $effect: prefer deriving a value over computing it inside an effect.

Side effects with $effect

Most reactivity in Svelte is handled by $state and $derived. For the rest — anything that reaches outside the component (timers, subscriptions, manual DOM work, logging) — use the $effect rune. It re-runs whenever the reactive values it reads change, and you return a cleanup function to undo the effect.

Clock.svelte
<script lang="ts">
  let now = $state(new Date());

  $effect(() => {
    const id = setInterval(() => (now = new Date()), 1000);
    return () => clearInterval(id); // cleanup when the component is destroyed
  });
</script>

<time>{now.toLocaleTimeString()}</time>

Notice there's no dependency array: Svelte tracks dependencies automatically by watching which reactive values the effect reads. Reach for $effect only when you genuinely need to step outside Svelte — for deriving data, use $derived instead.

Reusing logic with .svelte.ts modules

When several components need the same reactive logic, move it into a .svelte.ts module — an ordinary TypeScript file where runes are allowed. Export a function that creates and returns the state, and each caller gets its own independent copy.

toggle.svelte.ts
export function createToggle(initial = false) {
  let on = $state(initial);
  return {
    get on() {
      return on;
    },
    toggle: () => (on = !on),
  };
}
Panel.svelte
<script lang="ts">
  import { createToggle } from "./toggle.svelte.ts";
  const t = createToggle();
</script>

<button onclick={t.toggle}>{t.on ? "Hide" : "Show"}</button>

Two pieces are easy to trip over:

  • initial = false is a default parameter: createToggle() starts off, createToggle(true) starts on.
  • The get on() getter is what keeps reactivity working across the module boundary. If you returned { on } directly, you'd copy the boolean's value once and lose the live connection. A getter re-reads the $state variable each time it's accessed, so t.on always reflects the current value.

Two-way binding for forms

Svelte's bind:value makes an input and a state variable mirror each other automatically — type in the box and the variable updates, change the variable and the box updates. It replaces the value/onChange pair you'd write by hand elsewhere.

NameField.svelte
<script lang="ts">
  let name = $state("");
</script>

<input bind:value={name} />
<p>Hello, {name || "stranger"}</p>

Runes only run where Svelte compiles

Runes like $state, $derived, and $effect aren't imported — they're keywords the Svelte compiler recognizes inside .svelte files and .svelte.ts/.svelte.js modules. Use a rune in a plain .ts file and it won't work, because that file never goes through the compiler. This is the detail that most often trips up newcomers moving logic out of components.

On this page