Understanding the Snippet Mental Model

The Evolution from Slots to Snippets

In Svelte 4, component composition relied on slots (<slot>), a pattern borrowed from Web Components. While functional, slots introduced several pain points that affected both developer experience and code maintainability:

The Slot Limitations:

  • Unidirectional data flow problems: Passing data from child to parent required props callbacks or complex bindings
  • Verbose named slot syntax: <svelte:fragment slot="header"> felt heavy for simple use cases
  • Runtime-only slot detection: The $$slots object was only available at runtime, limiting TypeScript’s ability to catch errors at compile time
  • No reusability: Slots couldn’t be extracted and reused across different components—they were tightly coupled to their component
  • Unclear data flow: The implicit nature of slot content made it hard to trace where content came from and what data was available

The Snippet Philosophy

Svelte 5 introduces snippets — a paradigm shift that treats reusable markup as first-class values. This isn’t just syntactic sugar; it’s a fundamental rethinking of component composition based on these principles:

What Makes Snippets Different:

  • Functions, not holes: Snippets are function-like constructs that you explicitly call, not implicit content projections
  • Explicit is better than implicit: Data flow is visible—you can see exactly what data goes in and what comes out
  • Reusable and composable: Snippets can be passed as props, stored in variables, and reused across components like any other value
  • Type-safe by default: TypeScript can verify snippet signatures at compile time, catching errors before runtime
  • Programmatic control: You decide when, where, and how many times a snippet renders—full control over the rendering lifecycle

This tutorial focuses on defining snippets: their syntax, parameters, scoping rules, and how to export them for reuse. For rendering patterns and advanced composition techniques, see the companion article on @render.


The Core Concept: Markup as Values

Think of a snippet as a template function. Just as you define a JavaScript function to encapsulate logic, you define a snippet to encapsulate markup structure. The critical difference from regular markup: definition ≠ execution.

Defining Without Rendering

When you write a snippet, you’re creating a reusable template, not immediately adding elements to the DOM. This is analogous to how defining a function doesn’t execute it:

{#snippet greeting()}
	<p>Hello, welcome to our application!</p>
{/snippet}

What’s happening here? This creates a snippet named greeting that, when invoked, will produce a paragraph element. But notice: nothing appears in the DOM yet. The snippet is defined but not executed—similar to how defining a function doesn’t run its code:

// JavaScript function - defined but not executed
function greet() {
	return 'Hello, welcome!'
}

// Svelte snippet - defined but not rendered
{#snippet greeting()}
	<p>Hello, welcome to our application!</p>
{/snippet}

Why this matters: This separation allows you to define markup patterns once and reuse them multiple times, conditionally, or in different contexts—without duplicating the definition.

Explicit Rendering

To actually render the snippet’s content into the DOM, you must explicitly invoke it using the @render directive:

{#snippet greeting()}
	<p>Hello, welcome to our application!</p>
{/snippet}

<!-- NOW the paragraph appears in the DOM -->
{@render greeting()}

Why this separation matters: This explicit rendering model gives you precise control over:

  • When the markup renders (conditional rendering based on state)
  • Where it appears in the component tree (layout flexibility)
  • How many times it renders (iteration without duplication)
  • What data it receives each time (parameterization)

The key insight: Snippets separate definition (what markup to create) from rendering (when and where to insert it into the DOM). This separation is what enables the powerful composition patterns you’ll explore throughout this guide.


Why Snippets Matter: The Maintainability Crisis

The Cost of Code Duplication

When the same markup pattern appears in multiple places, you face a maintenance crisis: every update requires finding and changing every occurrence. Miss one, and you’ve introduced inconsistency. This isn’t just about saving keystrokes—it’s about reducing cognitive load and preventing bugs.

The DRY Principle in Action

Consider this common Svelte 4 pattern where the same markup appears in different conditional contexts:

<!-- Svelte 4: Repetitive, error-prone -->
{#each images as image}
	{#if image.href}
		<a href={image.href}>
			<figure>
				<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
				<figcaption>{image.caption}</figcaption>
			</figure>
		</a>
	{:else}
		<figure>
			<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
			<figcaption>{image.caption}</figcaption>
		</figure>
	{/if}
{/each}

The problems with this approach:

  • Duplication: The <figure> block appears twice with identical structure
  • Fragility: Changing the image structure requires updating both branches
  • Error-prone: Easy to update one branch and forget the other, causing visual inconsistencies
  • Readability: The actual logic (whether to wrap in a link) is obscured by repetitive markup
  • Scalability: Adding a third condition means triplicating the markup

With snippets, the same logic becomes:

<!-- Svelte 5: DRY and maintainable -->
{#snippet figure(image)}
	<figure>
		<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
		<figcaption>{image.caption}</figcaption>
	</figure>
{/snippet}

{#each images as image}
	{#if image.href}
		<a href={image.href}>
			{@render figure(image)}
		</a>
	{:else}
		{@render figure(image)}
	{/if}
{/each}

What improved:

  • Single source of truth: The figure structure is defined once at the top
  • Change propagation: Updates to the figure automatically apply everywhere it’s rendered
  • Clear intent: The conditional logic is now about wrapping, not recreating markup
  • Testability: You can test the figure structure independently of its usage context
  • Scalability: Adding more conditions just adds more {@render figure(image)} calls

Real-world impact

In a production application with hundreds of components, this pattern prevents entire classes of bugs. When you need to add lazy loading to images, update alt text format, or change the caption styling, you change it once instead of hunting through multiple files.


Snippet Parameters: Data-Driven Rendering

The Power of Parameterization

Snippets without parameters are useful, but parameterized snippets unlock the full power of reusable markup. By accepting parameters, snippets become adaptable templates that can render different data while maintaining consistent structure.

Passing Data to Markup

Think of snippet parameters like function arguments: they define what data the snippet needs to do its job. This creates a clear contract between the snippet definition and its usage.

Basic Parameters

Multiple Discrete Values

Use multiple parameters when you need to pass distinct, unrelated pieces of data:

{#snippet userCard(name, role, avatar)}
	<div class="user-card">
		<img src={avatar} alt="{name}'s avatar" />
		<h3>{name}</h3>
		<span class="role">{role}</span>
	</div>
{/snippet}

{@render userCard('Alice', 'Developer', '/avatars/alice.jpg')}
{@render userCard('Bob', 'Designer', '/avatars/bob.jpg')}

Why this works well:

  • Clear contract: You immediately see this snippet needs three pieces of data
  • Type safety: TypeScript can verify you’re passing the right number and types of arguments
  • Flexibility: Each render call can provide completely different data

When to use this pattern:

  • When the data naturally exists as separate variables
  • When you want to be explicit about what data is required
  • When the number of parameters is small (≤5 is a good rule)

Default Parameter Values

Progressive Enhancement

Parameters can have default values, implementing a progressive enhancement pattern where simpler use cases work with minimal code:

{#snippet badge(text, variant = 'default', size = 'medium')}
	<span class="badge badge-{variant} badge-{size}">
		{text}
	</span>
{/snippet}

{@render badge('New')}
<!-- Equivalent to: badge('New', 'default', 'medium') -->

{@render badge('Sale', 'danger')}
<!-- Equivalent to: badge('Sale', 'danger', 'medium') -->

{@render badge('Featured', 'success', 'large')}
<!-- All parameters explicit -->

What’s happening here:

  • First call: Only text is provided; variant and size use their defaults
  • Second call: text and variant are provided; size uses its default
  • Third call: All parameters are explicitly provided

When to use defaults:

  • When you have a common case that should be easy (like default styling)
  • When parameters represent progressive enhancements rather than core data
  • When you want to evolve your API without breaking existing uses

Design tip: Order parameters from most-to-least likely to be customized. This lets users provide fewer arguments for common cases.

Destructuring Parameters

Object-Based APIs

For complex data structures or when you have many related parameters, destructuring provides cleaner, more maintainable syntax:

{#snippet productItem({ name, price, inStock, imageUrl })}
	<article class="product" class:out-of-stock={!inStock}>
		<img src={imageUrl} alt={name} />
		<h3>{name}</h3>
		<p class="price">${price.toFixed(2)}</p>
		{#if !inStock}
			<span class="badge">Out of Stock</span>
		{/if}
	</article>
{/snippet}

{#each products as product}
	{@render productItem(product)}
{/each}

Why destructuring shines here:

  • Self-documenting: The snippet declaration shows exactly which product fields it uses
  • Flexible: Product objects can have additional fields that the snippet ignores
  • Refactoring-friendly: Adding new product fields doesn’t break the snippet
  • Readable at call site: {@render productItem(product)} is clearer than {@render productItem(product.name, product.price, product.inStock, product.imageUrl)}

When to use destructuring:

  • When data naturally exists as an object (like items from an array)
  • When you need more than 3-4 parameters
  • When the parameters form a cohesive unit (product properties, user data, configuration object)
  • When you want to extract only specific fields from a larger object

Combining destructuring with defaults:

{#snippet productCard({
	name,
	price,
	inStock = true,
	imageUrl = '/placeholder.jpg',
	rating = null
})}
	<article class="product-card">
		<img src={imageUrl} alt={name} />
		<h2>{name}</h2>
		<p class="price">${price}</p>

		{#if rating}
			<div class="rating">{rating}/5</div>
		{/if}

		{#if !inStock}
			<span class="badge">Out of Stock</span>
		{/if}
	</article>
{/snippet}

This combines the clarity of destructuring with the flexibility of defaults—products without inStock, imageUrl, or rating fields will still render correctly.

Critical Limitation

No Rest Parameters

Unlike JavaScript functions, snippets cannot use rest parameters (...rest) to capture variable-length argument lists:

<!-- AVOID: This will NOT work - compiler error -->
{#snippet example(first, ...others)}
	<p>First: {first}</p>
	<p>Others: {others.length} more items</p>
{/snippet}

<!-- PREFERRED Instead, pass an array explicitly -->
{#snippet example(first, others)}
	<p>First: {first}</p>
	<p>Others: {others.length} more items</p>
{/snippet}

{@render example('Alice', ['Bob', 'Charlie', 'Diana'])}

Why this limitation exists: Svelte compiles snippets into optimized rendering functions at build time. Rest parameters would require runtime argument manipulation, which conflicts with Svelte’s compile-time optimization model.

Workarounds when you need variable arguments:

  1. Pass an array: Explicitly pass a collection as a single parameter
  2. Destructuring with known fields: Use object destructuring for known fields
  3. Multiple snippets: Create different snippets for different arities (though this is rarely needed in practice)

Snippet Scope and Visibility: Lexical Scoping Rules

Understanding Snippet Scope

Snippets follow JavaScript’s lexical scoping rules, which means their visibility is determined by where they’re defined in your component’s structure, not where they’re rendered. This is both powerful (enabling closures over component state) and something you need to understand to avoid confusion.

Accessing Component State

Reactive Closure Over Script Variables

Snippets can access variables from the surrounding <script> block, creating a reactive closure. When these variables change, all rendered instances of the snippet automatically update:

<script>
	let { message = "it's great to see you!" } = $props()
	let currentTheme = $state('light')

	function toggleTheme() {
		currentTheme = currentTheme === 'light' ? 'dark' : 'light'
	}
</script>

{#snippet hello(name)}
	<p class="theme-{currentTheme}">
		Hello {name}! {message}
	</p>
{/snippet}

{@render hello('Alice')}
{@render hello('Bob')}

<button onclick={toggleTheme}>Toggle Theme</button>

What’s happening here:

  • The hello snippet “closes over” currentTheme and message from the parent scope
  • When currentTheme changes (via button click), both rendered snippets update automatically
  • Each @render call sees the current value of currentTheme—this is Svelte’s fine-grained reactivity at work

Why this matters:

  • Single source of truth: The theme state lives in one place but affects all snippet renders
  • No prop drilling: Snippets can access component state without needing to pass it as parameters
  • Automatic updates: Reactivity works across the snippet boundary—no manual subscriptions needed

When to use closure vs parameters:

  • Use closure for global component state (theme, user session, feature flags)
  • Use parameters for data that varies per render (list items, IDs, specific content)

Accessing Loop Variables: Per-Iteration Closures

Snippets defined inside loops can access the loop’s iteration variables, creating a separate closure for each iteration:

{#each categories as category, categoryIndex}
	{#snippet categoryHeader()}
		<h2>
			Category #{categoryIndex + 1}: {category.name}
			<span class="count">({category.items.length} items)</span>
		</h2>
	{/snippet}

	<section>
		{@render categoryHeader()}
		{#each category.items as item}
			<p>{item}</p>
		{/each}

		<!-- Can render the header again with the same category -->
		{@render categoryHeader()}
	</section>
{/each}

What makes this work:

  • Each iteration creates a new categoryHeader snippet with its own closure
  • The first iteration’s snippet closes over category[0] and categoryIndex = 0
  • The second iteration’s snippet closes over category[1] and categoryIndex = 1
  • Each snippet is independent—changing one doesn’t affect the others

Practical use cases:

  • Repeated headers/footers: Render the same content at top and bottom of a section
  • Conditional rendering: Show/hide repeated elements based on iteration data
  • Consistent formatting: Apply the same styling to different parts of an item

Visibility Rules: Lexical Scope in Practice

Snippets follow a predictable visibility pattern based on DOM structure. Understanding these rules prevents “snippet not defined” errors and helps you structure your components effectively.

What Can See Your Snippet

Snippets are visible to:

  1. Themselves (enabling recursion for tree structures)
  2. Sibling elements at the same nesting level
  3. Children of those siblings (descendants in the DOM tree)

What Cannot See Your Snippet

They are NOT visible to:

  1. Parent elements (outer/containing scopes)
  2. Elements in different branches of the DOM tree
  3. Other snippets at the same level (unless you pass them as parameters)

Visual Scoping Example

<div class="outer">
	{#snippet outer()}
		<p>I'm the outer snippet</p>

		{#snippet inner()}
			<span>I'm nested inside outer</span>
		{/snippet}

		<!-- PREFERRED: WORKS - inner is visible here (sibling + child scope) -->
		{@render inner()}
	{/snippet}

	<!-- PREFERRED: WORKS - outer is visible here (same level scope) -->
	{@render outer()}

	<!-- AVOID: ERROR - inner is not visible here (parent can't see child's snippet) -->
	{@render inner()}
</div>

<!-- AVOID: ERROR - outer is not visible here (outside the div) -->
{@render outer()}

Why these rules exist:

  • Prevent naming collisions: Different branches can have snippets with the same name
  • Clear mental model: Visibility follows the DOM tree structure
  • Encapsulation: Inner implementation details (nested snippets) don’t leak to outer scopes

Practical Implications

1. Top-Level Snippets for Global Reuse

When is snippet needed in multiple places of a component, define it at the top level:

<!-- Available everywhere in this component -->
{#snippet formatPrice(amount)}
	<span class="price">${amount.toFixed(2)}</span>
{/snippet}

<div class="products">
	{#each products as product}
		<div class="product">
			{@render formatPrice(product.price)}
		</div>
	{/each}
</div>

<div class="cart">
	<p>Total: {@render formatPrice(cartTotal)}</p>
</div>

2. Scoped Snippets for Localized Logic

When a snippet is only relevant within a specific conditional or loop, define it there to keep the namespace clean:

{#if showUserProfile}
	{#snippet profileField(label, value)}
		<div class="field">
			<span class="label">{label}:</span>
			<span class="value">{value}</span>
		</div>
	{/snippet}

	<div class="user-profile">
		{@render profileField('Name', user.name)}
		{@render profileField('Email', user.email)}
		{@render profileField('Role', user.role)}
	</div>
{/if}

<!-- profileField is not accessible here - keeps the namespace clean -->

3. Avoiding Scope Issues

When snippet needs to be used in multiple branches of a conditional, define it at a common ancestor level:

<!-- PREFERRED Define at common ancestor level -->
{#snippet statusBadge(status)}
	<span class="badge badge-{status}">{status}</span>
{/snippet}

{#if condition}
	<div>{@render statusBadge('active')}</div>
{:else}
	<div>{@render statusBadge('inactive')}</div>
{/if}

Not:

<!-- AVOID: Don't define in one branch and try to use in another -->
{#if condition}
	{#snippet statusBadge(status)}
		<span class="badge badge-{status}">{status}</span>
	{/snippet}
	<div>{@render statusBadge('active')}</div>
{:else}
	<!-- ERROR: statusBadge is not defined here -->
	<div>{@render statusBadge('inactive')}</div>
{/if}

Recursive Snippets

Recursion is a powerful technique for rendering hierarchical data structures of unknown depth. Svelte snippets support recursion, allowing you to define snippets that call themselves or each other to handle nested data elegantly.

When Recursion Makes Sense

Recursion in snippets solves a specific problem: rendering data of unknown depth. When you have tree structures, nested comments, file systems, or any hierarchical data where you don’t know how many levels deep it goes, recursive snippets are your solution.

Common use cases:

  • File/folder trees (depth unknown until runtime)
  • Comment threads with nested replies
  • Organization charts
  • Category hierarchies (e.g., e-commerce categories)
  • JSON/XML tree visualizers
  • Menu systems with unlimited nesting

Understanding Snippet Recursion

Self-Referencing Patterns

Snippets can reference themselves, just like JavaScript functions. The key is understanding the base case (when to stop) and the recursive case (when to continue).

Basic Recursion: Countdown Example

{#snippet countdown(n)}
	{#if n > 0}
		<!-- Recursive case: continue counting -->
		<span>{n}...</span>
		{@render countdown(n - 1)}
	{:else}
		<!-- Base case: stop recursion -->
		<span>🚀 Liftoff!</span>
	{/if}
{/snippet}

{@render countdown(5)}
<!-- Renders: 5... 4... 3... 2... 1... 🚀 Liftoff! -->

How it works:

  1. Initial call: countdown(5) renders “5…” then calls countdown(4)
  2. Second call: countdown(4) renders “4…” then calls countdown(3)
  3. Continues: Each call renders its number and makes a smaller call
  4. Base case: When n reaches 0, renders ”🚀 Liftoff!” and stops

Critical concept: The base case prevents infinite loops. Without {#if n > 0}, the snippet would call itself forever, eventually causing a stack overflow.

Mutually Recursive Snippets

Snippets can also call each other in a mutually recursive pattern:

{#snippet blastoff()}
	<span class="rocket">🚀</span>
{/snippet}

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

{@render countdown(10)}

What’s different here:

  • countdown doesn’t directly contain the rocket emoji
  • Instead, it delegates to blastoff when done
  • This separates concerns: counting logic vs final display

When to use mutual recursion:

  • When different states require different rendering logic
  • To keep individual snippets focused and single-purpose
  • When you want to reuse sub-snippets independently

Practical Example:

File Tree Structure

File systems are the canonical example of recursive data—folders contain files and other folders, which contain files and folders, ad infinitum.

Example Data Structure

<script>
	let fileTree = $state({
		name: 'project',
		type: 'folder',
		children: [
			{
				name: 'src',
				type: 'folder',
				children: [
					{
						name: 'components',
						type: 'folder',
						children: [
							{ name: 'Button.svelte', type: 'file' },
							{ name: 'Card.svelte', type: 'file' }
						]
					},
					{ name: 'App.svelte', type: 'file' },
					{ name: 'main.js', type: 'file' }
				]
			},
			{ name: 'package.json', type: 'file' },
			{ name: 'README.md', type: 'file' }
		]
	})
</script>

Use: The Recursive Renderer

{#snippet treeNode(node, depth = 0)}
	<div class="tree-node" style="padding-left: {depth * 20}px">
		{#if node.type === 'folder'}
			<!-- Folder case: show icon, name, then recursively show children -->
			<span class="folder"> {node.name}</span>

			{#if node.children}
				{#each node.children as child}
					<!-- RECURSION: render each child as a treeNode -->
					{@render treeNode(child, depth + 1)}
				{/each}
			{/if}
		{:else}
			<!-- Base case: files don't have children, stop recursing -->
			<span class="file">{node.name}</span>
		{/if}
	</div>
{/snippet}

{@render treeNode(fileTree)}

How the recursion flows:

  1. Start: Render treeNode(fileTree, 0) → “project” at depth 0
  2. First level: Loop through project’s children
    • Render treeNode(src, 1) - src at depth 1
    • Render treeNode(package.json, 1) - package.json (stops, it’s a file)
    • Render treeNode(README.md, 1) - README.md (stops, it’s a file)
  3. Second level: When rendering src, loop through its children
    • Render treeNode(components, 2) - components at depth 2
    • Render treeNode(App.svelte, 2) - App.svelte (stops)
    • Render treeNode(main.js, 2) - main.js (stops)
  4. Third level: When rendering components, loop through its children
    • Render treeNode(Button.svelte, 3) - Button.svelte (stops)
    • Render treeNode(Card.svelte, 3) - Card.svelte (stops)

Key features of this pattern:

  • Depth tracking: The depth parameter creates proper indentation
  • Automatic termination: Files (no children) naturally end recursion
  • Unknown depth: Works for any number of nesting levels
  • Uniform rendering: Each node rendered consistently regardless of depth

Advanced

Let’s enhance the file tree with expand/collapse functionality:

Interactive Collapsible Tree

<script>
	let expandedFolders = $state(new Set(['project', 'src']))

	function toggleFolder(folderName) {
		if (expandedFolders.has(folderName)) {
			expandedFolders.delete(folderName)
		} else {
			expandedFolders.add(folderName)
		}
		expandedFolders = expandedFolders // Trigger reactivity
	}

	let fileTree = $state({
		name: 'project',
		type: 'folder',
		children: [
			/* ... same as above ... */
		]
	})
</script>

{#snippet treeNode(node, depth = 0)}
	<div class="tree-node" style="padding-left: {depth * 20}px">
		{#if node.type === 'folder'}
			<button class="folder-toggle" onclick={() => toggleFolder(node.name)}>
				{expandedFolders.has(node.name) ? 'expanded' : 'collapsed'}
				{node.name}
			</button>

			{#if expandedFolders.has(node.name) && node.children}
				{#each node.children as child}
					{@render treeNode(child, depth + 1)}
				{/each}
			{/if}
		{:else}
			<span class="file">{node.name}</span>
		{/if}
	</div>
{/snippet}

{@render treeNode(fileTree)}

What changed:

  • State tracking: expandedFolders Set tracks which folders are open
  • Toggle function: Click handler adds/removes folders from the Set
  • Conditional rendering: Only show children if folder is expanded
  • Icon variation: Show different emoji for expanded/collapsed state

Why this is powerful:

  • The recursion handles arbitrarily deep nesting
  • State management is flat (Set of folder names), not nested
  • Each node can expand/collapse independently
  • Adding new files/folders requires zero code changes

Recursion Best Practices

1. Always Have a Base Case

<!-- AVOID: DANGEROUS - No base case, infinite recursion -->
{#snippet infinite(n)}
	<p>{n}</p>
	{@render infinite(n + 1)}
{/snippet}

<!-- PREFERRED SAFE: Clear base case stops recursion -->
{#snippet safe(n)}
	{#if n > 0}
		<p>{n}</p>
		{@render safe(n - 1)}
	{/if}
{/snippet}

2. Track Depth to Prevent Stack Overflow

For user-provided data, add a maximum depth:

{#snippet treeNode(node, depth = 0, maxDepth = 20)}
	{#if depth >= maxDepth}
		<span class="truncated">... (max depth reached)</span>
	{:else if node.type === 'folder'}
		<!-- normal folder rendering -->
		{#each node.children as child}
			{@render treeNode(child, depth + 1, maxDepth)}
		{/each}
	{:else}
		<!-- file rendering -->
	{/if}
{/snippet}

3. Consider Performance for Large Trees

For very large trees (thousands of nodes), consider:

  • Virtualization: Only render visible nodes
  • Lazy loading: Fetch children on-demand
  • Pagination: Limit children shown per level

Passing Snippets to Components: Inversion of Control

The Power of Render Delegation

Passing snippets to components implements inversion of control—instead of the component dictating exactly how content looks, it delegates rendering decisions back to the parent. This creates flexible, reusable components that adapt to different use cases without modification.

The traditional problem:

<!-- Rigid: Table component controls ALL rendering -->
<Table data={fruits} />

The Table component hard-codes how headers and rows look. Want custom styling? You’re stuck. Need to add a column? Modify the component.

The snippet solution:

<!-- Flexible: Parent controls rendering, Table provides structure -->
<Table data={fruits} {header} {row} />

The Table provides the structure (table, loops, layout) while the parent provides the content (headers, cell formatting). This separation of concerns is powerful. Here are two main ways to pass snippets as props.

1. Explicit Snippet Props

Define snippets in the parent and pass them as explicit props to the child component:

<!-- Parent: +page.svelte -->
<script>
	import Table from './Table.svelte'
	const fruits = [
		{ name: 'apples', qty: 5, price: 2, organic: true },
		{ name: 'bananas', qty: 10, price: 1, organic: false }
	]
</script>

{#snippet header()}
	<th>Fruit</th>
	<th>Quantity</th>
	<th>Price</th>
	<th>Organic</th>
{/snippet}

{#snippet row(item)}
	<td>{item.name}</td>
	<td>{item.qty}</td>
	<td>${item.price.toFixed(2)}</td>
	<td>{item.organic ? '' : ''}</td>
{/snippet}

<!-- Pass snippets as props, just like data -->
<Table data={fruits} {header} {row} />
<!-- Child: Table.svelte -->
<script>
	let { data, header, row } = $props()
</script>

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

What’s happening here:

  • Parent controls content: The parent defines exactly what headers and row cells contain
  • Child controls structure: The child handles the table wrapper, thead/tbody, and iteration
  • Clear interface: The Table’s props API (data, header, row) documents what it needs
  • Reusable component: The same Table can render products, users, orders—anything

When to use explicit props:

  • When snippets are defined separately from usage
  • When you want to reuse snippets across multiple components
  • When the relationship between snippets and component should be explicit
  • For complex components where documenting the API is important

2. Implicit Snippet Props

Snippets defined directly inside component tags automatically become props on that component:

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

	{#snippet row(item)}
		<td>{item.name}</td>
		<td>{item.qty}</td>
		<td>${item.price.toFixed(2)}</td>
		<td>{item.organic ? '' : ''}</td>
	{/snippet}
</Table>

This is semantically identical to the explicit version. The compiler transforms it into:

<!-- Compiler transformation (conceptual) -->
{#snippet header()}...{/snippet}
{#snippet row(item)}...{/snippet}
<Table data={fruits} {header} {row} />

When to use implicit props:

  • When snippets are only used with this specific component instance
  • For cleaner, more declarative syntax that shows content alongside component
  • When the parent-child relationship is obvious from context
  • For components following the “component wraps content” pattern

Choosing between explicit and implicit:

<!-- PREFERRED Explicit: When reusing snippets -->
{#snippet userRow(user)}
	<td>{user.name}</td>
	<td>{user.email}</td>
{/snippet}

<Table data={activeUsers} row={userRow} />
<Table data={inactiveUsers} row={userRow} />

<!-- PREFERRED Implicit: When snippets are specific to this instance -->
<UserTable data={users}>
	{#snippet row(user)}
		<td>{user.name}</td>
		<td>{user.email}</td>
		<td><button>Edit</button></td>
	{/snippet}
</UserTable>

The Implicit children Snippet

Every component can accept a special snippet prop named children, representing the default content slot. This allows you to pass arbitrary content to a component without explicitly naming the snippet.

Button component

<!-- Button.svelte -->
<script>
	// children is now a snippet prop, just like any other
	let { children } = $props()
</script>

<button>
	{@render children()}
</button>

When you put any content inside a component tags (without a named snippet), that content automatically becomes the children snippet.

<!-- Parent usage -->
<Button>Click me!</Button>

What the compiler does conceptually:

{#snippet children()}
	Click me!
{/snippet}

<Button {children} />

Why children is special:

  • Default content slot: Every component can accept content without naming it
  • Progressive enhancement: Simple components work without explicit snippet names
  • Familiar pattern: Mirrors how HTML elements accept content
  • Backwards compatible: Similar to how slots worked, easing migration

Mixing Named Snippets and Children

You can combine children with named snippets for complex layouts:

<!-- Parent -->
<Card>
	{#snippet header()}
		<h2>Product Details</h2>
	{/snippet}

	{#snippet footer()}
		<button>Buy Now</button>
	{/snippet}

	<!-- This content becomes the children snippet -->
	<p>Amazing product description...</p>
	<ul>
		<li>Feature 1</li>
		<li>Feature 2</li>
	</ul>
</Card>
<!-- Card.svelte -->
<script>
	let { children, header, footer } = $props()
</script>

<article class="card">
	{#if header}
		<div class="card-header">
			{@render header()}
		</div>
	{/if}

	<div class="card-body">
		{@render children()}
	</div>

	{#if footer}
		<div class="card-footer">
			{@render footer()}
		</div>
	{/if}
</article>

The result:

<article class="card">
	<div class="card-header">
		<h2>Product Details</h2>
	</div>

	<div class="card-body">
		<p>Amazing product description...</p>
		<ul>
			<li>Feature 1</li>
			<li>Feature 2</li>
		</ul>
	</div>

	<div class="card-footer">
		<button>Buy Now</button>
	</div>
</article>

Component API Design Patterns

1. Required vs Optional Snippets

Design component APIs that clearly communicate what’s required:

<!-- Table.svelte -->
<script>
	// row is required, header/footer are optional
	let { data, row, header, footer } = $props()
</script>

<table>
	{#if header}
		<thead><tr>{@render header()}</tr></thead>
	{/if}

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

	{#if footer}
		<tfoot><tr>{@render footer()}</tr></tfoot>
	{/if}
</table>

Usage:

<!-- Minimal: just required snippets -->
<Table data={items}>
	{#snippet row(item)}
		<td>{item.name}</td>
	{/snippet}
</Table>

<!-- Full: all optional snippets provided -->
<Table data={items}>
	{#snippet header()}
		<th>Name</th>
	{/snippet}
	{#snippet row(item)}
		<td>{item.name}</td>
	{/snippet}
	{#snippet footer()}
		<th>Total: {items.length}</th>
	{/snippet}
</Table>

2. Snippet Parameters for Context

Pass contextual data to snippets so they can make informed rendering decisions:

<!-- List.svelte -->
<script>
	let { items, item } = $props()
</script>

<ul>
	{#each items as currentItem, index}
		<li>
			{@render item(currentItem, {
				index,
				isFirst: index === 0,
				isLast: index === items.length - 1
			})}
		</li>
	{/each}
</ul>

Usage:

<List items={products}>
	{#snippet item(product, { index, isFirst, isLast })}
		<div class:featured={isFirst}>
			{index + 1}. {product.name}
			{#if isLast}
				<span class="badge">Last item!</span>
			{/if}
		</div>
	{/snippet}
</List>

Why pass context:

  • Snippets can access position information (first, last, index)
  • Parent doesn’t need to compute this data
  • Component encapsulates the iteration logic
  • Snippets remain focused on rendering

For more advanced rendering patterns, optional snippet handling, and dynamic snippet selection, see the companion article on {@render ...}.

TypeScript Integration: Type-Safe Component APIs

Why Type Snippets?

In JavaScript, snippets are flexible but error-prone—you can pass the wrong number of arguments, wrong types, or forget required snippets entirely. TypeScript transforms snippets from runtime guesswork into compile-time guarantees.

Benefits of typing snippets:

  • IDE autocomplete: See what snippets a component expects and what parameters they need
  • Compile-time errors: Catch missing snippets or incorrect parameters before running code
  • Refactoring safety: Rename parameters or change types with confidence
  • Self-documenting APIs: The types serve as always-up-to-date documentation

The Snippet Type

Svelte 5 provides the Snippet type from the 'svelte' package. This type represents a snippet with optional parameter types.

Basic Snippet Typing

For snippets without parameters, use Snippet directly:

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

	interface Props {
		children: Snippet // Required snippet, no parameters
		title: string
	}

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

<div class="container">
	<h1>{title}</h1>
	{@render children()}
</div>

What this accomplishes:

  • Required check: TypeScript errors if parent doesn’t provide children
  • No parameters enforced: TypeScript errors if you call children(someArg)
  • Clear contract: Anyone using this component knows they must provide a children snippet

Typing Snippets with Parameters

Snippet parameters are typed as a tuple (ordered list of types):

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

	interface Props {
		children: Snippet // No parameters
		row: Snippet<[string]> // One parameter: string
		cell: Snippet<[string, number]> // Two parameters: string, number
	}

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

<div>
	{@render children()}
	{@render row('Alice')}
	{@render cell('Name', 42)}
</div>

Type safety in action:

<!-- PREFERRED Correct usage -->
<MyComponent>
	{#snippet children()}
		<p>Default content</p>
	{/snippet}

	{#snippet row(name)}
		<div>{name}</div>
	{/snippet}

	{#snippet cell(label, value)}
		<span>{label}: {value}</span>
	{/snippet}
</MyComponent>

<!-- AVOID: TypeScript errors -->
<MyComponent>
	<!-- Error: row requires one parameter -->
	{#snippet row()}
		<div>Missing parameter!</div>
	{/snippet}

	<!-- Error: cell requires two parameters -->
	{#snippet cell(label)}
		<span>{label}</span>
	{/snippet}
</MyComponent>

Why tuples? Tuples preserve parameter order and individual types, unlike arrays which have a single element type.

Advanced: Generic Snippets for Type Safety

Generics ensure type consistency between data and snippets that render that data. This prevents subtle bugs where the data shape doesn’t match what the snippet expects.

Generic Table Example

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

	interface Props {
		data: T[] // Array of generic type T
		row: Snippet<[T]> // Snippet receives items of type T
	}

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

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

How this works:

<!-- Usage with Product[] -->
<script lang="ts">
	interface Product {
		name: string
		price: number
		inStock: boolean
	}

	const products: Product[] = [{ name: 'Apple', price: 1.99, inStock: true }]
</script>

<!-- TypeScript infers T = Product -->
<Table data={products}>
	{#snippet row(product)}
		<!-- TypeScript knows 'product' is type Product -->
		<td>{product.name}</td>
		<td>${product.price.toFixed(2)}</td>
		<td>{product.inStock ? '' : ''}</td>
	{/snippet}
</Table>

What TypeScript guarantees:

  • data is a Product[]
  • row receives a Product parameter
  • If you try to access product.unknownField, TypeScript errors
  • If you change products to a different type, the snippet must match

Generic with multiple snippets:

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

	interface Props {
		data: T[]
		header: Snippet
		row: Snippet<[T, number]> // Item + index
		footer?: Snippet<[number]> // Optional, receives total count
	}

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

<table>
	<thead><tr>{@render header()}</tr></thead>
	<tbody>
		{#each data as item, index}
			<tr>{@render row(item, index)}</tr>
		{/each}
	</tbody>
	{#if footer}
		<tfoot><tr>{@render footer(data.length)}</tr></tfoot>
	{/if}
</table>

Generic with Constraints

Sometimes you need to ensure the generic type has specific properties:

<script lang="ts" generics="T extends { id: string | number }">
	import type { Snippet } from 'svelte'

	interface Props {
		items: T[]
		item: Snippet<[T]>
		onDelete?: (id: T['id']) => void
	}

	let { items, item, onDelete }: Props = $props()
</script>

<ul>
	{#each items as currentItem}
		<li>
			{@render item(currentItem)}
			{#if onDelete}
				<button onclick={() => onDelete(currentItem.id)}>Delete</button>
			{/if}
		</li>
	{/each}
</ul>

The constraint extends { id: string | number } ensures:

  • Every item in items must have an id property
  • The id must be either a string or number
  • TypeScript errors at compile-time if you pass items without id

Optional Snippet Props

Mark snippets as optional using TypeScript’s ? operator:

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

	interface Props {
		children: Snippet // Required
		header?: Snippet // Optional, no params
		footer?: Snippet<[string]> // Optional, with param
		row?: Snippet<[number, string]> // Optional, multiple params
	}

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

<div class="card">
	{#if header}
		<div class="header">{@render header()}</div>
	{/if}

	<div class="body">{@render children()}</div>

	{#if footer}
		<div class="footer">{@render footer('Default footer text')}</div>
	{/if}
</div>

Parent usage:

<!-- PREFERRED Valid: Only required snippets provided -->
<Card>
	{#snippet children()}
		<p>Main content</p>
	{/snippet}
</Card>

<!-- PREFERRED Valid: Optional snippets added -->
<Card>
	{#snippet header()}
		<h2>Title</h2>
	{/snippet}

	{#snippet children()}
		<p>Main content</p>
	{/snippet}

	{#snippet footer(text)}
		<p>{text}</p>
	{/snippet}
</Card>

<!-- AVOID: Error - Missing required 'children' snippet -->
<Card>
	{#snippet header()}
		<h2>Title</h2>
	{/snippet}
</Card>

Typing Patterns for Common Use Cases

Pattern 1: Data + Render Snippet

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

	interface Props {
		data: T[]
		render: Snippet<[T]>
	}

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

{#each data as item}
	{@render render(item)}
{/each}

Pattern 2: Optional Children with Fallback

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

	interface Props {
		children?: Snippet
	}

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

{#if children}
	{@render children()}
{:else}
	<p>Default fallback content</p>
{/if}
<script lang="ts" generics="T">
	import type { Snippet } from 'svelte'

	interface Props {
		items: T[]
		itemHeader?: Snippet<[T]>
		itemBody: Snippet<[T]>
		itemFooter?: Snippet<[T]>
	}

	let { items, itemHeader, itemBody, itemFooter }: Props = $props()
</script>

{#each items as item}
	<article>
		{#if itemHeader}
			<header>{@render itemHeader(item)}</header>
		{/if}
		<main>{@render itemBody(item)}</main>
		{#if itemFooter}
			<footer>{@render itemFooter(item)}</footer>
		{/if}
	</article>
{/each}

Type Safety Best Practices

  1. Always type public component APIs: Even if your project doesn’t enforce strict typing everywhere, type component props that other developers will use

  2. Use generics for data-driven components: When components iterate over data and pass it to snippets, use generics to maintain type safety

  3. Be explicit about optional snippets: Use ? for optional and omit it for required—this makes the API contract clear

  4. Document parameter semantics: Use JSDoc comments to explain what snippet parameters represent:

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

	interface Props {
		data: T[]
		/**
		 * Renders a single row.
		 * @param item - The data item for this row
		 * @param index - Zero-based row index
		 * @param isSelected - Whether this row is currently selected
		 */
		row: Snippet<[item: T, index: number, isSelected: boolean]>
	}
</script>

Exporting Snippets

Snippets aren’t limited to the component where they’re defined. Svelte 5 lets you export snippets so they can be reused across your app or even published as part of a template library.

Building Reusable Template Libraries

By exporting snippets, you can create libraries of reusable UI templates, formatting helpers, or design primitives. These libraries can be shared across your project or even published for others to use, making your codebase more modular and maintainable. This approach is ideal for design systems, shared UI patterns, or any situation where you want to standardize markup and logic.

The Problem: Snippet Locality

By default, snippets are component-scoped — they only exist within the component where they’re defined. This works great for component-local markup patterns, but what about formatting logic you need across many components?

Before Svelte 5.5.0, you had two unsatisfying options:

  1. Duplicate the snippet in every component that needs it (DRY violation)
  2. Create a helper component (heavyweight solution for simple markup)

Starting with Svelte 5.5.0, you can export snippets from module scripts, making them importable like any other module value.

Defining Exportable Snippets

To export snippets, define them in a <script module> block and use an export statement listing the snippet names.

<!-- Snippets.svelte -->
<script module>
	// List snippet names to export
	export { formattedPrice, stockBadge, userAvatar }
</script>

{#snippet formattedPrice(amount, currency = 'USD')}
	<span class="price">
		{new Intl.NumberFormat('en-US', {
			style: 'currency',
			currency
		}).format(amount)}
	</span>
{/snippet}

{#snippet stockBadge(quantity)}
	{#if quantity === 0}
		<span class="badge badge-danger">Out of Stock</span>
	{:else if quantity < 10}
		<span class="badge badge-warning">Low Stock ({quantity})</span>
	{:else}
		<span class="badge badge-success">In Stock</span>
	{/if}
{/snippet}

{#snippet userAvatar(url, name, size = 'medium')}
	<img src={url} alt="{name}'s avatar" class="avatar avatar-{size}" />
{/snippet}

Key requirements:

  • Snippets must be top-level (not nested inside {#if}, {#each}, etc.)
  • The export statement must be in a <script module> block
  • You export just the snippet names, not their full definitions

Importing and Using Exported Snippets

exported snippets can be imported into any component using standard ES module syntax:

<!-- ProductCard.svelte -->
<script>
	import { formattedPrice, stockBadge } from './Snippets.svelte'

	let { product } = $props()
</script>

<article class="product-card">
	<h3>{product.name}</h3>

	<div class="pricing">
		{@render formattedPrice(product.price)}
		{#if product.salePrice}
			<del>{@render formattedPrice(product.originalPrice)}</del>
		{/if}
	</div>

	<div class="stock">
		{@render stockBadge(product.stockQuantity)}
	</div>
</article>

What this achieves:

  • Consistency: Price formatting looks identical across the entire app
  • Maintainability: Update formattedPrice once, changes propagate everywhere
  • Reusability: Import only what you need per component
  • Testability: Snippet libraries can be tested independently

When to Export Snippets

Good candidates for export:

  • Formatting utilities: Dates, currencies, phone numbers
  • Common UI patterns: Badges, avatars, status indicators
  • Branded elements: Company-specific styled components
  • Accessibility patterns: Icon + text combinations, semantic markup helpers

Not recommended for export:

  • Component-specific layouts: Snippets tied to a single component’s structure
  • Stateful snippets: Those relying on component state (see limitations below)
  • Large markup blocks: Consider a full component instead

Critical Limitation

there are important limitations to understand when exporting snippets from module scripts.

No Instance State Access

Exported snippets cannot reference variables from instance <script> blocks—they can only access module-level constants and their own parameters.

Why this limitation exists: Module scripts execute once when the module loads, not per component instance. Instance state doesn’t exist at that time.

<script module>
	export { broken }
</script>

<!-- AVOID: This will NOT work -->
<script>
	let themeColor = $state('blue') // Instance state
</script>

{#snippet broken()}
	<!-- ERROR: themeColor doesn't exist in module scope -->
	<div style="color: {themeColor}">This will cause a compilation error!</div>
{/snippet}

Why it fails:

  • themeColor is instance state—it exists separately for each component instance
  • Module snippets are shared across all instances—they can’t have instance-specific behavior
  • The snippet tries to access something that doesn’t exist in its scope

Correct approach

Use module constants

When writing exportable snippets, always use module-level constants or functions for any shared values or helpers. These are defined in the <script module> block and are available to all snippets exported from the module. This ensures your snippets are self-contained, predictable, and safe to use in any component—without relying on instance-specific state.

For example:

<script module>
	// THEME_COLOR is a module-level constant - exists once, shared by all imports
	const THEME_COLOR = 'blue'
	// Export the snippet
	export { working }
</script>

<!-- define "working" snippet with use of module level variable  -->
{#snippet working()}
	<div style="color: {THEME_COLOR}">This works because THEME_COLOR is module-scoped</div>
{/snippet}

Pass state as parameters (alternative)

If your snippet needs to work with dynamic or instance-specific data, the best practice is to pass that data as parameters. This keeps the snippet flexible and decoupled from any particular component’s state. By making all required values explicit parameters, you ensure the snippet can be reused in any context, with any data.

in following example , we define a snippet that accepts a color parameter, allowing the parent component to control the color dynamically:

<script module>
	export { themeableDiv }
</script>

<!-- snippet accepting a color parameter -->
{#snippet themeableDiv(content, color)}
	<div style="color: {color}">
		{content}
	</div>
{/snippet}

Usage:

<script>
	import { themeableDiv } from './Snippets.svelte'
	// Instance state defining the theme color
	let themeColor = $state('blue')
</script>

<!-- use themeColor as snippet argument -->
{@render themeableDiv('Hello world', themeColor)}

Organizing Snippet Libraries

When building snippet libraries, organization is key. Here are some common patterns for structuring your snippet exports.

1. Domain-Specific Libraries

One of patterns is to group related snippets into domain-specific files. it is especially useful for formatting utilities or common UI elements. This logical grouping improves maintainability and clarity.

src/lib/snippets/
├── formatting.svelte      # Date, currency, number formatting
├── badges.svelte          # Status badges, notifications
├── user-ui.svelte         # Avatars, user cards, profile elements
└── icons.svelte           # Icon + label combinations

than you can import only what you need:

<!-- formatting.svelte -->
<script module>
	export { formatDate, formatCurrency, formatPercentage }
</script>

{#snippet formatDate(date, format = 'short')}
	<time datetime={date.toISOString()}>
		{date.toLocaleDateString('en-US', { dateStyle: format })}
	</time>
{/snippet}

{#snippet formatCurrency(amount, currency = 'USD')}
	<span class="currency">
		{new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)}
	</span>
{/snippet}

{#snippet formatPercentage(value, decimals = 0)}
	<span class="percentage">
		{(value * 100).toFixed(decimals)}%
	</span>
{/snippet}

2.: Themed UI Components

Another pattern is to create themed UI snippet libraries. Its useful for design systems or branded elements that need consistent styling across the app.

<!-- badges.svelte -->
<script module>
	const BADGE_VARIANTS = {
		success: { color: '#059669', bg: '#d1fae5' },
		warning: { color: '#d97706', bg: '#fef3c7' },
		danger: { color: '#dc2626', bg: '#fee2e2' },
		info: { color: '#2563eb', bg: '#dbeafe' }
	}

	export { badge, statusBadge }
</script>

{#snippet badge(text, variant = 'info')}
	{@const theme = BADGE_VARIANTS[variant]}
	<span
		class="badge"
		style="
			color: {theme.color}; 
			background: {theme.bg};
			padding: 0.25rem 0.75rem;
			border-radius: 9999px;
			font-size: 0.875rem;
		"
	>
		{text}
	</span>
{/snippet}

{#snippet statusBadge(status)}
	{#if status === 'active'}
		{@render badge('Active', 'success')}
	{:else if status === 'pending'}
		{@render badge('Pending', 'warning')}
	{:else if status === 'error'}
		{@render badge('Error', 'danger')}
	{:else}
		{@render badge(status, 'info')}
	{/if}
{/snippet}

Usage across application:

<!-- OrderList.svelte -->
<script>
	import { statusBadge } from '$lib/snippets/badges.svelte'
	let { orders } = $props()
</script>

{#each orders as order}
	<div class="order">
		<span>Order #{order.id}</span>
		{@render statusBadge(order.status)}
	</div>
{/each}

Versioning and Evolution

As your snippet libraries grow, you may need to evolve their APIs. A common pattern is to version your snippet files, allowing multiple versions to coexist.

<!-- snippets/v1/formatting.svelte -->
<script module>
	export { formatPrice }
</script>

{#snippet formatPrice(amount)}
	<!-- Legacy implementation -->
{/snippet}
<!-- snippets/v2/formatting.svelte -->
<script module>
	export { formatPrice }
</script>

{#snippet formatPrice(amount, options = {})}
	<!-- Enhanced implementation with more options -->
{/snippet}

This allows gradual migration as you improve snippet implementations.

Best Practices: Designing with Snippets

When designing components that leverage snippets, following best practices ensures your code remains maintainable, reusable, and clear. Here are key principles to guide you.

1. Name Snippets Descriptively

Snippet names should communicate what they render, not just that they’re snippets. Good names serve as inline documentation.

<!-- AVOID: Vague, non-descriptive naming -->
{#snippet item(data)}
	<div>{data.name}</div>
{/snippet}

{#snippet thing(x)}
	<span>{x}</span>
{/snippet}

{#snippet fn(a, b)}
	<p>{a} - {b}</p>
{/snippet}

<!-- PREFERRED Descriptive, self-documenting naming -->
{#snippet productCard(product)}
	<article class="product">
		<h3>{product.name}</h3>
		<p>${product.price}</p>
	</article>
{/snippet}

{#snippet userAvatar(user)}
	<img src={user.avatar} alt="{user.name}'s avatar" />
{/snippet}

{#snippet dateRange(startDate, endDate)}
	<time>{startDate}</time><time>{endDate}</time>
{/snippet}

Naming conventions:

  • Use nouns for entity rendering: userCard, productTile, commentThread
  • Use verbs + nouns for actions/formatting: formatDate, renderStatus, showError
  • Be specific: compactUserCard is better than userCard2
  • Match domain language: If your team calls them “listings,” use listingCard, not itemCard

2. Keep Snippets Focused

Each snippet should have one clear purpose. Large, multi-responsibility snippets become hard to maintain and reuse.

Anti-pattern: God Snippet

<!-- AVOID: Too much responsibility in one snippet -->
{#snippet userSection(user)}
	<div class="user-section">
		<!-- Avatar -->
		<img src={user.avatar} alt={user.name} />

		<!-- Bio -->
		<div class="bio">
			<h2>{user.name}</h2>
			<p>{user.bio}</p>
		</div>

		<!-- Stats -->
		<div class="stats">
			<div>Posts: {user.postCount}</div>
			<div>Followers: {user.followers}</div>
			<div>Following: {user.following}</div>
		</div>

		<!-- Actions -->
		<div class="actions">
			<button>Follow</button>
			<button>Message</button>
			<button>Share</button>
		</div>
	</div>
{/snippet}

Problem: This snippet does too much. What if you need just the avatar elsewhere? Or want to reorder sections? You’d need to duplicate or create a variant.

Better: Decomposed, Focused Snippets

<!-- PREFERRED Each snippet has one clear purpose -->
{#snippet userAvatar(user, size = 'medium')}
	<img
		src={user.avatar}
		alt="{user.name}'s avatar"
		class="avatar avatar-{size}"
	/>
{/snippet}

{#snippet userBio(user)}
	<div class="bio">
		<h2>{user.name}</h2>
		<p>{user.bio}</p>
	</div>
{/snippet}

{#snippet userStats(user)}
	<div class="stats">
		<div class="stat">
			<span class="label">Posts</span>
			<span class="value">{user.postCount}</span>
		</div>
		<div class="stat">
			<span class="label">Followers</span>
			<span class="value">{user.followers}</span>
		</div>
		<div class="stat">
			<span class="label">Following</span>
			<span class="value">{user.following}</span>
		</div>
	</div>
{/snippet}

{#snippet userActions(user, onFollow, onMessage)}
	<div class="actions">
		<button onclick={onFollow}>Follow</button>
		<button onclick={onMessage}>Message</button>
		<button>Share</button>
	</div>
{/snippet}

{#snippet userProfile(user, actions = {}})}
	<div class="user-profile">
		{@render userAvatar(user, 'large')}
		{@render userBio(user)}
		{@render userStats(user)}
		{@render userActions(user, actions.onFollow, actions.onMessage)}
	</div>
{/snippet}

Benefits of decomposition:

  • Reusability: Use userAvatar in comments, listings, headers
  • Testability: Test avatar rendering independently
  • Flexibility: Reorder sections by changing userProfile
  • Maintainability: Change avatar styling in one place
  • Composition: Create variants like compactUserProfile using existing pieces

3. Type Your Component APIs

For any component that other developers will use, always provide TypeScript types for snippet props. This transforms your component API from documentation that might be outdated to enforced contracts.

Without types:

<!-- Table.svelte -->
<script>
	let { data, row, header, footer } = $props()
</script>

<!-- Users must guess:
     - What types do these snippets accept?
     - Which are required?
     - What parameters do they receive?
-->

With types:

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

	interface Props {
		/** Array of data items to display */
		data: T[]
		/** Required: renders table header */
		header: Snippet
		/** Required: renders a single row */
		row: Snippet<[item: T, index: number]>
		/** Optional: renders table footer */
		footer?: Snippet<[totalCount: number]>
	}

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

<table>
	<thead><tr>{@render header()}</tr></thead>
	<tbody>
		{#each data as item, index}
			<tr>{@render row(item, index)}</tr>
		{/each}
	</tbody>
	{#if footer}
		<tfoot><tr>{@render footer(data.length)}</tr></tfoot>
	{/if}
</table>

What this achieves:

  • IDE autocomplete: Developers see parameter types as they type
  • Compile-time errors: Missing required snippets caught before running
  • Self-documentation: JSDoc comments appear in IDE tooltips
  • Refactoring safety: Rename parameters with confidence

4. Consider Snippet Granularity

Finding the right level of abstraction is an art. Too fine-grained and you have snippet soup. Too coarse-grained and you lose reusability.

Too fine-grained:

<!-- AVOID: Over-abstracted -->
{#snippet text(content)}
	<span>{content}</span>
{/snippet}

{#snippet bold(content)}
	<strong>{content}</strong>
{/snippet}

{#snippet italic(content)}
	<em>{content}</em>
{/snippet}

<!-- This is just making HTML harder to read -->
<p>
	{@render text('Hello ')}
	{@render bold('world')}
	{@render text('!')}
</p>

Just right:

<!-- PREFERRED Meaningful abstraction -->
{#snippet formattedMessage(type, text)}
	{#if type === 'success'}
		<div class="message success">
			<span class="icon"></span>
			<span class="text">{text}</span>
		</div>
	{:else if type === 'error'}
		<div class="message error">
			<span class="icon"></span>
			<span class="text">{text}</span>
		</div>
	{:else}
		<div class="message info">
			<span class="icon"></span>
			<span class="text">{text}</span>
		</div>
	{/if}
{/snippet}

<!-- Much more useful -->
{@render formattedMessage('success', 'Profile saved!')}
{@render formattedMessage('error', 'Network error occurred')}

Questions to ask:

  • Will this snippet be reused more than twice?
  • Does it encapsulate meaningful styling or logic?
  • Would extracting it make the code clearer or more obscure?

5. Use Snippets for Conditional Complexity

When conditional logic makes your template hard to read, extract it into snippets:

Before:

<!-- AVOID: Complex conditional logic inline -->
<div class="product">
	{#if product.onSale}
		<div class="price">
			<span class="original">${product.originalPrice}</span>
			<span class="sale">${product.salePrice}</span>
			<span class="savings">Save ${product.originalPrice - product.salePrice}!</span>
		</div>
	{:else if product.comingSoon}
		<div class="price">
			<span class="coming-soon">Coming Soon</span>
			<span class="notify">
				<button>Notify Me</button>
			</span>
		</div>
	{:else if product.outOfStock}
		<div class="price">
			<span class="out-of-stock">Out of Stock</span>
			<span class="price-value">${product.price}</span>
		</div>
	{:else}
		<div class="price">
			<span class="price-value">${product.price}</span>
		</div>
	{/if}
</div>

After:

<!-- PREFERRED Cleaner with snippets -->
{#snippet salePrice(product)}
	<div class="price price-sale">
		<span class="original">${product.originalPrice}</span>
		<span class="sale">${product.salePrice}</span>
		<span class="savings">Save ${product.originalPrice - product.salePrice}!</span>
	</div>
{/snippet}

{#snippet comingSoonPrice()}
	<div class="price price-coming-soon">
		<span class="status">Coming Soon</span>
		<button class="notify-button">Notify Me</button>
	</div>
{/snippet}

{#snippet outOfStockPrice(product)}
	<div class="price price-out-of-stock">
		<span class="status">Out of Stock</span>
		<span class="value">${product.price}</span>
	</div>
{/snippet}

{#snippet regularPrice(product)}
	<div class="price price-regular">
		<span class="value">${product.price}</span>
	</div>
{/snippet}

<div class="product">
	{#if product.onSale}
		{@render salePrice(product)}
	{:else if product.comingSoon}
		{@render comingSoonPrice()}
	{:else if product.outOfStock}
		{@render outOfStockPrice(product)}
	{:else}
		{@render regularPrice(product)}
	{/if}
</div>

Benefits:

  • Readability: The main template shows the decision tree clearly
  • Maintainability: Each price variant is isolated and easy to update
  • Testability: Test each price display variant independently
  • Reusability: Use price snippets elsewhere (e.g., cart, search results)

6. Document Complex Snippets

For snippets with non-obvious behavior, add JSDoc comments:

/** * Renders a user card with avatar, name, and optional bio. * * @param user - The user object to
display * @param showBio - Whether to show the user's bio (default: true) * @param size - Card size
variant: 'compact' | 'normal' | 'expanded' * * @example * {@render userCard(currentUser)}
* {@render userCard(currentUser, false, 'compact')}
*/

{#snippet userCard(user, showBio = true, size = 'normal')}
	<article class="user-card user-card-{size}">
		<img src={user.avatar} alt={user.name} />
		<h3>{user.name}</h3>
		{#if showBio && user.bio}
			<p class="bio">{user.bio}</p>
		{/if}
	</article>
{/snippet}

IDE tooltips will show this documentation when using the snippet.


Common Pitfalls and How to Avoid Them

While snippets are powerful, there are common mistakes developers make when using them. Understanding these pitfalls helps you avoid bugs and confusion.

1. Scope Confusion

The Problem: Trying to use a snippet outside its lexical scope results in “snippet not defined” errors.

<!-- AVOID: ERROR - Snippet used outside its scope -->
{#if userLoggedIn}
	{#snippet userGreeting()}
		<p>Welcome back, {username}!</p>
	{/snippet}

	<div class="header">
		{@render userGreeting()}
	</div>
{:else}
	<!-- ERROR: userGreeting is not defined here! -->
	<div class="header">
		{@render userGreeting()}
	</div>
{/if}

Why it fails: The userGreeting snippet is defined inside the if block, so it only exists within that block. The else branch can’t see it.

Solution 1: Define at Common Ancestor

<!-- PREFERRED Define snippet before the conditional -->
{#snippet userGreeting()}
	<p>Welcome back, {username}!</p>
{/snippet}

{#if userLoggedIn}
	<div class="header">
		{@render userGreeting()}
	</div>
{:else}
	<div class="header">
		{@render userGreeting()}
	</div>
{/if}

Solution 2: Use Different Snippets

Sometimes you actually want different content in each branch:

<!-- PREFERRED Different snippets for different cases -->
{#if userLoggedIn}
	{#snippet loggedInHeader()}
		<p>Welcome back, {username}!</p>
	{/snippet}

	<div class="header">
		{@render loggedInHeader()}
	</div>
{:else}
	{#snippet guestHeader()}
		<p>Welcome, guest!</p>
	{/snippet}

	<div class="header">
		{@render guestHeader()}
	</div>
{/if}

Solution 3: Pass Data Instead of Duplicating

<!-- PREFERRED Single snippet, different data -->
{#snippet header(isLoggedIn, name)}
	{#if isLoggedIn}
		<p>Welcome back, {name}!</p>
	{:else}
		<p>Welcome, guest!</p>
	{/if}
{/snippet}

<div class="header">
	{@render header(userLoggedIn, username)}
</div>

2. Confusing Definition with Rendering

The Problem: Expecting a snippet to render just by defining it.

<!-- AVOID: Nothing appears in the DOM! -->
{#snippet greeting()}
	<p>Hello, world!</p>
{/snippet}

<!-- Where's my paragraph? I defined it above! -->

Why it fails: Snippet definition is like function definition—it creates a template but doesn’t execute it. You must explicitly render it.

Solution:

<!-- PREFERRED Define AND render -->
{#snippet greeting()}
	<p>Hello, world!</p>
{/snippet}

<!-- NOW it appears -->
{@render greeting()}

Mental model: Think of {#snippet} as function and {@render} as calling that function.

3. Forgetting Optional Chaining for Optional Snippets

The Problem: Trying to render an optional snippet without checking if it exists causes runtime errors.

<!-- Component.svelte -->
<script>
	let { header, children } = $props()
</script>

<!-- AVOID: Runtime error if header is undefined! -->
<div>
	{@render header()}
	{@render children()}
</div>

What happens: If parent doesn’t provide header, header() tries to call undefined → runtime error.

Solution 1: Optional Chaining (Recommended)

<!-- PREFERRED Safe: renders nothing if header is undefined -->
<div>
	{@render header?.()}
	{@render children()}
</div>

Solution 2: Explicit Conditional

<!-- PREFERRED Also safe, with optional fallback -->
<div>
	{#if header}
		{@render header()}
	{:else}
		<h2>Default Header</h2>
	{/if}
	{@render children()}
</div>

When to use each:

  • Optional chaining: When you want no output if snippet is missing
  • Conditional: When you want fallback content

4. Passing Wrong Number or Type of Arguments

The Problem: Snippet expects parameters but doesn’t receive them (or receives wrong types).

{#snippet userCard(name, email, avatar)}
	<div class="user-card">
		<img src={avatar} alt={name} />
		<h3>{name}</h3>
		<p>{email}</p>
	</div>
{/snippet}

<!-- AVOID: Missing parameters! -->
{@render userCard()}

<!-- AVOID: Wrong number of parameters! -->
{@render userCard('Alice')}

<!-- PREFERRED Correct usage -->
{@render userCard('Alice', 'alice@example.com', '/avatars/alice.jpg')}

Without TypeScript: These errors only appear at runtime (or produce undefined values silently).

Solution: Add TypeScript Types

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

	interface Props {
		user: Snippet<[name: string, email: string, avatar: string]>
	}

	let { user }: Props = $props()
</script>

<!-- TypeScript now catches parameter mismatches at compile time! -->

5. Modifying Snippet Parameters

The Problem: Trying to reassign snippet parameters inside the snippet.

<!-- AVOID: Parameters are read-only! -->
{#snippet incrementer(count)}
	<button onclick={() => count++}>
		Count: {count}
	</button>
{/snippet}

Why it fails: Snippet parameters are immutable—they’re passed by value, not by reference. Modifying them has no effect on the source.

Solution: Use State or Callbacks

<script>
	let count = $state(0)

	function increment() {
		count++
	}
</script>

<!-- PREFERRED Snippet doesn't modify parameters, calls parent function -->
{#snippet counter(currentCount, onIncrement)}
	<button onclick={onIncrement}>
		Count: {currentCount}
	</button>
{/snippet}

{@render counter(count, increment)}

Or use $state directly:

<script>
	let count = $state(0)
</script>

<!-- PREFERRED Snippet closes over $state, which IS mutable -->
{#snippet counter()}
	<button onclick={() => count++}>
		Count: {count}
	</button>
{/snippet}

{@render counter()}

6. Infinite Recursion

The Problem: Recursive snippets without proper base case cause stack overflow.

<!-- AVOID: DANGER - Infinite recursion! -->
{#snippet countdown(n)}
	<span>{n}...</span>
	{@render countdown(n - 1)}
{/snippet}

{@render countdown(5)}
<!-- Crashes: 5... 4... 3... 2... 1... 0... -1... -2... -3... (forever) -->

Why it fails: No stopping condition—the snippet keeps calling itself indefinitely.

Solution: Always Have a Base Case

<!-- PREFERRED SAFE: Stops at 0 -->
{#snippet countdown(n)}
	{#if n > 0}
		<span>{n}...</span>
		{@render countdown(n - 1)}
	{:else}
		<span>🚀</span>
	{/if}
{/snippet}

{@render countdown(5)}
<!-- Renders: 5... 4... 3... 2... 1... 🚀 -->

For user-generated data, add depth limit:

{#snippet treeNode(node, depth = 0, maxDepth = 10)}
	{#if depth >= maxDepth}
		<span class="truncated">... (max depth)</span>
	{:else if node.children}
		<div>
			{node.name}
			{#each node.children as child}
				{@render treeNode(child, depth + 1, maxDepth)}
			{/each}
		</div>
	{:else}
		<div>{node.name}</div>
	{/if}
{/snippet}

7. Overusing Snippets

The Problem: Creating snippets for things that should just be HTML or components.

<!-- AVOID: Over-engineered: These don't need to be snippets -->
{#snippet paragraph(text)}
	<p>{text}</p>
{/snippet}

{#snippet heading(text)}
	<h2>{text}</h2>
{/snippet}

{#snippet link(href, text)}
	<a {href}>{text}</a>
{/snippet}

<!-- Just write HTML! -->
<p>This is simpler.</p>
<h2>So is this.</h2>
<a href="/about">And this.</a>

When NOT to use snippets:

  • Simple, one-off markup
  • Basic HTML elements with no special logic
  • Things that would be clearer as inline HTML

When TO use snippets:

  • Repeated patterns (2+ times)
  • Complex conditional rendering
  • Markup that needs to be customized by parent components
  • Rendering logic you might extract to a shared library

8. Accessing Non-Existent Snippet Properties

The Problem: Treating snippets like objects with properties.

<!-- AVOID: Snippets are not objects! -->
{#snippet user(data)}
	<p>{data.name}</p>
{/snippet}

<!-- ERROR: Can't access .name, .params, etc. -->
<p>Snippet name: {user.name}</p>

Why it fails: Snippets are compiled rendering functions, not runtime objects with accessible properties.

Solution: Snippets are for rendering, not introspection. If you need metadata, store it separately:

<script>
	const userSnippetMeta = {
		name: 'user',
		description: 'Renders user information'
	}
</script>

{#snippet user(data)}
	<p>{data.name}</p>
{/snippet}

<!-- Use metadata separately -->
<p>Snippet: {userSnippetMeta.name}</p>
{@render user(currentUser)}

Quick Reference

<!-- DEFINING SNIPPETS -->
{#snippet name()}...{/snippet}                   <!-- No parameters -->
{#snippet name(param)}...{/snippet}              <!-- One parameter -->
{#snippet name(a, b, c)}...{/snippet}            <!-- Multiple parameters -->
{#snippet name(param = default)}...{/snippet}   <!-- Default value -->
{#snippet name({ x, y })}...{/snippet}           <!-- Destructured -->

<!-- TYPING (TypeScript) -->
import type { Snippet } from 'svelte';
children: Snippet                                 <!-- No params -->
row: Snippet<[Item]>                              <!-- One param -->
cell: Snippet<[Item, number]>                     <!-- Multiple params -->
header?: Snippet                                  <!-- Optional -->

<!-- EXPORTING (Svelte 5.5.0+) -->
<script module>
	export { snippetName }
</script>

Conclusion

The {#snippet} block fundamentally transforms how we think about component composition in Svelte. By treating UI fragments as first-class values that can be defined, passed, and invoked programmatically, snippets enable architectural patterns that were previously cumbersome or impossible with the slot-based system. From simple content projection to complex recursive structures, from component library APIs to advanced composition patterns, snippets provide the primitive building blocks for flexible, maintainable component systems.

Mastering snippets requires understanding their scope rules, parameter patterns, and integration with Svelte’s reactive system. The ability to define snippets inline, pass them as props, export them from components, and compose them recursively opens up new possibilities for component API design. Whether you’re building design systems with variant-based rendering, implementing recursive tree components, or creating flexible layout systems with multiple content slots, snippets provide the foundation for expressing complex composition patterns with clarity and type safety.

Key Takeaways

  • {#snippet} defines reusable template fragments with parameters, acting as template-level functions that can be passed as props, stored in variables, or exported from components
  • Snippet parameters enable data passing with {#snippet card({ title, description })} syntax, receiving arguments when invoked via {@render card({ title: 'Hello', description: 'World' })}
  • Scope follows lexical rules - snippets access variables from their defining scope, not the calling scope, enabling closures over component state
  • Multiple snippets provide flexible component APIs - components accept multiple snippet props for headers, footers, content areas, enabling customizable layouts without prop drilling
  • Recursive snippets require base cases to prevent infinite loops: {#if condition}{@render self(recursiveData)}{/if} ensures termination
  • Type safety with TypeScript using Snippet type from svelte package: let { header }: { header: Snippet<[string]> } = $props() for type-checked parameters
  • Export snippets for external use by listing their names in an export { snippetName } statement inside a <script module> block, making them importable across your application like any other module export
  • Snippets are reactive - they re-render automatically when any reactive state they close over changes, just like regular template expressions; the rendering is always in sync with the current state

See Also