Full Stack SvelteKit, a full and comprehensive video course that will teach you how to build and deploy full stack web applications using SvelteKit. Full Stack SvelteKit

Svelte snippets: the new way to reuse markup in Svelte 5

By Justin Ahinon.

Last updated

If you're writing Svelte 5, snippets are how you reuse markup. They replace slots, and they cover a lot of the cases where you'd otherwise spin up a helper component just to pass some UI around.

This post walks through what snippets are, how to use them with {@render ...}, how to type them, and the patterns that come up over and over in real code. If you're migrating from slots, there's a section at the end pointing you to the full walkthrough.

What snippets are

A snippet is a named piece of markup you can render anywhere in your template. You declare it with {#snippet ...} and render it with {@render ...}.

{#snippet greeting(name)}
	<p>Hello, {name}!</p>
{/snippet}

{@render greeting('Justin')}
{@render greeting('Ada')}

That's the whole idea. Snippets are functions that return markup. You define them once, call them with arguments, and Svelte renders the result.

The interesting part is what that lets you do: pass snippets to child components (replacing slots), compose them recursively, export them from other files, type them with generics. We'll get to each of those in turn.

Declaring and rendering snippets

The {#snippet name(params)}...{/snippet} block can go anywhere in your template. Parameters work like a regular function signature. You can have any number of them, destructure them, and set defaults:

{#snippet card({ title, body = 'No content' })}
	<article>
		<h3>{title}</h3>
		<p>{body}</p>
	</article>
{/snippet}

{@render card({ title: 'Hello' })}

To render a snippet, use {@render snippetName(args)}. The expression inside {@render ...} can be any JavaScript that resolves to a snippet:

{@render (isAdmin ? adminPanel : userPanel)()}

Passing snippets to components

Most of the time, you'll pass snippets to a child component rather than render them in the same file. There are three patterns for this.

The children prop (default-slot replacement)

Any content you put inside a component's tags that isn't itself a snippet declaration becomes the children prop:

<!-- parent.svelte -->
<script lang="ts">
	import Button from './button.svelte';
</script>

<Button>click me</Button>
<!-- button.svelte -->
<script lang="ts">
	import type { Snippet } from 'svelte';

	let { children }: { children: Snippet } = $props();
</script>

<button class="btn">
	{@render children()}
</button>

This is the direct replacement for the default <slot />. If your component only needs one content placeholder, use children.

Named snippets as explicit props

Snippets are values, so you can pass them as props like anything else:

<!-- parent.svelte -->
<script lang="ts">
	import Table from './table.svelte';

	type Fruit = { name: string; qty: number };

	const fruits: Fruit[] = [{ name: 'apples', qty: 5 }];
</script>

{#snippet header()}
	<th>Fruit</th>
	<th>Qty</th>
{/snippet}

{#snippet row(fruit: Fruit)}
	<td>{fruit.name}</td>
	<td>{fruit.qty}</td>
{/snippet}

<Table data={fruits} {header} {row} />

The child component receives them as regular props and renders them:

<!-- table.svelte -->
<script lang="ts">
	import type { Snippet } from 'svelte';

	type Fruit = { name: string; qty: number };

	let {
		data,
		header,
		row
	}: {
		data: Fruit[];
		header: Snippet;
		row: Snippet<[Fruit]>;
	} = $props();
</script>

<table>
	<thead><tr>{@render header()}</tr></thead>
	<tbody>
		{#each data as item}
			<tr>{@render row(item)}</tr>
		{/each}
	</tbody>
</table>

Named snippets as implicit props

You can also declare the snippets inside the component tags. Svelte will treat them as props on that component automatically:

<Table data={fruits}>
	{#snippet header()}
		<th>Fruit</th>
		<th>Qty</th>
	{/snippet}

	{#snippet row(fruit: Fruit)}
		<td>{fruit.name}</td>
		<td>{fruit.qty}</td>
	{/snippet}
</Table>

The table.svelte component is identical to the explicit version. It receives header and row as props either way. The implicit form is the one you'll reach for most, because it keeps the snippets close to where they're used.

Passing data back to the parent

Slots had let: for passing data from child to parent. Snippets solve this more naturally: the child calls the snippet with arguments, the parent declares the parameters.

In the table example above, notice that the child does {@render row(item)}. It passes each item to the row snippet. On the parent side:

{#snippet row(fruit: Fruit)}
	<td>{fruit.name}</td>
{/snippet}

fruit is just the parameter name. Whatever the child passes will be bound to it. No let: directive, and no scope confusion. Just function arguments.

A complete example: a typed DataTable

Here's a DataTable component that brings together the patterns above: required and optional snippets, a fallback for the empty state, and full TypeScript types.

<!-- data-table.svelte -->
<script lang="ts" generics="T">
	import type { Snippet } from 'svelte';

	let {
		data,
		row,
		header,
		empty
	}: {
		data: T[];
		row: Snippet<[T]>;
		header?: Snippet;
		empty?: Snippet;
	} = $props();
</script>

{#if data.length === 0}
	{#if empty}
		{@render empty()}
	{:else}
		<p>No data to display.</p>
	{/if}
{:else}
	<table>
		{#if header}
			<thead><tr>{@render header()}</tr></thead>
		{/if}
		<tbody>
			{#each data as item}
				<tr>{@render row(item)}</tr>
			{/each}
		</tbody>
	</table>
{/if}

Using it looks like this:

<script lang="ts">
	import DataTable from './data-table.svelte';

	const fruits = [
		{ name: 'Apples', qty: 5, price: 2 },
		{ name: 'Bananas', qty: 10, price: 1 }
	];
</script>

<DataTable data={fruits}>
	{#snippet header()}
		<th>Fruit</th>
		<th>Qty</th>
		<th>Price</th>
	{/snippet}

	{#snippet row(fruit)}
		<td>{fruit.name}</td>
		<td>{fruit.qty}</td>
		<td>${fruit.price}</td>
	{/snippet}

	{#snippet empty()}
		<p>No fruit in stock.</p>
	{/snippet}
</DataTable>

The generic <T> means row's parameter is typed correctly. Inside the snippet, fruit autocompletes with name, qty, and price. You'd write this component once and reuse it across the app.

What snippets can do that slots couldn't

A few capabilities that aren't obvious until you need them.

Snippets can be declared anywhere in the template. Not just at the component boundary. You can put one inside an {#if} block, inside an {#each}, or at the top of the file. They're visible to everything in the same lexical scope and its children.

Snippets can reference themselves and each other. That means recursion works out of the box:

{#snippet blastoff()}
	<span>🚀</span>
{/snippet}

{#snippet countdown(n: number)}
	{#if n > 0}
		<span>{n}...</span>
		{@render countdown(n - 1)}
	{:else}
		{@render blastoff()}
	{/if}
{/snippet}

{@render countdown(3)}

Snippets are first-class values. You can assign them to variables, store them in arrays, pass them through functions. A slot was a template construct; a snippet is a JavaScript value that happens to produce markup.

Optional snippets and fallback content

When a snippet prop might not be provided, optional chaining is the simplest path:

<script lang="ts">
	import type { Snippet } from 'svelte';

	let { children }: { children?: Snippet } = $props();
</script>

{@render children?.()}

If you want fallback content, use an {#if} block:

<script lang="ts">
	import type { Snippet } from 'svelte';

	let { children }: { children?: Snippet } = $props();
</script>

{#if children}
	{@render children()}
{:else}
	<p>Nothing to show.</p>
{/if}

There's no dedicated "fallback" syntax like there was for slots. {#if ... {:else} ...} is the idiom.

TypeScript with snippets

Snippets implement the Snippet interface from the svelte package:

<script lang="ts">
	import type { Snippet } from 'svelte';

	interface Props {
		data: unknown[];
		children: Snippet;
		row: Snippet<[unknown]>;
	}

	let { data, children, row }: Props = $props();
</script>

The type parameter is a tuple because snippets can have multiple parameters. Snippet<[string, number]> types a snippet with two parameters.

For components that work with arbitrary data types, like the DataTable above, declare a generic on the <script> tag:

<script lang="ts" generics="T">
	import type { Snippet } from 'svelte';

	let {
		data,
		row
	}: {
		data: T[];
		row: Snippet<[T]>;
	} = $props();
</script>

Now T is inferred from whatever you pass as data, and row's parameter type follows. Types flow end-to-end, with zero casting. Slots never made this easy.

Exporting snippets from another file

Snippets declared at the top level of a .svelte file can be exported from a <script module> block for use elsewhere:

<!-- lib/snippets.svelte -->
<script lang="ts" module>
	export { add };
</script>

{#snippet add(a: number, b: number)}
	{a} + {b} = {a + b}
{/snippet}
<!-- consumer.svelte -->
<script lang="ts">
	import { add } from '$lib/snippets.svelte';
</script>

{@render add(1, 2)}

Requires Svelte 5.5.0+. The snippet can't reference anything declared in a non-module <script>. It has to be self-contained or reference other exported module-level declarations.

Programmatic snippets with createRawSnippet

For rare cases where you need to build a snippet from JavaScript (custom actions, library internals, test helpers), there's createRawSnippet:

import { createRawSnippet } from 'svelte';

const greeting = createRawSnippet<[string]>((name) => ({
	render: () => `<p>Hello, ${name()}!</p>`
}));

You probably don't need this. If you're reaching for it, double-check you can't solve the problem with a regular {#snippet}.

Common pitfalls

Snippets are always truthy

A snippet is a function reference, so {#if someSnippet} is always true, even when the snippet renders nothing (say, an empty {#each}).

<!-- Wrong: always renders the wrapper -->
{#if body}
	<div class="wrapper">{@render body()}</div>
{/if}

<!-- Right: check the data, not the snippet -->
{#if items.length > 0}
	<div class="wrapper">{@render body()}</div>
{/if}

Check the data that drives the snippet, not the snippet itself.

You can't have a children prop and slotted content

If you accept a children prop explicitly and a consumer also passes content between the component tags, Svelte can't decide which wins. Pick one. If you want both named snippets and a default content area, use children only for the default area.

Snippets aren't components

Snippets don't have their own lifecycle, state, or scoped styles. If you find yourself reaching for $state inside a snippet or wanting per-snippet CSS, what you actually want is a component. Use a snippet when all you need is parameterized markup.

FAQ

Are Svelte snippets the same as React children?

Conceptually, yes. Both are a way to pass UI into a component. Snippets are more flexible because they can take parameters (React's equivalent is a render prop) and because they're first-class values in the template. Where React has you choose between children, render props, or compound components, Svelte unifies all three under snippets.

Do I have to migrate from slots?

Not yet. Slots still work in Svelte 5 and are documented under legacy mode. But they're deprecated, and future versions may remove them. If you're starting a new component, write it with snippets. If you're touching an existing slots-based component, run npx sv migrate svelte-5 or migrate by hand. See the slots post for a side-by-side migration walkthrough.

Can I use snippets with custom elements?

No. Custom elements still use the native <slot> element. In a future version when Svelte removes its internal slot implementation, <slot> inside a custom element will pass through as a regular DOM slot.

Can I conditionally render inside a snippet?

Yes. Snippets contain normal template syntax, so {#if}, {#each}, and nested {@render ...} calls all work. The caveat is about rendering the snippet itself conditionally from outside (see the "always truthy" pitfall above).

How do I pass a snippet to a regular function?

Snippets are values, so you pass them like anything else. But rendering a snippet requires a template context ({@render}), so you typically pass a snippet through JavaScript and render it in markup, not to a function that renders it itself. If you need the latter, createRawSnippet exists.

Wrapping up

Snippets are how you compose UI in Svelte 5. Default content via children, named placeholders via props, data passing through parameters, full TypeScript support. The whole thing is more consistent than slots ever were, and it sits naturally alongside runes and the rest of the Svelte 5 model.

If you're coming from slots, the side-by-side migration guide is in the slots post. And if you want to see snippets in a full app context, that's exactly what we build in the Full Stack SvelteKit course.

Happy coding!

Related posts