Invoking Snippets with Precision
Where #snippet defines reusable markup blocks, the @render tag brings those definitions to life. This division of labor creates a powerful separation between what content looks like and where it appears.
This tutorial assumes you’re familiar with snippet basics from the #snippet article. Here we focus on invocation patterns: optional rendering, dynamic selection, the children pattern, and building sophisticated component APIs.
Basic Invocation
The simplest use of @render calls a snippet with no arguments:
{#snippet logo()}
<svg viewBox="0 0 100 100" class="logo">
<circle cx="50" cy="50" r="40" fill="currentColor" />
</svg>
{/snippet}
<header>
{@render logo()}
<h1>My Application</h1>
</header>
<footer>
{@render logo()}
</footer> The snippet logo is defined once but rendered twice. Any changes to the definition automatically propagate to both locations.
Rendering with Arguments
Pass data to parameterized snippets:
{#snippet userCard(name, role)}
<div class="user-card">
<h3>{name}</h3>
<span class="role">{role}</span>
</div>
{/snippet}
{@render userCard('Alice Chen', 'Lead Developer')}
{@render userCard('Bob Smith', 'UI Designer')} Arguments flow into the snippet just like function arguments. For complex data, pass objects:
{#snippet productCard(product)}
<article class="product">
<h3>{product.name}</h3>
<p class="price">${product.price.toFixed(2)}</p>
</article>
{/snippet}
{#each products as product (product.id)}
{@render productCard(product)}
{/each} Optional Snippets: Safe Rendering Patterns
A common scenario: a snippet prop may or may not be provided. Attempting to render an undefined snippet throws an error. Svelte provides two patterns for handling this gracefully.
1. Optional Chaining
The simplest approach uses JavaScript’s optional chaining operator:
<script>
let { icon, children } = $props()
</script>
<button class="btn">
{@render icon?.()}
{@render children?.()}
</button> If icon is undefined, icon?.() evaluates to undefined and nothing renders—no error, no empty element. This is perfect when missing content simply means “don’t show anything.”
2. Conditional Rendering with Fallback
When you need fallback content for missing snippets, use an {#if} block:
<script>
let { header, children, footer } = $props()
</script>
<article class="card">
{#if header}
<header class="card-header">
{@render header()}
</header>
{:else}
<header class="card-header">
<span class="default-header">Untitled</span>
</header>
{/if}
<div class="card-body">
{#if children}
{@render children()}
{:else}
<p class="empty-state">No content provided</p>
{/if}
</div>
{@render footer?.()}
</article> This demonstrates both patterns:
headerhas a fallback when missingchildrenhas a different fallback (empty state message)footersimply doesn’t render if absent
Cascading Fallback Chains
Combine conditionals with optional chaining for sophisticated fallback logic:
<script>
let { primaryAction, secondaryAction, tertiaryAction } = $props()
</script>
<div class="action-bar">
{#if primaryAction}
{@render primaryAction()}
{:else if secondaryAction}
{@render secondaryAction()}
{:else}
{@render tertiaryAction?.()}
{#if !tertiaryAction}
<button class="default-action">Continue</button>
{/if}
{/if}
</div> This creates a cascading fallback: try primaryAction, then secondaryAction, then tertiaryAction, and finally a hardcoded default.
Dynamic Snippet Selection
The render tag accepts any expression that evaluates to a snippet—not just identifiers. This enables powerful dynamic patterns.
Ternary Selection
Choose between snippets based on a condition:
<script>
let isExpanded = $state(false)
</script>
{#snippet compactView(item)}
<div class="compact">{item.title}</div>
{/snippet}
{#snippet expandedView(item)}
<div class="expanded">
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
{/snippet}
{#each items as item}
{@render (isExpanded ? expandedView : compactView)(item)}
{/each}
<button onclick={() => (isExpanded = !isExpanded)}>
{isExpanded ? 'Collapse' : 'Expand'} All
</button> The parentheses around the ternary expression are necessary—they group the expression before the invocation ().
Conditional Logic for Multiple Options
For more than two options, use conditional blocks:
<script>
let viewMode = $state('grid') // 'grid' | 'list' | 'table'
</script>
{#snippet gridView(items)}
<div class="grid">
{#each items as item}
<div class="grid-card">{item.name}</div>
{/each}
</div>
{/snippet}
{#snippet listView(items)}
<ul class="list">
{#each items as item}
<li class="list-item">{item.name}</li>
{/each}
</ul>
{/snippet}
{#snippet tableView(items)}
<table>
<tbody>
{#each items as item}
<tr><td>{item.name}</td><td>{item.status}</td></tr>
{/each}
</tbody>
</table>
{/snippet}
{#if viewMode === 'grid'}
{@render gridView(items)}
{:else if viewMode === 'list'}
{@render listView(items)}
{:else}
{@render tableView(items)}
{/if} Function-Based Selection
For logic-heavy selection, wrap the decision in a function:
<script>
function getRenderer(item) {
if (item.type === 'image') return imageRenderer
if (item.type === 'video') return videoRenderer
return fallbackRenderer
}
</script>
{#snippet imageRenderer(item)}
<img src={item.src} alt={item.alt} />
{/snippet}
{#snippet videoRenderer(item)}
<video src={item.src} controls></video>
{/snippet}
{#snippet fallbackRenderer(item)}
<div class="unknown-type">Unsupported: {item.type}</div>
{/snippet}
{#each mediaItems as item}
{@render getRenderer(item)(item)}
{/each} The double parentheses—getRenderer(item)(item)—first call the function to get a snippet, then invoke that snippet.
Using $derived for Dynamic Selection
Combine $derived with snippet selection for reactive, computed rendering:
<script>
let { items, filter = '' } = $props()
let filteredItems = $derived(
items.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase()))
)
let hasResults = $derived(filteredItems.length > 0)
</script>
{#snippet itemList(items)}
<ul>
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
</ul>
{/snippet}
{#snippet emptyState()}
<div class="empty">
<p>No items found matching "{filter}"</p>
</div>
{/snippet}
{@render (hasResults ? itemList : emptyState)(filteredItems)} The children Snippet Pattern
When content is placed inside component tags without a #snippet wrapper, Svelte automatically creates a snippet named children:
<!-- Parent component usage -->
<Card>
<h2>Welcome Back</h2>
<p>You have 3 new notifications.</p>
</Card> <!-- Card.svelte -->
<script>
let { children } = $props()
</script>
<article class="card">
{@render children?.()}
</article> Passing Data Back Through Children
Components can pass data back to the children snippet, enabling “renderless” or “headless” component patterns:
<!-- DataProvider.svelte -->
<script>
let { url, children } = $props()
let data = $state(null)
let loading = $state(true)
let error = $state(null)
$effect(() => {
loading = true
fetch(url)
.then((r) => r.json())
.then((d) => {
data = d
loading = false
})
.catch((e) => {
error = e
loading = false
})
})
</script>
{@render children?.({ data, loading, error })} <!-- Usage -->
<DataProvider url="/api/users">
{#snippet children({ data, loading, error })}
{#if loading}
<p>Loading...</p>
{:else if error}
<p>Error: {error.message}</p>
{:else}
<ul>
{#each data as user}
<li>{user.name}</li>
{/each}
</ul>
{/if}
{/snippet}
</DataProvider> The DataProvider manages logic while delegating rendering entirely to consumers.
Component API Design Patterns
When building reusable components, snippets and the render tag enable flexible content projection and customization. There are two main patterns for passing snippets as props.
Explicit vs Implicit Snippet Props
Snippets can be passed both explicitly (as named props) and implicitly (defined inside component tags):
Explicit:
{#snippet header()}
<th>Name</th><th>Email</th>
{/snippet}
<Table data={users} {header} /> Implicit:
<Table data={users}>
{#snippet header()}
<th>Name</th><th>Email</th>
{/snippet}
</Table> Both approaches result in identical behavior—the choice is stylistic. Implicit props group related snippets visually with their target component.
Flexible Component with Multiple Render Points
<!-- DataTable.svelte -->
<script>
let { data, header, row, emptyState } = $props()
</script>
<table class="data-table">
{#if header}
<thead>{@render header()}</thead>
{/if}
<tbody>
{#if data.length > 0}
{#each data as item (item.id)}
{@render row(item)}
{/each}
{:else if emptyState}
<tr><td colspan="100%">{@render emptyState()}</td></tr>
{:else}
<tr><td colspan="100%">No data available</td></tr>
{/if}
</tbody>
</table> Usage:
<DataTable data={users}>
{#snippet header()}
<tr><th>Name</th><th>Email</th><th>Actions</th></tr>
{/snippet}
{#snippet row(user)}
<tr>
<td>{user.name}</td>
<td>{user.email}</td>
<td><button onclick={() => edit(user)}>Edit</button></td>
</tr>
{/snippet}
{#snippet emptyState()}
<div class="empty">
<img src="/empty-table.svg" alt="" />
<p>No users found</p>
</div>
{/snippet}
</DataTable> Real-World Patterns
In real-world applications, the render tag and snippets enable advanced patterns for component composition, state containers, and recursive structures. Let’s explore practical examples you’ll encounter in production applications.
1. Layout Composition
Layout components often accept multiple snippet props for different regions:
<!-- Layout.svelte -->
<script>
let { header, sidebar, children, footer } = $props()
</script>
<div class="layout">
<header class="layout-header">
{#if header}
{@render header()}
{:else}
<nav class="default-nav"><a href="/">Home</a></nav>
{/if}
</header>
<div class="layout-body">
{#if sidebar}
<aside class="layout-sidebar">{@render sidebar()}</aside>
{/if}
<main class="layout-main">{@render children?.()}</main>
</div>
{@render footer?.()}
</div> 2. Modal/Dialog Pattern
Modals benefit from snippet-based content projection and $bindable for state management:
<!-- Modal.svelte -->
<script>
let { isOpen = $bindable(false), title, children, footer } = $props()
let dialogEl = $state(null)
$effect(() => {
if (isOpen && dialogEl) {
dialogEl.showModal()
} else if (!isOpen && dialogEl) {
dialogEl.close()
}
})
function handleClose() {
isOpen = false
}
</script>
<dialog bind:this={dialogEl} onclose={handleClose}>
<header>
{#if title}
<h2>{title}</h2>
{/if}
<button onclick={handleClose} aria-label="Close">×</button>
</header>
<div class="content">
{@render children?.()}
</div>
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</dialog>
<style>
dialog {
max-width: 500px;
border-radius: 8px;
border: 1px solid #ccc;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
</style> Usage:
<script>
let showModal = $state(false)
function handleDelete() {
// Delete logic
showModal = false
}
</script>
<button onclick={() => (showModal = true)}>Delete Item</button>
<Modal bind:isOpen={showModal} title="Confirm Action">
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
{#snippet footer()}
<button onclick={() => (showModal = false)}>Cancel</button>
<button onclick={handleDelete} class="danger">Delete</button>
{/snippet}
</Modal> 3. Tab Component Pattern
Tabs demonstrate dynamic snippet selection with state management:
<!-- Tabs.svelte -->
<script>
let { tabs = [], activeTab = $bindable(0), children } = $props()
</script>
<div class="tabs">
<div role="tablist">
{#each tabs as tab, index}
<button
role="tab"
aria-selected={activeTab === index}
tabindex={activeTab === index ? 0 : -1}
onclick={() => (activeTab = index)}
>
{tab}
</button>
{/each}
</div>
<div role="tabpanel">
{@render children?.(activeTab)}
</div>
</div>
<style>
.tabs {
border: 1px solid #ddd;
border-radius: 4px;
}
[role='tablist'] {
display: flex;
gap: 4px;
border-bottom: 1px solid #ddd;
padding: 8px;
}
[role='tab'] {
padding: 8px 16px;
border: none;
background: transparent;
cursor: pointer;
}
[role='tab'][aria-selected='true'] {
border-bottom: 2px solid blue;
}
[role='tabpanel'] {
padding: 16px;
}
</style> Usage:
<script>
let activeTab = $state(0)
</script>
<Tabs tabs={['Profile', 'Settings', 'Notifications']} bind:activeTab>
{#snippet children(index)}
{#if index === 0}
<h3>Profile</h3>
<p>Manage your profile information</p>
{:else if index === 1}
<h3>Settings</h3>
<p>Configure your preferences</p>
{:else}
<h3>Notifications</h3>
<p>Manage notification settings</p>
{/if}
{/snippet}
</Tabs> 4. Loading State Pattern
Handle loading, error, and success states elegantly:
<!-- LoadingState.svelte -->
<script>
let { loading, error, data, skeleton, errorFallback, children } = $props()
</script>
{#if loading}
{#if skeleton}
{@render skeleton()}
{:else}
<div class="skeleton-loader" aria-busy="true" aria-live="polite">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
{/if}
{:else if error}
{#if errorFallback}
{@render errorFallback(error)}
{:else}
<div class="error-state" role="alert">
<p>Something went wrong: {error.message}</p>
</div>
{/if}
{:else}
{@render children?.(data)}
{/if}
<style>
.skeleton-loader {
padding: 16px;
}
.skeleton-line {
height: 16px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
margin-bottom: 8px;
border-radius: 4px;
}
.skeleton-line.short {
width: 60%;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style> Usage:
<script>
let users = $state(null)
let loading = $state(true)
let error = $state(null)
$effect(() => {
fetch('/api/users')
.then((r) => r.json())
.then((data) => {
users = data
loading = false
})
.catch((err) => {
error = err
loading = false
})
})
</script>
<LoadingState {loading} {error} data={users}>
{#snippet skeleton()}
<div class="user-skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-text"></div>
<div class="skeleton-text short"></div>
</div>
{/snippet}
{#snippet errorFallback(err)}
<div class="custom-error">
<h3>Failed to load users</h3>
<p>{err.message}</p>
<button onclick={() => window.location.reload()}>Retry</button>
</div>
{/snippet}
{#snippet children(userData)}
<ul class="user-list">
{#each userData as user (user.id)}
<li>{user.name}</li>
{/each}
</ul>
{/snippet}
</LoadingState> 5. Toast Notification System
Build a flexible notification system with customizable rendering:
<!-- ToastContainer.svelte -->
<script>
let { toasts = [], toast, position = 'top-right' } = $props()
function removeToast(id) {
toasts = toasts.filter((t) => t.id !== id)
}
</script>
<div class="toast-container toast-{position}">
{#each toasts as item (item.id)}
{#if toast}
{@render toast(item, removeToast)}
{:else}
<div class="toast toast-{item.type}">
{item.message}
<button onclick={() => removeToast(item.id)} aria-label="Close notification"> × </button>
</div>
{/if}
{/each}
</div>
<style>
.toast-container {
position: fixed;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
}
.toast-top-right {
top: 0;
right: 0;
}
.toast {
background: white;
padding: 12px 16px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 8px;
min-width: 300px;
}
.toast-success {
border-left: 4px solid green;
}
.toast-error {
border-left: 4px solid red;
}
</style> Usage with custom rendering:
<script>
let toasts = $state([])
let nextId = $state(1)
function addToast(type, title, message) {
toasts = [...toasts, { id: nextId++, type, title, message }]
}
</script>
<ToastContainer {toasts}>
{#snippet toast(item, onClose)}
<article class="custom-toast" class:success={item.type === 'success'}>
<header>
<strong>{item.title}</strong>
<button onclick={() => onClose(item.id)} aria-label="Close">×</button>
</header>
<p>{item.message}</p>
</article>
{/snippet}
</ToastContainer>
<button onclick={() => addToast('success', 'Success!', 'Your changes have been saved.')}>
Show Toast
</button> 6. Renderless State Container
A “renderless” component manages state and passes it to children for rendering:
<!-- Toggleable.svelte -->
<script>
let { children, initialOpen = false } = $props()
let isOpen = $state(initialOpen)
function toggle() {
isOpen = !isOpen
}
function open() {
isOpen = true
}
function close() {
isOpen = false
}
</script>
{@render children({ isOpen, toggle, open, close })} Usage:
<Toggleable>
{#snippet children({ isOpen, toggle })}
<div class="accordion">
<button onclick={toggle} aria-expanded={isOpen}>
FAQ: How do I reset my password?
<span>{isOpen ? '−' : '+'}</span>
</button>
{#if isOpen}
<div class="accordion-body">
<p>Click on "Forgot Password" on the login page...</p>
</div>
{/if}
</div>
{/snippet}
</Toggleable> 7. Recursive Rendering (Tree Structures)
Snippets can call themselves to render recursive data structures:
<script>
let tree = $state({
name: 'Root',
children: [
{
name: 'Documents',
children: [{ name: 'Resume.pdf' }, { name: 'Cover-Letter.docx' }]
},
{
name: 'Pictures',
children: [{ name: 'beach.jpg' }, { name: 'sunset.png' }]
}
]
})
</script>
{#snippet treeNode(node, depth = 0)}
<div class="tree-node" style="--depth: {depth}">
<button
onclick={() => {
if (node.children) {
node.expanded = !node.expanded
}
}}
>
<span>{node.children ? (node.expanded ? '📂' : '📁') : '📄'}</span>
<span>{node.name}</span>
</button>
{#if node.children && node.expanded}
{#each node.children as child}
{@render treeNode(child, depth + 1)}
{/each}
{/if}
</div>
{/snippet}
{@render treeNode(tree)}
<style>
.tree-node {
padding-left: calc(var(--depth) * 1.5rem);
}
.tree-node button {
display: flex;
align-items: center;
gap: 8px;
background: none;
border: none;
padding: 4px 8px;
cursor: pointer;
}
.tree-node button:hover {
background: #f0f0f0;
}
</style> TypeScript Integration
Type snippet props for better developer experience and type safety. Svelte provides a Snippet type for this purpose:
<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
data: User[]
children: Snippet // No parameters
row: Snippet<[User]> // One parameter
cell?: Snippet<[User, keyof User]> // Optional, two parameters
}
let { data, children, row, cell }: Props = $props()
</script> For generic type safety:
<script lang="ts" generics="T extends { id: string | number }">
import type { Snippet } from 'svelte'
interface Props {
items: T[]
renderItem: Snippet<[T, number]>
}
let { items, renderItem }: Props = $props()
</script>
<ul>
{#each items as item, index (item.id)}
<li>{@render renderItem(item, index)}</li>
{/each}
</ul> Common Pitfalls
Mastering Svelte’s @render tag unlocks powerful composition patterns, but it’s easy to make subtle mistakes—especially when working with snippets, dynamic selection, or optional content. Understanding these pitfalls will help you avoid bugs and write more maintainable code.
1. Forgetting to Invoke the Snippet
A snippet must always be invoked with parentheses—even if it takes no arguments. Omitting the parentheses will not render the snippet; instead, it just references the snippet function without calling it.
Tip: Always check for parentheses () after your snippet name:
<!-- AVOID: Wrong: Just references the snippet -->
{@render mySnippet}
<!-- PREFERRED: Invokes the snippet -->
{@render mySnippet()} 2. Rendering Undefined Snippets
Attempting to render a snippet that is undefined will throw an error. This commonly happens when you expect a prop or slot to be optional but forget to guard against it being missing.
Solution: Use optional chaining (?.()) or conditional checks:
<!-- AVOID: Throws error if maybeSnippet is undefined -->
{@render maybeSnippet()}
<!-- PREFERRED: Safe with optional chaining -->
{@render maybeSnippet?.()}
<!-- PREFERRED: Safe with conditional -->
{#if maybeSnippet}
{@render maybeSnippet()}
{/if} 3. Misplacing Parentheses in Dynamic Selection
When dynamically selecting between snippets—such as using a ternary expression—it’s a common mistake to invoke both snippets before making the selection. This can lead to unexpected behavior or errors.
Best Practice: Wrap your selection in parentheses before invoking: (condition ? snippetA : snippetB)()
<!-- AVOID: Calls both snippets before selecting -->
{@render condition ? snippetA() : snippetB()}
<!-- PREFERRED: Selects snippet first, then invokes -->
{@render (condition ? snippetA : snippetB)()} 4. Expecting Snippets to Return Values
Snippets are designed solely for rendering UI and do not return values that can be used in JavaScript expressions or assigned to variables. They output DOM elements within the template context.
Remember: Snippets produce DOM, not data. Use them for rendering UI within your template—never expect them to return values for use in JavaScript expressions.
<!-- AVOID: This doesn't work -->
<script>
let result = someSnippet() // Snippets don't return values
</script>
<!-- PREFERRED:Snippets are rendered in templates -->
{@render someSnippet()} 5. Overusing Large or Complex Snippets
While snippets are powerful, overusing them for large or deeply nested content can make your code harder to read, maintain, and optimize. Large snippets become monolithic, mixing multiple concerns and making it difficult to reason about updates or reuse parts of your UI.
Example of an overly complex snippet:
{#snippet dashboard(user, stats, notifications)}
<section class="dashboard">
<header>
<h1>Welcome, {user.name}</h1>
<span>{stats.points} points</span>
</header>
<main>
<div class="stats">
<!-- lots of markup and logic here -->
</div>
<div class="notifications">
{#each notifications as note}
<div class="note">{note.message}</div>
{/each}
</div>
<!-- more nested content -->
</main>
<footer>
<!-- more markup -->
</footer>
</section>
{/snippet} Better approach: Break large snippets into smaller, focused ones:
{#snippet dashboardHeader(user, stats)}
<header>
<h1>Welcome, {user.name}</h1>
<span>{stats.points} points</span>
</header>
{/snippet}
{#snippet dashboardNotifications(notifications)}
<div class="notifications">
{#each notifications as note}
<div class="note">{note.message}</div>
{/each}
</div>
{/snippet}
{#snippet dashboard(user, stats, notifications)}
<section class="dashboard">
{@render dashboardHeader(user, stats)}
<main>
{@render dashboardNotifications(notifications)}
</main>
</section>
{/snippet} Tip: Prefer small, single-purpose snippets and compose them for complex UIs. This improves readability, maintainability, and performance.
Performance Considerations
While the @render tag is highly flexible and enables powerful composition patterns, it’s important to be mindful of performance when using snippets extensively. Each invocation of @render creates new DOM nodes, and unnecessary re-renders can impact responsiveness—especially in large or dynamic UIs.
Render tags are lightweight—essentially function calls that produce DOM. However:
Each
@renderproduces new DOM nodes. If a snippet is expensive and rendered frequently, ensure re-renders are necessary.Use
#keyfor controlled updates when you want to force recreation:
{#key data.id}
{@render expensiveSnippet(data)}
{/key} Keep snippets focused. Large monolithic snippets are harder to optimize than small, composed ones.
Avoid unnecessary snippet invocations in loops. If rendering hundreds of items, ensure your snippet logic is minimal:
<!-- Better: Pre-compute outside the loop -->
<script>
let itemsWithMetadata = $derived(
items.map((item) => ({
...item,
formattedDate: formatDate(item.date),
isExpired: Date.now() > item.expiryTime
}))
)
</script>
<!-- Less optimal: Complex logic in snippet -->
{#snippet itemCard(item)}
<div>
{@const formattedDate = formatDate(item.date)}
{@const isExpired = Date.now() > item.expiryTime}
<!-- ... complex rendering ... -->
</div>
{/snippet}
{#each itemsWithMetadata as item}
{@render itemCard(item)}
{/each} Accessibility Considerations
When using @render for dynamic or optional content, ensure your rendered snippets maintain proper accessibility:
ARIA Roles and Labels
Always set appropriate ARIA attributes within your snippets:
{#snippet accessibleButton(label, action)}
<button type="button" aria-label={label} onclick={action}>
{label}
</button>
{/snippet}
{@render accessibleButton('Close dialog', closeDialog)} Focus Management
For modals and dynamic content, manage focus appropriately:
<!-- Modal.svelte -->
<script>
let { isOpen = $bindable(false), children } = $props()
let dialogEl = $state(null)
$effect(() => {
if (isOpen && dialogEl) {
dialogEl.showModal()
// Focus first focusable element
const firstFocusable = dialogEl.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
firstFocusable?.focus()
}
})
</script> Screen Reader Announcements
Use live regions for dynamic content:
{#snippet loadingStatus(loading)}
<div aria-live="polite" aria-busy={loading}>
{#if loading}
Loading content...
{/if}
</div>
{/snippet} SSR (Server-Side Rendering) Considerations
The @render tag works seamlessly with SvelteKit SSR. However, if your snippets depend on browser-only APIs, guard those usages to avoid hydration errors:
<script>
import { browser } from '$app/environment'
let windowWidth = $state(browser ? window.innerWidth : 0)
</script>
{#snippet clientOnly()}
{#if browser}
<p>Window width: {windowWidth}px</p>
{/if}
{/snippet}
{@render clientOnly()} Best Practice: Move browser API calls into $effect blocks or use SvelteKit’s browser check:
<script>
import { browser } from '$app/environment'
let data = $state(null)
$effect(() => {
if (browser) {
data = localStorage.getItem('key')
}
})
</script> Quick Reference
<!-- Basic invocation -->
{@render snippetName()}
<!-- With arguments -->
{@render snippetName(arg1, arg2)}
<!-- Optional (safe if undefined) -->
{@render snippetName?.()}
<!-- Conditional with fallback -->
{#if snippetName}
{@render snippetName()}
{:else}
<p>Fallback content</p>
{/if}
<!-- Dynamic selection -->
{@render (condition ? snippetA : snippetB)()}
<!-- Children snippet -->
{@render children?.()}
<!-- Children with data -->
{@render children?.({ loading, error, data })}
<!-- With $derived -->
{@render (hasResults ? resultList : emptyState)(filteredData)} Troubleshooting
Error: “Cannot read properties of undefined (reading ‘call’)”
- This usually means you tried to render an undefined snippet
- Solution: Use optional chaining:
{@render maybeSnippet?.()}
Nothing renders
- Did you forget to invoke the snippet with
()? - Always use parentheses:
{@render mySnippet()}
Hydration mismatch in SvelteKit
- If you use browser-only APIs in snippets, wrap them in
$effector check forbrowser - Import
browserfrom$app/environmentand guard your code
Props not reactive
- Make sure to use Svelte 5 runes (
$state,$derived,$effect) for reactivity - Check that parent components are also using runes correctly
Performance issues with many renders
- Pre-compute data with
$derivedbefore passing to snippets - Keep snippets small and focused
- Use
#keyblocks judiciously for controlled updates
Conclusion
The {@render} tag transforms Svelte’s templating model by treating UI fragments as first-class values that can be passed, stored, and invoked programmatically. This paradigm shift from slots to snippets with explicit rendering gives you unprecedented control over component composition patterns. Whether you’re building component libraries with customizable rendering, implementing complex conditional layouts, or creating recursive data structures, {@render} provides the foundation for flexible, maintainable component APIs.
Mastering {@render} means understanding not just the syntax, but the architectural patterns it enables. Optional rendering with null-checking becomes trivial. Dynamic snippet selection based on runtime conditions becomes elegant. Complex delegation patterns where parent components control child rendering become natural. By combining {@render} with $derived for computed snippet selection and proper null-safety patterns, you can build component systems that are both powerful and type-safe.
Key Takeaways
{@render}invokes snippet functions defined with#snippet, passing arguments to snippet parameters for dynamic, reusable UI fragments- Optional rendering requires null-checking using
{#if snippet}or optional chaining:{@render snippet?.()}to prevent runtime errors when snippets are undefined - Arguments are passed in parentheses with type safety:
{@render card({ title, description, imageUrl })}passes data to snippet parameters - Dynamic snippet selection enables runtime-determined rendering patterns like
{@render variants[size]?.()}for variant-based component APIs - Snippets are reactive - when snippet dependencies change, Svelte automatically re-renders the output without manual effect management
- Component delegation patterns allow parent components to control child rendering through snippet props, enabling flexible composition without prop drilling
- SSR-safe by default but browser-only APIs in snippets require
$effectwrapping orbrowserenvironment checks for hydration safety - Performance considerations include pre-computing data with
$derivedbefore passing to snippets and keeping snippet scope minimal for efficient updates
See Also
- Official Svelte 5 Documentation -
{@render} $props()- Component props including snippet props$derived- Reactive computations for snippet selection logic- Component Composition Patterns - Advanced component API design
- TypeScript with Svelte - Type-safe snippet definitions