Slots & Composition
Component composition is how you build complex UIs from simple pieces. Svelte provides slots for projecting content into components and snippets for flexible template reuse. In Svelte 5, the children snippet pattern is the modern approach, but understanding all composition tools helps you choose the right one for each situation.
Default Slots
A slot is a placeholder in a child component where the parent can inject content:
<!-- Card.svelte -->
<div class="card">
<slot />
</div>
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
</style>
<!-- Parent.svelte -->
<script>
import Card from './Card.svelte';
</script>
<Card>
<h2>Product Name</h2>
<p>This is a product description.</p>
</Card>
Everything between <Card> and </Card> replaces the <slot /> in the child. If no content is provided, you can define fallback content:
<!-- Card.svelte -->
<div class="card">
<slot>
<p>No content provided.</p>
</slot>
</div>
Named Slots
When a component needs content in multiple locations, use named slots:
<!-- Modal.svelte -->
<script>
let { open = $bindable(false) }: { open: boolean } = $props();
</script>
{#if open}
<div class="overlay" onclick={() => open = false}>
<div class="modal" onclick|stopPropagation>
<header>
<slot name="header">
<h2>Modal</h2>
</slot>
</header>
<div class="body">
<slot />
</div>
<footer>
<slot name="footer">
<button onclick={() => open = false}>Close</button>
</slot>
</footer>
</div>
</div>
{/if}
<style>
.overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.5);
display: grid; place-items: center;
}
.modal {
background: white; border-radius: 8px;
padding: 1.5rem; min-width: 400px;
}
</style>
<!-- Parent.svelte -->
<script>
import Modal from './Modal.svelte';
let showModal = $state(false);
</script>
<button onclick={() => showModal = true}>Open Modal</button>
<Modal bind:open={showModal}>
<h2 slot="header">Confirm Deletion</h2>
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
<div slot="footer">
<button onclick={() => showModal = false}>Cancel</button>
<button onclick={handleDelete} class="danger">Delete</button>
</div>
</Modal>
The default (unnamed) <slot /> receives content without a slot attribute. Named slots receive content marked with slot="name".
Slot Props
Slots can pass data back to the parent, enabling the child to expose internal state:
<!-- Tabs.svelte -->
<script>
let { tabs }: { tabs: string[] } = $props();
let activeIndex = $state(0);
</script>
<div class="tabs">
<div class="tab-headers">
{#each tabs as tab, i}
<button class:active={i === activeIndex} onclick={() => activeIndex = i}>
{tab}
</button>
{/each}
</div>
<div class="tab-content">
<slot activeTab={tabs[activeIndex]} {activeIndex} />
</div>
</div>
<style>
.active { font-weight: bold; border-bottom: 2px solid currentColor; }
</style>
<!-- Parent.svelte -->
<script>
import Tabs from './Tabs.svelte';
</script>
<Tabs tabs={['Profile', 'Settings', 'Billing']} let:activeTab let:activeIndex>
<p>Currently viewing: {activeTab} (tab {activeIndex})</p>
</Tabs>
The let:activeTab syntax receives the data passed through the slot. This pattern is useful for headless components that manage logic but leave rendering to the parent.
The Children Snippet Pattern (Svelte 5)
In Svelte 5, content passed between component tags is available as a children snippet. This is the modern alternative to the default slot:
<!-- Card.svelte -->
<script>
let { children, title } = $props();
</script>
<div class="card">
<h2>{title}</h2>
{@render children()}
</div>
<!-- Parent.svelte -->
<script>
import Card from './Card.svelte';
</script>
<Card title="Welcome">
<p>This content becomes the children snippet.</p>
</Card>
The children prop is automatically a snippet when the parent places content between the component tags. You render it with {@render children()}.
Named Snippets as Props
For multiple content areas, pass snippets as named props:
<!-- Layout.svelte -->
<script>
let { sidebar, children } = $props();
</script>
<div class="layout">
<aside>
{@render sidebar()}
</aside>
<main>
{@render children()}
</main>
</div>
<style>
.layout { display: grid; grid-template-columns: 250px 1fr; }
</style>
<!-- Parent.svelte -->
<script>
import Layout from './Layout.svelte';
</script>
<Layout>
{#snippet sidebar()}
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
{/snippet}
<h1>Page Content</h1>
<p>This is the main content area.</p>
</Layout>
Content not inside a named {#snippet} block becomes the children snippet automatically.
Building Real Components with Composition
Accordion
<!-- Accordion.svelte -->
<script>
let { items }: { items: { title: string; content: string }[] } = $props();
let openIndex = $state(-1);
</script>
<div class="accordion">
{#each items as item, i (item.title)}
<div class="item">
<button
class="header"
class:open={openIndex === i}
onclick={() => openIndex = openIndex === i ? -1 : i}
>
{item.title}
<span class="arrow">{openIndex === i ? '\u25B2' : '\u25BC'}</span>
</button>
{#if openIndex === i}
<div class="body">
<p>{item.content}</p>
</div>
{/if}
</div>
{/each}
</div>
Alert with Variants
<!-- Alert.svelte -->
<script lang="ts">
interface Props {
variant?: 'info' | 'warning' | 'error' | 'success';
dismissible?: boolean;
children: any;
}
let { variant = 'info', dismissible = false, children }: Props = $props();
let visible = $state(true);
</script>
{#if visible}
<div class="alert alert-{variant}" role="alert">
<div class="content">
{@render children()}
</div>
{#if dismissible}
<button class="dismiss" onclick={() => visible = false} aria-label="Dismiss">
x
</button>
{/if}
</div>
{/if}
<style>
.alert { padding: 1rem; border-radius: 4px; display: flex; gap: 1rem; }
.alert-info { background: #e3f2fd; color: #1565c0; }
.alert-warning { background: #fff3e0; color: #e65100; }
.alert-error { background: #fce4ec; color: #c62828; }
.alert-success { background: #e8f5e9; color: #2e7d32; }
.content { flex: 1; }
.dismiss { background: none; border: none; cursor: pointer; font-size: 1.2rem; }
</style>
<Alert variant="error" dismissible>
<strong>Error:</strong> Failed to save your changes. Please try again.
</Alert>
When to Use Slots vs Props vs Components
Use slots/snippets when:
- The parent needs to control the rendered markup.
- The content varies significantly between uses.
- You are building layout components (cards, modals, pages).
Use props when:
- The data is structured and the child controls how it is rendered.
- The customization is limited (a title string, an icon name, a variant).
- Type safety for the data matters more than flexibility.
Extract a new component when:
- A piece of UI has its own state or logic.
- It is reused across multiple parent components.
- The template is getting too long to read.
A practical guideline: if you are passing more than two or three snippets to a component, the abstraction might be wrong. Consider whether the component is trying to do too much.
Common Pitfalls
- Mixing slot syntax with snippet syntax: In Svelte 5, both work, but mixing them in the same component creates confusion. Pick one pattern and use it consistently. For new code, prefer snippets.
- Forgetting fallback content: If a slot or snippet might not be provided, handle the missing case. Check if the snippet prop exists before calling
{@render}, or provide default slot content. - Over-abstracting with slots: Not every component needs slots. A button component with a label prop is simpler than a button component with a slot, unless you need to put icons or custom markup inside it.
- Deeply nested slot forwarding: Passing slots through multiple layers of components becomes hard to follow. If you need to forward content through three or more levels, consider context or restructuring your component hierarchy.
- Ignoring accessibility in composed components: When building modals, accordions, and tabs with slots, the accessibility attributes (role, aria-expanded, aria-controls) must still be correct. The composition pattern does not handle this automatically.
Key Takeaways
- Slots project parent content into child component placeholders. Default slots handle the common case; named slots handle multiple content areas.
- Slot props let children expose data to the parent, enabling headless component patterns.
- In Svelte 5, the
childrensnippet pattern is the modern approach to default slot content. Named snippets replace named slots. - Use slots/snippets for flexible layout and presentation. Use props for structured data. Extract components when logic warrants it.
- Composition keeps components focused. A card does not need to know about the content inside it. A modal does not need to know about the form it contains.