Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
ReactDesign PatternsSlot PatternComponent CompositionDesign Systems

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.

Jul 5, 202612 min read

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:

  1. Definition — placeholders, flexible building, clean APIs
  2. Single default slot — children prop
  3. Named slots — header, footer, ReactNode props
  4. Named slot map — dynamic slot object
  5. Slots vs compound components — when each wins
  6. Common use cases — modals, cards, layouts, tables, CMS
  7. Practical task — responsive toolbar with collapsing slots
  8. Practical task — toast system with slot templates

Note

Slots solve layout flexibility — where content goes. They do not share implicit state between parts. When tabs or accordion need coordinated open/active state, combine slots with Context — see Compound Components Pattern.

Quick index

#Section
1Definition
2Single default slot
3Named slots
4Named slot map
5Slots vs compound components
6Common use cases
7Task: Responsive toolbar
8Task: Toast slot templates
9Best 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.

plaintext
┌─────────────────────────────────┐
│  Card (shell)                   │
│  ┌───────────────────────────┐  │
│  │ header slot  ← consumer   │  │
│  ├───────────────────────────┤  │
│  │ content slot ← consumer   │  │
│  ├───────────────────────────┤  │
│  │ footer slot  ← consumer   │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘
ConceptMeaning
Hole / placeholderNamed or default region awaiting content
Consumer injectionParent passes JSX into specific layout spots
Flexible buildingSame shell, different content per call site
Clean APIsNo showX / renderX prop explosion

Before — boolean and render props soup

tsx
<Modal
  showHeader
  showFooter
  headerTitle="Delete item"
  renderBody={() => <p>Are you sure?</p>}
  renderFooter={() => <button>Confirm</button>}
/>

After — slots

tsx
<Modal>
  <Modal.Header>Delete item</Modal.Header>
  <Modal.Body>Are you sure?</Modal.Body>
  <Modal.Footer>
    <button type="button">Confirm</button>
  </Modal.Footer>
</Modal>

Tip

If your component API has more than two renderX or showX props, you are describing layout regions — refactor to slots.

Back to index


2. Single default slot

The simplest slot is children — one region accepting any valid JSX.

tsx
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>
}
tsx
// 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>
CharacteristicDetail
PropStandard children
AcceptsAny JSX — elements, strings, fragments
ComplexityLowest — one insertion point
Best forWrappers, simple containers, layout shells

Note

Default children is still the slot pattern — you do not need named props until the shell has multiple distinct regions with fixed layout between them.

Back to index


3. Named slots

When a shell has fixed regions, expose each as a ReactNode prop — typically header, content, footer.

tsx
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>
  )
}
tsx
<PageLayout header={<AppHeader />} sidebar={<NavLinks />} footer={<Copyright />}>
  <DashboardContent />
</PageLayout>

Modal with named slots

tsx
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>
  )
}

Tip

Use named slot props when you have a small, fixed set of regions (2–5 slots) and the shell layout never changes — clear API, easy TypeScript typing.

Back to index


4. Named slot map

Pass an object of slots as a single prop when regions are dynamic or numerous — avoids prop explosion.

tsx
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>
  )
}
tsx
// 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

tsx
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>
  )
}

Warning

If you pass too many individual slot props (slot1, slot2, … slot12), switch to a slot map — the interface stays maintainable and CMS-driven pages can pass dynamic keys.

Note

Optional slots in a map should be guarded with conditional render — {slots.title && …} — so missing CMS fields do not leave empty padded gaps.

Back to index


5. Slots vs compound components

Both patterns compose flexible UI — they differ in mechanism and state sharing.

DimensionSlot patternCompound components
MechanismProps inject content into regionsSub-components compose via JSX tree
ExposureShell + slot props or mapParent + Parent.Sub dot notation
Shared stateNone by defaultContext shares open/active/index
ComplexityLower — layout-onlyHigher — coordination logic
RelationshipCompound components often use slots internallyTabs, Accordion = compound + slots

Slot pattern — prop injection

tsx
<Toolbar left={<BackButton />} center={<SearchInput />} right={<UserMenu />} />

Compound components — composition tree

tsx
<Tabs defaultTab="overview">
  <Tabs.List>
    <Tabs.Tab id="overview">Overview</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel id="overview">Content…</Tabs.Panel>
</Tabs>

Interview Answer

"When do you choose slots over compound components?"

Choose slots when the shell is presentational — Card, Modal, PageLayout, Toolbar — and consumers only need insertion points. Choose compound components when sub-parts must share implicit state (active tab, expanded accordion item, open menu index) without prop drilling.

Tip

Many design systems combine both: compound API for stateful widgets (Dialog.Root, Dialog.Content) where each sub-part is internally a named slot in the shell.

Back to index


6. Common use cases

Use caseTypical slotsPattern type
Modals / dialogsheader, body, footerNamed slots
Cardsmedia, title, body, actionsNamed slots or map
Layoutsheader, sidebar, main, footerNamed slots
Toolbarsleft, center, rightNamed slots
Formslabel, input, icon, errorNamed slots per field
Data tablescell, row, empty stateRender slot props
Toasts / alertsicon, title, message, actionSlot map templates
CMS content blocksdynamic block keysNamed slot map

Form field with slots

tsx
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

tsx
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>
  )
}

Performance

Slot props that receive inline JSX create new element trees each render — fine for layout shells. For heavy slot content, pass memoized subtrees or child components when profiling shows unnecessary re-renders.

Back to index


7. Task: Responsive toolbar

Build a toolbar with left, center, and right slots that collapse on narrow viewports.

tsx
'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>
  )
}
tsx
<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>
    </>
  }
/>

Tip

On mobile, collapse low-priority slots into a overflow menu rather than stacking five toolbars — keep left for wayfinding, defer right actions to a ⋯ dropdown.

Back to index


8. Task: Toast slot templates

A toast host renders slot templates per variant — consumers publish content into fixed regions.

tsx
'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>
  )
}
tsx
// 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>
  )
}

Note

Pair slot-based toasts with Pub-Sub vs Observer Patterns when publishers live in unrelated tree branches — appEvents.publish('toast:show', payload) decouples trigger from ToastHost.

Back to index


9. Best practices & pitfalls

Practice / pitfallGuidance
Too many slot propsSwitch to slot map when regions exceed ~5
Slots for stateful widgetsUse compound components when subs share state
Optional slotsConditional render — no empty padded gaps
Untyped slot mapsDefine type CardSlots = { … } for autocomplete
Mixing patternsDocument when shell uses props vs children vs map

Warning

Do not use slots to pass data callbacks disguised as JSX when a simple prop would suffice — footer={<Submit onClick={submit} />} is fine; forcing every string through a slot obscures the API.

Warning

Named slots without layout consistency — different call sites placing actions in header vs footer — confuse users. Document slot semantics in your design system (header = title + close, footer = primary actions).

Tip

Production checklist

  • Default children for single-region shells
  • Named ReactNode props for 2–5 fixed regions
  • Slot map for CMS / dynamic layouts
  • Compound components when shared state is required
  • Conditional render for optional slots
  • Typed slot interfaces in design systems
  • Responsive collapse plan for toolbar-style slots
  • Pub-Sub for global toast slot hosts

Back to index


Summary

VariationAPI shapeBest for
Default slotchildrenSimple wrappers
Named slotsheader, footer, …Modal, layout, toolbar
Named slot mapslots={{ 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.

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime