Anatomy of a Svelte file
Svelte is a compiler, not a runtime framework: a .svelte file is a single component (script, markup, and styles together) that compiles to plain JavaScript which updates the DOM directly, so no framework library ships to the browser.
File extensions: .svelte
Every part of the example below is labelled and explained. This page is one of 55 annotated tours on AnatomyOf, a free, open-source project by LunarWerx Studios.
What is inside a Svelte file
Comment
Markup comments use HTML syntax; script and style blocks use their own.
Comments in the markup section are ordinary HTML comments and are stripped from the compiled output rather than emitted into the DOM. Inside the <script> block the usual JavaScript comments apply, and inside <style> the usual CSS block comments. A few markup comments are meaningful to the compiler rather than decorative: <!-- svelte-ignore a11y_click_events_have_key_events --> silences a specific warning for the element that follows, and a comment starting with <!-- @component --> becomes the component's documentation, shown on hover in editors that support it.
Script block
The component's logic. Runs once per component instance.
Everything in <script> runs once when an instance of the component is created, and every top-level declaration is visible to the markup below it. There are no this, no render function, and no component object to export: the file itself is the component, and the compiler wires the two halves together. Adding lang="ts" switches the block to TypeScript. A second, rarer block written as <script module> runs once per module rather than once per instance, which makes it the place for constants or state genuinely shared by every instance of the component.
Props
Values passed in from a parent, read with the $props() rune.
let { name = 'world' } = $props() destructures the values the parent passed to this component, with a default for anything omitted. Because it is ordinary destructuring, renaming (let { class: className } = $props()) and collecting the rest (let { title, ...others } = $props()) work exactly as they do anywhere else in JavaScript. Props are read-only by default: assigning to one is a mistake the compiler will flag, because the parent owns that value. A prop declared $bindable() opts into two-way flow so the child can write back to it, which is what bind: on a component uses under the hood. Befo
Reactive state
A value the DOM tracks, declared with the $state() rune.
let count = $state(0) declares a variable whose reads are tracked and whose writes schedule an update. Nothing else is required: count++ in an event handler is enough for every place the markup mentions count to re-render, with no setter function and no immutable-update ceremony. Objects and arrays passed to $state() are deeply reactive, wrapped in a proxy so items.push(x) or user.name = y are observed just like a reassignment. $state.raw() opts out of that when a large object should only ever be replaced wholesale, and $state.snapshot() produces a plain, un-proxied copy for code (like structu
Derived value
A value computed from other state with $derived(), recalculated automatically.
let doubled = $derived(count * 2) declares a value defined by an expression rather than assigned by hand. Svelte records which reactive values the expression read and recomputes it lazily whenever one of them changes, so doubled can never drift out of sync with count. The expression should be pure. For anything needing statements, $derived.by(() => { ... }) takes a function and returns its result. Deriving is almost always preferable to writing to a second $state from an effect: it is declarative, it recomputes only when actually read, and it cannot produce the update loops that manual synchro
Effect
Side-effect code that re-runs after the DOM updates ($effect()).
$effect(() => { ... }) runs its function after the component mounts, then again whenever any reactive value it read has changed. Dependencies are tracked automatically from what the function actually touched on its last run, so there is no dependency array to keep in sync and no way for it to go stale. Effects are for reaching outside the component: setting document.title, drawing to a canvas, starting a subscription, talking to a non-Svelte library. Returning a function from the effect registers cleanup, which runs before the next execution and once more when the component is destroyed. Using
Markup
Everything outside <script> and <style> is the component's template.
The markup section is HTML, with no wrapping root element required and no separate template syntax to learn: an element is an element, an attribute is an attribute. Capitalized tags reference imported components (<Card title="Hi" />), and a handful of special elements such as <svelte:head>, <svelte:window>, and <svelte:boundary> reach outside the component tree. Text and attributes may be interpolated with curly braces, and the compiler turns the whole section into imperative DOM code that touches only the nodes an update actually affects. There is no virtual DOM and no diff at runtime, which
Expression
Curly braces embed a JavaScript expression in the markup.
{count} inserts the current value as text, and the same braces work in attributes (title={label}), where value={value} can be shortened to {value}. Any JavaScript expression is allowed, including calls and ternaries, and it is re-evaluated only when the reactive values it reads change. Interpolated text is inserted as text, never parsed as HTML, so markup in a string shows up literally rather than being injected into the page. {@html string} is the deliberate escape hatch for trusted HTML, and {@const} inside a block computes a local value for that block.
Event handler
A normal DOM attribute: onclick={handler}.
Event handlers are plain attributes whose value is a function, so onclick={add} and onclick={() => count++} both read exactly like the DOM property they compile to. There is no custom directive syntax and no special casing: anything the platform dispatches, from onclick to onpointermove to a custom element's own events, is written the same way. Svelte 5 replaced the older on:click directive with this form and, along with it, the modifier suffixes such as |preventDefault; the handler now does that work itself. Component-to-parent communication is likewise just a prop that happens to hold a func
Two-way binding
bind:value keeps an element and a variable in sync in both directions.
<input bind:value={draft} /> writes the element's value into draft on input and pushes changes to draft back into the element, replacing the handwritten handler-plus-attribute pair that form state otherwise requires. Bindings exist for most interactive elements and for a number of read-only measurements, such as bind:clientWidth or a media element's bind:currentTime. bind:this={el} captures the DOM node itself, which is the way to reach an element imperatively (to focus it, or hand it to a charting library). Components can be bound too, provided the prop on the receiving side was declared $bin
Logic block
Conditionals and loops in the markup: {#if}, {#each}, {#await}.
A block opens with {#if condition} or {#each items as item}, may branch with {:else} or {:else if}, and closes with a matching {/if} or {/each}. Because the block is part of the template rather than the script, the compiler knows exactly which nodes to create and destroy when the condition or the list changes. {#each items as item, i (item.id)} supplies both an index and a *key*: the parenthesized expression identifies each item so that reordering the list moves existing DOM nodes instead of rewriting them in place. {#await promise} renders pending, fulfilled, and rejected states inline, and {
Style block
CSS scoped to this component by default.
Rules in <style> apply only to this component's own markup. The compiler adds a generated class to the elements a selector matches and rewrites the selector to require it, so a bare button { ... } rule cannot reach a button rendered by any other component. Unused selectors are reported at build time rather than shipped. The :global(...) modifier deliberately opts a selector out of scoping, and a whole block can be escaped with :global { ... }. Because the styles are real CSS in a real stylesheet, media queries, custom properties, nesting, and keyframes all behave normally, no preprocessor or C
Official Svelte site · All languages on AnatomyOf
The interactive tour needs JavaScript. Enable it to hover a callout and trace it into the code.