Control Flow
Svelte uses template syntax for control flow rather than JavaScript expressions inside JSX. Conditionals, loops, and async handling are built into the template language with dedicated block syntax. This makes templates readable and avoids the awkward patterns that JSX requires for branching and iteration.
{#if}/{:else if}/{:else} Conditionals
<script>
let user = $state(null);
let loading = $state(true);
let error = $state(null);
</script>
{#if loading}
<p>Loading...</p>
{:else if error}
<p class="error">Something went wrong: {error.message}</p>
{:else if user}
<h1>Welcome, {user.name}</h1>
{:else}
<p>No user found.</p>
{/if}
Blocks can be nested:
{#if user}
<h1>{user.name}</h1>
{#if user.role === 'admin'}
<button>Admin Panel</button>
{/if}
{/if}
The condition is any JavaScript expression. It is evaluated reactively: when any reactive value in the expression changes, the block re-evaluates and the DOM updates.
{#each} for Lists
The {#each} block iterates over arrays. Always provide a key when the list can change, so Svelte can efficiently update the DOM.
<script>
let todos = $state([
{ id: 1, text: 'Buy groceries', done: false },
{ id: 2, text: 'Clean house', done: true },
{ id: 3, text: 'Write code', done: false }
]);
</script>
<ul>
{#each todos as todo (todo.id)}
<li class:done={todo.done}>
<input type="checkbox" bind:checked={todo.done}>
{todo.text}
</li>
{/each}
</ul>
<style>
.done { text-decoration: line-through; opacity: 0.6; }
</style>
The (todo.id) after the iteration variable is the key expression. It tells Svelte which DOM nodes correspond to which items when the array is reordered, filtered, or modified.
Destructuring & Index
{#each entries as { title, body }, index (title)}
<article>
<h2>{index + 1}. {title}</h2>
<p>{body}</p>
</article>
{/each}
Empty State with {:else}
{#each results as result (result.id)}
<div class="result">{result.name}</div>
{:else}
<p>No results found.</p>
{/each}
The {:else} block renders when the array is empty. This is a common pattern for search results, filtered lists, and data tables.
{#await} for Promises
{#await} handles the three states of a promise directly in the template: pending, fulfilled, and rejected.
<script>
let dataPromise = $state(fetchUserData());
async function fetchUserData() {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Failed to load user');
return response.json();
}
function refresh() {
dataPromise = fetchUserData();
}
</script>
{#await dataPromise}
<p>Loading user data...</p>
{:then user}
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onclick={refresh}>Refresh</button>
{:catch error}
<p class="error">{error.message}</p>
<button onclick={refresh}>Retry</button>
{/await}
Short Form
If you do not need the loading state:
{#await dataPromise then user}
<h1>{user.name}</h1>
{/await}
If you only care about the resolved value and want to ignore errors:
{#await dataPromise then user}
<p>Hello, {user.name}</p>
{/await}
{#await} is useful for component-level data fetching, but in SvelteKit applications, prefer loading data in +page.ts or +page.server.ts load functions instead.
{@html} for Raw HTML
{@html} renders a string as raw HTML. Svelte does not escape the content, so use it carefully.
<script>
let markdown = $state('');
let rendered = $derived(markdownToHtml(markdown));
</script>
<textarea bind:value={markdown}></textarea>
<div class="preview">
{@html rendered}
</div>
The content is inserted into the DOM without sanitization. If the HTML comes from user input, you must sanitize it to prevent XSS attacks:
<script>
import DOMPurify from 'dompurify';
let { content } = $props();
let safeHtml = $derived(DOMPurify.sanitize(content));
</script>
{@html safeHtml}
Use {@html} for:
- Rendered markdown.
- Content from a CMS that includes HTML formatting.
- SVG strings generated dynamically.
Never use it for user-provided content without sanitization.
{#snippet} for Reusable Template Blocks
Snippets define reusable chunks of template within a component. They replace the need for extracting small pieces of markup into separate components.
<script>
let users = $state([
{ name: 'Alice', role: 'admin', active: true },
{ name: 'Bob', role: 'user', active: true },
{ name: 'Charlie', role: 'user', active: false }
]);
</script>
{#snippet userRow(user)}
<tr class:inactive={!user.active}>
<td>{user.name}</td>
<td>{user.role}</td>
<td>{user.active ? 'Active' : 'Inactive'}</td>
</tr>
{/snippet}
<table>
<thead>
<tr><th>Name</th><th>Role</th><th>Status</th></tr>
</thead>
<tbody>
{#each users as user (user.name)}
{@render userRow(user)}
{/each}
</tbody>
</table>
<style>
.inactive { opacity: 0.5; }
</style>
Snippets accept parameters and are rendered with {@render}. They are scoped to the component they are defined in.
Passing Snippets as Props
Snippets can be passed to child components, enabling powerful composition patterns:
<!-- DataTable.svelte -->
<script>
let { items, row, header } = $props();
</script>
<table>
{#if header}
<thead>{@render header()}</thead>
{/if}
<tbody>
{#each items as item (item.id)}
{@render row(item)}
{/each}
</tbody>
</table>
<!-- Parent.svelte -->
<script>
import DataTable from './DataTable.svelte';
let products = $state([
{ id: 1, name: 'Widget', price: 9.99 },
{ id: 2, name: 'Gadget', price: 24.99 }
]);
</script>
<DataTable items={products}>
{#snippet header()}
<tr><th>Product</th><th>Price</th></tr>
{/snippet}
{#snippet row(product)}
<tr>
<td>{product.name}</td>
<td>${product.price.toFixed(2)}</td>
</tr>
{/snippet}
</DataTable>
{#key} for Forcing Re-renders
{#key} destroys and recreates its contents when the expression changes. This is useful when you need a component to fully reset rather than update in place.
<script>
let userId = $state(1);
</script>
<button onclick={() => userId++}>Next User</button>
{#key userId}
<UserProfile id={userId} />
{/key}
Without {#key}, changing userId would update the existing UserProfile component. With {#key}, the old instance is destroyed and a new one is created. This ensures onMount runs again, internal state resets, and transitions replay.
Common uses:
- Resetting form state when switching between items.
- Replaying entry animations.
- Forcing third-party components to reinitialize.
{#key selectedTab}
<div in:fade={{ duration: 200 }}>
<TabContent tab={selectedTab} />
</div>
{/key}
Combining Control Flow Blocks
Control flow blocks compose naturally:
<script>
let filter = $state('all');
let items = $state([
{ id: 1, name: 'Task A', status: 'active' },
{ id: 2, name: 'Task B', status: 'completed' },
{ id: 3, name: 'Task C', status: 'active' }
]);
let filtered = $derived(
filter === 'all' ? items : items.filter(i => i.status === filter)
);
</script>
<select bind:value={filter}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
{#if filtered.length > 0}
<ul>
{#each filtered as item (item.id)}
<li>
{item.name}
{#if item.status === 'completed'}
<span class="check">Done</span>
{/if}
</li>
{/each}
</ul>
{:else}
<p>No items match the current filter.</p>
{/if}
Common Pitfalls
- Forgetting keys on {#each}: Without a key, Svelte uses index-based updates. This causes bugs when items are reordered, inserted, or removed: event handlers and component state get attached to the wrong items. Always use a stable, unique key.
- Using {@html} with unsanitized input: This is an XSS vulnerability. Always sanitize user-provided HTML with a library like DOMPurify.
- Overusing {#key}: Destroying and recreating components is expensive. Only use
{#key}when you genuinely need a full reset. In most cases, reactive updates handle changes correctly without it. - Complex logic in templates: If your conditional has five nested levels, extract the logic into a
$derivedvalue or break the template into smaller components. Templates should be readable. - Using {#await} for SvelteKit page data: In SvelteKit, load functions in
+page.tshandle data fetching with better SSR support, streaming, and error handling. Reserve{#await}for client-side fetches within components.
Key Takeaways
{#if}/{:else if}/{:else}handles conditional rendering. Conditions are reactive expressions.{#each}iterates arrays. Always provide a key for lists that can change.{#await}handles promise states (pending, fulfilled, rejected) directly in the template.{@html}renders raw HTML. Sanitize user-provided content to prevent XSS.{#snippet}and{@render}create reusable template blocks within and across components.{#key}forces full destruction and recreation when a value changes. Use sparingly.