Slot Pattern in React
Master the React Slot pattern — default children, named slots, slot maps, vs compound components, Modal/Card/Layout examples, responsive toolbar and toast tasks.
Introduction
The Slot pattern creates named holes inside a component shell — consumers inject JSX into predefined regions without the shell knowing what content will appear. Unlike boolean props (showFooter, renderHeader), slots keep APIs flexible and layouts clean.
This guide covers:
- Definition — placeholders, flexible building, clean APIs
- Single default slot —
childrenprop - Named slots —
header,footer,ReactNodeprops - Named slot map — dynamic slot object
- Slots vs compound components — when each wins
- Common use cases — modals, cards, layouts, tables, CMS
- Practical task — responsive toolbar with collapsing slots
- Practical task — toast system with slot templates
Quick index
| # | Section |
|---|---|
| 1 | Definition |
| 2 | Single default slot |
| 3 | Named slots |
| 4 | Named slot map |
| 5 | Slots vs compound components |
| 6 | Common use cases |
| 7 | Task: Responsive toolbar |
| 8 | Task: Toast slot templates |
| 9 | Best practices & pitfalls |
1. Definition
A slot is a placeholder region inside a component where the consumer places arbitrary JSX. The shell owns structure and styling; the consumer owns content.
┌─────────────────────────────────┐
│ Card (shell) │
│ ┌───────────────────────────┐ │
│ │ header slot ← consumer │ │
│ ├───────────────────────────┤ │
│ │ content slot ← consumer │ │
│ ├───────────────────────────┤ │
│ │ footer slot ← consumer │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘| Concept | Meaning |
|---|---|
| Hole / placeholder | Named or default region awaiting content |
| Consumer injection | Parent passes JSX into specific layout spots |
| Flexible building | Same shell, different content per call site |
| Clean APIs | No showX / renderX prop explosion |
Before — boolean and render props soup
<Modal
showHeader
showFooter
headerTitle="Delete item"
renderBody={() => <p>Are you sure?</p>}
renderFooter={() => <button>Confirm</button>}
/>After — slots
<Modal>
<Modal.Header>Delete item</Modal.Header>
<Modal.Body>Are you sure?</Modal.Body>
<Modal.Footer>
<button type="button">Confirm</button>
</Modal.Footer>
</Modal>2. Single default slot
The simplest slot is children — one region accepting any valid JSX.
import type { ReactNode } from 'react'
type PanelProps = {
children: ReactNode
className?: string
}
export function Panel({ children, className = '' }: PanelProps) {
return <section className={`rounded-lg border bg-white p-4 shadow-sm ${className}`}>{children}</section>
}// Consumer — full control of inner markup
<Panel>
<h2 className="text-lg font-bold">Notifications</h2>
<p className="text-sm text-neutral-600">You have 3 unread messages.</p>
<button type="button" className="mt-2 text-blue-600">
View all
</button>
</Panel>| Characteristic | Detail |
|---|---|
| Prop | Standard children |
| Accepts | Any JSX — elements, strings, fragments |
| Complexity | Lowest — one insertion point |
| Best for | Wrappers, simple containers, layout shells |
3. Named slots
When a shell has fixed regions, expose each as a ReactNode prop — typically header, content, footer.
import type { ReactNode } from 'react'
type PageLayoutProps = {
header?: ReactNode
sidebar?: ReactNode
children: ReactNode
footer?: ReactNode
}
export function PageLayout({ header, sidebar, children, footer }: PageLayoutProps) {
return (
<div className="flex min-h-screen flex-col">
{header && <header className="border-b px-6 py-4">{header}</header>}
<div className="flex flex-1">
{sidebar && <aside className="w-64 shrink-0 border-r p-4">{sidebar}</aside>}
<main className="flex-1 p-6">{children}</main>
</div>
{footer && <footer className="border-t px-6 py-4 text-sm text-neutral-500">{footer}</footer>}
</div>
)
}<PageLayout header={<AppHeader />} sidebar={<NavLinks />} footer={<Copyright />}>
<DashboardContent />
</PageLayout>Modal with named slots
type ModalProps = {
open: boolean
onClose: () => void
header?: ReactNode
footer?: ReactNode
children: ReactNode
}
export function Modal({ open, onClose, header, footer, children }: ModalProps) {
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" role="dialog" aria-modal="true">
<div className="w-full max-w-md rounded-lg bg-white shadow-xl">
{header && (
<header className="flex items-center justify-between border-b px-4 py-3">
<div className="font-bold">{header}</div>
<button type="button" aria-label="Close" onClick={onClose}>
×
</button>
</header>
)}
<div className="px-4 py-4">{children}</div>
{footer && <footer className="flex justify-end gap-2 border-t px-4 py-3">{footer}</footer>}
</div>
</div>
)
}4. Named slot map
Pass an object of slots as a single prop when regions are dynamic or numerous — avoids prop explosion.
import type { ReactNode } from 'react'
type SlotMap = Record<string, ReactNode>
type BlockLayoutProps = {
slots: SlotMap
order?: string[]
}
export function BlockLayout({ slots, order }: BlockLayoutProps) {
const keys = order ?? Object.keys(slots)
return (
<article className="space-y-4">
{keys.map((key) =>
slots[key] ? (
<div key={key} data-slot={key}>
{slots[key]}
</div>
) : null,
)}
</article>
)
}// CMS block — editor defines which slots exist
<BlockLayout
order={['hero', 'body', 'cta']}
slots={{
hero: <img src="/banner.jpg" alt="Launch" className="rounded-lg" />,
body: (
<>
<h2>New feature release</h2>
<p>Ship faster with slot-based layouts.</p>
</>
),
cta: (
<button type="button" className="rounded bg-black px-4 py-2 text-white">
Get started
</button>
),
}}
/>Typed slot map for design systems
type CardSlots = {
media?: ReactNode
title?: ReactNode
description?: ReactNode
actions?: ReactNode
}
type CardProps = {
slots: CardSlots
}
export function Card({ slots }: CardProps) {
return (
<div className="overflow-hidden rounded-lg border">
{slots.media && <div className="aspect-video bg-neutral-100">{slots.media}</div>}
<div className="p-4">
{slots.title && <h3 className="font-bold">{slots.title}</h3>}
{slots.description && <p className="mt-1 text-sm">{slots.description}</p>}
{slots.actions && <div className="mt-4 flex gap-2">{slots.actions}</div>}
</div>
</div>
)
}5. Slots vs compound components
Both patterns compose flexible UI — they differ in mechanism and state sharing.
| Dimension | Slot pattern | Compound components |
|---|---|---|
| Mechanism | Props inject content into regions | Sub-components compose via JSX tree |
| Exposure | Shell + slot props or map | Parent + Parent.Sub dot notation |
| Shared state | None by default | Context shares open/active/index |
| Complexity | Lower — layout-only | Higher — coordination logic |
| Relationship | Compound components often use slots internally | Tabs, Accordion = compound + slots |
Slot pattern — prop injection
<Toolbar left={<BackButton />} center={<SearchInput />} right={<UserMenu />} />Compound components — composition tree
<Tabs defaultTab="overview">
<Tabs.List>
<Tabs.Tab id="overview">Overview</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="overview">Content…</Tabs.Panel>
</Tabs>6. Common use cases
| Use case | Typical slots | Pattern type |
|---|---|---|
| Modals / dialogs | header, body, footer | Named slots |
| Cards | media, title, body, actions | Named slots or map |
| Layouts | header, sidebar, main, footer | Named slots |
| Toolbars | left, center, right | Named slots |
| Forms | label, input, icon, error | Named slots per field |
| Data tables | cell, row, empty state | Render slot props |
| Toasts / alerts | icon, title, message, action | Slot map templates |
| CMS content blocks | dynamic block keys | Named slot map |
Form field with slots
type FieldProps = {
label?: ReactNode
icon?: ReactNode
error?: ReactNode
children: ReactNode
}
function Field({ label, icon, error, children }: FieldProps) {
return (
<label className="block space-y-1">
{label && <span className="text-sm font-medium">{label}</span>}
<div className="relative flex items-center">
{icon && <span className="absolute left-3 text-neutral-400">{icon}</span>}
<div className={icon ? 'pl-10' : ''}>{children}</div>
</div>
{error && <span className="text-sm text-red-600">{error}</span>}
</label>
)
}
// Usage
;<Field label="Email" icon={<MailIcon />} error={errors.email}>
<input type="email" className="w-full rounded border px-3 py-2" />
</Field>Data table cell slot
type Column<T> = {
key: keyof T
header: ReactNode
cell?: (row: T) => ReactNode
}
function DataTable<T extends { id: string }>({ rows, columns }: { rows: T[]; columns: Column<T>[] }) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
{columns.map((col) => (
<td key={String(col.key)}>{col.cell ? col.cell(row) : String(row[col.key])}</td>
))}
</tr>
))}
</tbody>
</table>
)
}7. Task: Responsive toolbar
Build a toolbar with left, center, and right slots that collapse on narrow viewports.
'use client'
import type { ReactNode } from 'react'
type ToolbarProps = {
left?: ReactNode
center?: ReactNode
right?: ReactNode
}
export function Toolbar({ left, center, right }: ToolbarProps) {
return (
<div className="flex flex-wrap items-center gap-2 border-b px-4 py-2">
<div className="flex min-w-0 flex-1 items-center gap-2">{left}</div>
<div className="hidden flex-1 justify-center md:flex">{center}</div>
<div className="flex shrink-0 items-center gap-2">{right}</div>
{/* Mobile — center slot moves below */}
{center && <div className="w-full md:hidden">{center}</div>}
</div>
)
}<Toolbar
left={
<>
<button type="button" aria-label="Back">
←
</button>
<h1 className="truncate font-semibold">Project Alpha</h1>
</>
}
center={<input placeholder="Search…" className="w-full max-w-xs rounded border px-3 py-1" />}
right={
<>
<button type="button">Share</button>
<button type="button">Settings</button>
</>
}
/>8. Task: Toast slot templates
A toast host renders slot templates per variant — consumers publish content into fixed regions.
'use client'
import type { ReactNode } from 'react'
type ToastSlots = {
icon?: ReactNode
title?: ReactNode
message?: ReactNode
action?: ReactNode
}
type ToastVariant = 'success' | 'error' | 'info'
type ToastPayload = {
id: string
variant: ToastVariant
slots: ToastSlots
}
const variantStyles: Record<ToastVariant, string> = {
success: 'border-green-500 bg-green-50',
error: 'border-red-500 bg-red-50',
info: 'border-blue-500 bg-blue-50',
}
function ToastItem({ toast, onDismiss }: { toast: ToastPayload; onDismiss: (id: string) => void }) {
return (
<div
role="alert"
className={`flex items-start gap-3 rounded-lg border-l-4 p-4 shadow ${variantStyles[toast.variant]}`}>
{toast.slots.icon && <div className="shrink-0">{toast.slots.icon}</div>}
<div className="min-w-0 flex-1">
{toast.slots.title && <p className="font-bold">{toast.slots.title}</p>}
{toast.slots.message && <p className="text-sm">{toast.slots.message}</p>}
</div>
{toast.slots.action}
<button type="button" aria-label="Dismiss" onClick={() => onDismiss(toast.id)} className="shrink-0">
×
</button>
</div>
)
}// Trigger from anywhere — slot-based payload
function SaveButton() {
function handleSave() {
showToast({
id: crypto.randomUUID(),
variant: 'success',
slots: {
icon: <span aria-hidden>✓</span>,
title: 'Saved',
message: 'Your changes are live.',
action: (
<button type="button" className="text-sm underline">
Undo
</button>
),
},
})
}
return (
<button type="button" onClick={handleSave}>
Save
</button>
)
}9. Best practices & pitfalls
| Practice / pitfall | Guidance |
|---|---|
| Too many slot props | Switch to slot map when regions exceed ~5 |
| Slots for stateful widgets | Use compound components when subs share state |
| Optional slots | Conditional render — no empty padded gaps |
| Untyped slot maps | Define type CardSlots = { … } for autocomplete |
| Mixing patterns | Document when shell uses props vs children vs map |
Summary
| Variation | API shape | Best for |
|---|---|---|
| Default slot | children | Simple wrappers |
| Named slots | header, footer, … | Modal, layout, toolbar |
| Named slot map | slots={{ hero, body, cta }} | CMS blocks, dynamic templates |
The Slot pattern creates flexible component shells with clean insertion points. Use named props for fixed layouts, slot maps when regions are dynamic, and graduate to compound components when sub-parts must share state.
Next reads: Compound Components Pattern for shared implicit state, Container-Presenter Pattern for splitting shell from data logic, and React Design Patterns for the full pattern roadmap.
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime