Skip to content

Snippets & Render

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
Define once, render many times

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:

src/lib/OptionalParam.svelte
<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}
src/lib/optionalparam.test.ts
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 undefined

greeting(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:

src/lib/NoRender.svelte
<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>
src/lib/norender.test.ts
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 runtime

The 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.

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 instead
https://svelte.dev/e/slot_element_deprecated

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.

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.

What you getBenefitCost
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 parametersCaller can omit an argument cleanly, proven aboveEasy to forget the default only applies to undefined, not null/0/''
<slot> backward-compatOld code and tutorials keep working unmodifiedDeprecated (real warning, proven above) — mixing it into new runes-mode code reads as outdated
  • “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, with invalid_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.

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-based children/named-snippet prop instead.
  • Does a snippet’s default parameter assume it covers “falsy” callers (0, '', false), when it only actually triggers for undefined?
  • 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.

How do you define and render a snippet?
What did snippets replace in Svelte 5?
What happens when this line runs?
{#snippet row(item)}
  <li>{item}</li>
{/snippet}
{row(1)}
Can a snippet parameter have a default value?
What is true about <slot> in a runes-mode Svelte 5 component?
<script>
  let count = $state(0);
</script>
<slot />