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/>}andlist.map(...)→{#if}and{#each}template blocks. - Custom hooks → plain functions in a
.svelte.tsmodule. - 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.
<script lang="ts">
let name = "world";
</script>
<h1>Hello, {name}</h1><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.
<script lang="ts">
let { name }: { name: string } = $props();
</script>
<h1>Hello, {name}</h1><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 thenamefield 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 calledname, and it must be a string." In plain JavaScript you'd drop it and writelet { 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.
<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.
<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.
<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.
<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.
<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.
<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.
export function createToggle(initial = false) {
let on = $state(initial);
return {
get on() {
return on;
},
toggle: () => (on = !on),
};
}<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 = falseis 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$statevariable each time it's accessed, sot.onalways 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.
<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.