Snippets & Render
Snippets: reusable chunks of markup
Section titled “Snippets: reusable chunks of markup”A snippet is a named block of markup you can render multiple times. Define it with {#snippet name(params)}…{/snippet} and render it with {@render name(args)}. It’s Svelte’s way to stay DRY inside a template.
<script> let items = $state([ { name: 'Coffee', price: 3 }, { name: 'Tea', price: 2 }, ]);</script>
{#snippet row(item)} <tr><td>{item.name}</td><td>${item.price}</td></tr>{/snippet}
<table> {#each items as item (item.name)} {@render row(item)} {/each}</table>The snippet row is defined once and rendered per item. Snippets take parameters like functions, so the same markup adapts to different data.
flowchart LR
def["{#snippet row(item)} ... {/snippet}"] --> render1["{@render row(a)}"]
def --> render2["{@render row(b)}"]
render1 --> out["markup instances"]
render2 --> out Snippet parameters, including optional ones
Section titled “Snippet parameters, including optional ones”A snippet’s parameters work exactly like a function’s — including default values for parameters the caller might not pass:
<script lang="ts"> let names: (string | undefined)[] = ['Ada', undefined];</script>{#snippet greeting(name = 'friend')} <p data-testid="g">Hello, {name}!</p>{/snippet}{#each names as n} {@render greeting(n)}{/each}import { test, expect } from 'vitest';import { render } from '@testing-library/svelte';import OptionalParam from './OptionalParam.svelte';
test('a snippet parameter can have a default value, used when the argument is undefined', () => { const { container } = render(OptionalParam); const texts = [...container.querySelectorAll('[data-testid="g"]')].map((el) => el.textContent); expect(texts).toEqual(['Hello, Ada!', 'Hello, friend!']);});✓ a snippet parameter can have a default value, used when the argument is undefinedgreeting(n) with n === undefined falls back to 'friend', exactly like a JS default parameter — because that’s precisely what it compiles to.
Forgetting {@render}: a real runtime error
Section titled “Forgetting {@render}: a real runtime error”Writing a snippet’s name as a plain call — {row(item)} — instead of {@render row(item)} compiles without any error or warning, but throws at runtime the moment that line actually executes:
<script lang="ts"> let items = [1, 2, 3];</script>{#snippet row(item: number)} <li data-testid="row">{item}</li>{/snippet}<ul> {#each items as item} {row(item)} {/each}</ul>import { test, expect } from 'vitest';import { render } from '@testing-library/svelte';import NoRender from './NoRender.svelte';
test('calling a snippet without {@render} throws at runtime', () => { expect(() => render(NoRender)).toThrowError(/invalid_snippet_arguments/);});✓ calling a snippet without {@render} throws at runtimeThe real error: invalid_snippet_arguments — “A snippet function was passed invalid arguments. Snippets should only be instantiated via {@render ...}” (svelte.dev/e/invalid_snippet_arguments). Under the hood a snippet compiles to a function whose first parameter is an internal DOM anchor supplied by {@render} — calling it directly passes your own argument (item, an array index) into that slot instead, which is what trips the check. Since neither the compiler nor svelte-check catches this (it’s a template-shape mistake, not a type error), this is exactly the kind of thing to check for when reading generated code — see below.
Snippets replaced slots
Section titled “Snippets replaced slots”In Svelte 4, you passed markup into a component with <slot> (and named slots via <slot name="x">). Svelte 5 replaced that entirely with snippets — they’re more powerful because they can take parameters and are just values you pass around.
<!-- Svelte 4 (old): a component defined <slot /> and <slot name="header" /> --><!-- Svelte 5 (new): a component renders {@render children()} and {@render header()} -->If you’re reading older tutorials that use <slot>, the modern equivalent is a snippet rendered with {@render}. The next lesson covers exactly how a parent passes snippets (including the special children snippet) into a child.
<slot> still compiles and works in a runes-mode component today (Svelte 5.57.1) — it’s backward-compatible, not removed — but the compiler flags it with a real deprecation warning:
Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags insteadhttps://svelte.dev/e/slot_element_deprecatedSnippets are values
Section titled “Snippets are values”Because a snippet is a value, you can pass it to another snippet, store it in a variable, or choose between snippets conditionally:
{#snippet loading()}<p>Loading…</p>{/snippet}{#snippet ready(data)}<p>{data}</p>{/snippet}
{#if isLoading} {@render loading()}{:else} {@render ready(result)}{/if}This composability — snippets as first-class, parameterized values — is why they replaced the more rigid slot mechanism.
Mental Model
Section titled “Mental Model”A snippet is a function that renders markup — {#snippet name(params)} is the declaration, {@render name(args)} is the call. Everything about ordinary functions applies: parameters, defaults, closures over outer variables, passing them around as values. The one thing that’s not like an ordinary function is calling it directly ({row(item)}) — the framework, not your code, supplies the real first argument (the DOM anchor) when you go through {@render}, so bypassing {@render} hands your own value into a slot meant for Svelte’s internals.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
Snippets over <slot> | Parameters, first-class values, composable (proven in Snippets are values) | One more concept to learn if you’re coming from Svelte 4 tutorials |
| Snippet default parameters | Caller can omit an argument cleanly, proven above | Easy to forget the default only applies to undefined, not null/0/'' |
<slot> backward-compat | Old code and tutorials keep working unmodified | Deprecated (real warning, proven above) — mixing it into new runes-mode code reads as outdated |
Common Misconceptions
Section titled “Common Misconceptions”- “Forgetting
{@render}gives a compile error, like a type mistake would.” It doesn’t — proven above,{row(item)}compiles cleanly and only fails once that template line actually runs, withinvalid_snippet_arguments. - “
<slot>in a Svelte 5 component is a hard error.” It isn’t — proven above, it still compiles and renders correctly; it’s a deprecation warning, not a removal. - “A snippet with a default parameter behaves like a component prop default.” Similar idea, different mechanism — a snippet’s defaults are plain JS default-parameter semantics evaluated at
{@render}time, not$props()’s destructuring.
Reading AI-generated code
Section titled “Reading AI-generated code”When reviewing generated snippet code, check for:
- Is a
{#snippet ...}referenced as{name(args)}anywhere instead of{@render name(args)}? It will compile silently and only fail at runtime, proven above — grep for the snippet’s name and confirm every use goes through{@render}. - Is
<slot>used inside a component that otherwise uses runes ($state,$props(), etc.)? It still works (proven above), but mixing Svelte 4’s slot with Svelte 5 runes in the same file is inconsistent and a real deprecation warning — should be a snippet-basedchildren/named-snippet prop instead. - Does a snippet’s default parameter assume it covers “falsy” callers (
0,'',false), when it only actually triggers forundefined? - Is a snippet defined but never rendered anywhere (dead markup, or a typo in the name passed to
{@render})?
🐛 Spot the bug
<script>let count = $state(0);</script>
<div>{count}</div>
<slot />
<button onclick={() => count++}>+1</button>Show the answer
This component mixes runes ($state) with the Svelte 4 <slot> element. It still compiles and renders parent content correctly — <slot> is backward-compatible, not removed — but the compiler emits a real deprecation warning: Using <slot> to render parent content is deprecated. Use {@render ...} tags instead (proven above). Generated code that mixes eras like this is a signal the model was trained on a mix of Svelte 4 and 5 examples. The fix: give the component a children prop via $props() and render it with {@render children()}, matching the rest of the file’s runes style.