Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 010 / 100

Snippets: Svelte 5's slots — what developers should know

Reusable markup inside components.

Expona Admin6 min read

Snippets: Svelte 5's slots — what developers should know

Svelte 5 introduces snippets, a powerful way to pass reusable markup between components that fundamentally changes how we think about component composition. If you've worked with slots in earlier versions, snippets feel like the natural evolution—more flexible, more composable, and easier to reason about. By understanding snippets, you'll write cleaner component APIs and avoid prop drilling patterns that plague many component hierarchies.

What is a snippet?

A snippet is a named block of markup that you can define once and render multiple times, or pass down to child components. Think of it as a reusable template fragment. Unlike slots in Svelte 4, snippets are explicit, named, and behave like regular JavaScript values. This means you can pass them around, conditionally render them, or compose them together without fighting the framework.

At its core, a snippet is declared using the {#snippet} block syntax:

typescript
<script>
  let items = [
    { id: 1, name: 'Alice', role: 'Engineer' },
    { id: 2, name: 'Bob', role: 'Designer' },
    { id: 3, name: 'Carol', role: 'Manager' }
  ];
</script>

{#snippet row(item)}
  <tr>
    <td>{item.id}</td>
    <td>{item.name}</td>
    <td>{item.role}</td>
  </tr>
{/snippet}

In this example, the snippet row accepts a single parameter item and outputs a table row. The snippet captures variables from its surrounding scope (like items), and you can pass parameters just like a function. This makes snippets incredibly powerful for data-driven UI patterns.

Rendering snippets with @render

Once you've defined a snippet, you render it using the {@render} tag. This is where snippets show their flexibility compared to traditional slots—you control exactly when and where markup appears, and you can pass data directly.

Here's how you'd render the snippet we defined above:

typescript
<script>
  let items = [
    { id: 1, name: 'Alice', role: 'Engineer' },
    { id: 2, name: 'Bob', role: 'Designer' },
    { id: 3, name: 'Carol', role: 'Manager' }
  ];
</script>

<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    {#each items as item (item.id)}
      {#snippet row(item)}
        <tr>
          <td>{item.id}</td>
          <td>{item.name}</td>
          <td>{item.role}</td>
        </tr>
      {/snippet}
      {@render row(item)}
    {/each}
  </tbody>
</table>

Notice the {@render row(item)} call. This executes the snippet and inserts its output. You're passing item as an argument, which the snippet receives as its parameter. This pattern is cleaner than slot props in Svelte 4 because the intent is explicit and the data flow is obvious.

In real-world scenarios, this is invaluable. Imagine building a list component that needs to render items in different ways depending on context. Without snippets, you'd either create multiple components or use complex slot prop logic. With snippets, you simply define how each row should render and pass that definition around.

Passing snippets to child components

The real power of snippets emerges when you pass them down to child components. This is where they replace the slot system entirely. Instead of dealing with slot scoping rules and named slots, you pass a snippet as a prop, and the child component renders it wherever needed.

Let's build a reusable DataTable component that accepts a snippet:

typescript
<!-- DataTable.svelte -->
<script>
  export let items;
  export let rowSnippet;
</script>

<table class="data-table">
  <tbody>
    {#each items as item (item.id)}
      {@render rowSnippet(item)}
    {/each}
  </tbody>
</table>

Now, in a parent component, you define what each row looks like and pass that definition:

typescript
<!-- App.svelte -->
<script>
  import DataTable from './DataTable.svelte';

  let employees = [
    { id: 1, name: 'Alice', department: 'Engineering' },
    { id: 2, name: 'Bob', department: 'Design' },
    { id: 3, name: 'Carol', department: 'Management' }
  ];
</script>

{#snippet employeeRow(employee)}
  <tr>
    <td>{employee.name}</td>
    <td>{employee.department}</td>
    <td>
      <button>Edit</button>
    </td>
  </tr>
{/snippet}

<DataTable items={employees} rowSnippet={employeeRow} />

This is dramatically simpler than managing slot scoping. The parent has complete control over the row template, and the child just renders what it receives. You can define multiple different row templates and swap them without changing the component's logic. This pattern scales well for data grids, lists, and any component that needs to delegate presentation to its consumers.

The children snippet — default content

Svelte 5 introduces a special snippet called children. This is the default snippet that represents the component's content when nothing is explicitly passed. It's the modern equivalent of the default slot in older frameworks, but more predictable and composable.

Consider a card component that wants to allow custom content:

typescript
<!-- Card.svelte -->
<script>
  let { children } = $props();
</script>

<div class="card">
  <div class="card-header">
    <h2>Card Title</h2>
  </div>
  <div class="card-body">
    {#if children}
      {@render children()}
    {/if}
  </div>
</div>

When you use the Card component, anything between the opening and closing tags becomes the children snippet:

typescript
<Card>
  <p>This is custom content inside the card.</p>
  <button>Click me</button>
</Card>

The children snippet is automatically available and needs to be destructured from props. You render it like any other snippet using {@render children()}. This approach keeps the API simple while maintaining the compositional power of snippets.

In production systems where you're building component libraries, this pattern avoids the confusion that comes from mixing named slots and default slots. Everything is a snippet, everything is explicit, and the data flow is traceable.

Common mistakes

Forgetting to call the snippet as a function. Snippets are like functions—you must invoke them with parentheses when passing them as props. Writing rowSnippet={row} is correct, but then rendering it as {@render rowSnippet} without parameters won't work. Always use {@render rowSnippet(item)}.

Trying to use snippets outside their scope. Snippets capture variables from their lexical scope. If you define a snippet inside a component and try to access it from a completely different module, you'll run into issues. Snippets are designed for local reuse or explicit prop passing, not global template sharing.

Mixing snippets and conditional rendering incorrectly. Be careful with {#if children} checks—a snippet is always truthy if it exists. Instead, only render it if you know it's defined. Check the component's prop signature to understand what's guaranteed versus optional.

Over-parameterizing snippets. It's tempting to pass lots of context into a snippet, but that often signals you should refactor. If your snippet needs 5+ parameters, consider breaking it into smaller pieces or restructuring the component hierarchy. Snippets are most elegant when they're focused.

Key takeaways

  • Snippets are reusable markup blocks defined with {#snippet name(params)} that replace Svelte 4's slot system.
  • Render snippets using {@render snippetName(args)}, making the intent clear and data flow explicit.
  • Pass snippets as props to child components for powerful, flexible component composition without slot scoping confusion.
  • Use the special children snippet to access default content passed to your component.
  • Snippets capture variables from their surrounding scope, enabling rich data-driven templates.
  • Keep snippets focused and parameterized simply; if they become too complex, break them into smaller pieces.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles