Template Logic
Svelte markup is HTML plus a small set of block tags - {#if}, {#each}, {#await} - and the
bind: directive. Where React uses JavaScript expressions (map, ternaries) and Vue uses
attributes (v-if, v-for), Svelte gives control flow its own syntax, opened with {#...} and
closed with {/...}. Five minutes of syntax, then the gotchas that actually matter.
Branching: {#if}
{#if cart.length === 0}
Your cart is empty.
{:else if cart.length < 10}
{cart.length} items.
{:else}
Bulk order!
{/if}
A false branch unmounts its contents - DOM removed, component state inside destroyed, same
semantics as conditional rendering everywhere. There's no built-in "hide with CSS instead"
directive (Vue's v-show); when you need state to survive a frequent toggle, keep the element and
toggle a class: <div class:hidden={!open}> (that class: shorthand toggles a class by boolean -
you'll use it constantly).
Repeating: {#each} and the key
{#each todos as todo (todo.id)}
{todo.text}
{:else}
Nothing to do. Suspicious.
{/each}
What just happened: one <li> per item, with two pieces of syntax worth naming. The
parenthesized (todo.id) is the key expression - the same identity contract as every
framework: it's how Svelte matches old items to new ones when the array changes, so a reorder
moves DOM instead of rewriting every row. And {:else} inside an each block renders when the
array is empty - the empty-state pattern built into the syntax.
⚠️ Gotcha: the key is syntactically optional, and that's a trap: without it, Svelte updates
each-block rows by position. Delete the first todo and every row's DOM shifts contents up one -
fine for pure text, state-corrupting the moment rows hold inputs, checkboxes, or component state
(the typed text stays in row one while the labels shift). If the list can ever reorder, insert, or
delete: key it with a stable id. (index) as the key is the same bug with extra steps.
Promises in markup: {#await}
The block with no direct equivalent in React or Vue:
{#await productPromise}
Loading…
{:then product}
{product.name}
{:catch error}
Couldn't load: {error.message}
{/await}
What just happened: the three states of a promise - pending, resolved, rejected - each get a
branch, right in the markup. No loading flag, no error state variable, no effect: hand the block a
promise and it renders the appropriate branch as the promise settles. Refetching is reassigning:
productPromise = fetchProduct(newId) swaps in a new promise and the block starts over at
pending. (For fetch-on-navigation, SvelteKit's load functions - phase 8 - are the fuller answer;
{#await} covers the in-component cases.)
Two-way forms: bind:
I agree
Free
Pro
Signing up {email || '…'} for the {plan} plan.
Sign up
What just happened: bind:value wires the input to the state variable in both directions -
value in, keystrokes back. Checkboxes bind checked, selects bind value, and
<input type="number"> binds through bind:value with the string-to-number coercion handled.
Validation is then just expressions over state, as the disabled button shows.
bind: reaches beyond forms, worth knowing exists: bind:this={el} gives you the DOM element
itself (Svelte's ref mechanism, for focus management or third-party libraries), and read-only
bindings like bind:clientWidth observe layout without a ResizeObserver in sight.
Events, for completeness, are plain attributes in the runes dialect: onclick={handler},
onsubmit={save}. One habit transfers from every framework: pass the function. To preventDefault,
wrap it - onsubmit={e => { e.preventDefault(); save(); }} (the old dialect's |preventDefault
modifier is gone).
Recap
{#if}unmounts on false; for state-preserving toggles useclass:and CSS.{#each list as item (item.id)}- the key is optional syntax but mandatory practice for any list that changes shape;{:else}handles empty.{#await}renders a promise's three states declaratively; reassign the promise to restart.bind:value/bind:checkedare two-way form wiring;bind:thisis the element ref.- Events are attributes (
onclick); no modifiers - callpreventDefaultyourself.
[
{
"q": "A todo list's rows contain checkboxes. Deleting the first todo makes the wrong rows show as checked. The each block has no key expression. What happened?",
"choices": [
"The delete handler mutated the array, which Svelte doesn't track",
"Without a key, rows update by position - the DOM (and its checkbox state) stayed put while item contents shifted up",
"Checkbox state needs bind:checked to survive deletions",
"The {:else} branch interfered with row matching"
],
"answer": 1,
"why": [
"Mutation is fine in Svelte - $state proxies track push/splice; the update happened, just positionally.",
null,
"bind:checked wires state to a row - but without keys, the row itself is reassigned to a different item.",
"{:else} only renders when the list is empty; it plays no role in matching."
],
"explain": "Unkeyed each blocks match old and new items by index. Row DOM gets reused for whatever item now sits at that index - add (todo.id) so identity follows the item."
},
{
"q": "What does the {#await} block replace from the classic client-side fetch pattern?",
"choices": [
"The fetch call itself",
"The loading flag, error state, and the conditional rendering between them",
"The need for async functions",
"Request cancellation"
],
"answer": 1,
"why": [
"You still create the promise - the block consumes it.",
null,
"Async code is still async - the block just renders its states.",
"Cancellation still needs your own handling (or a data layer) - the block renders whatever promise it's given."
],
"explain": "Pending, resolved, and rejected each get a markup branch, so the three state variables and their if/else chain disappear. The promise is the state."
}
]
← Phase 2: Runes: State That Compiles · Guide overview · Phase 4: Components: Props, Callbacks, and Snippets →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. A todo list's rows contain checkboxes. Deleting the first todo makes the wrong rows show as checked. The each block has no key expression. What happened?
2. What does the {#await} block replace from the classic client-side fetch pattern?