Composing UIs with Children, Layouts, and Slots
Learn to build flexible UIs by composing components around children, including layout and wrapper components, an introduction to compound components and render props, and passing components themselves as props.
React's most powerful organizing idea isn't a hook or an API — it's composition: building complex UIs by nesting simple components inside one another rather than configuring one giant component with dozens of props. This lesson focuses on children as the primary composition mechanism, then previews three related patterns — compound components, render props, and component-as-prop — that you'll see in much greater depth later in the course.
children as the composition primitive
Whatever you nest between a component's opening and closing JSX tags arrives inside that component as its children prop. <Card><p>Hello</p></Card> means Card's children is a single <p> element; <Card><p>One</p><p>Two</p></Card> means children is an array of two elements. This is what makes a component like Card reusable for wildly different content without ever needing to know what that content is — Card just needs to know where to put it.
The key insight is that children inverts control: instead of a parent passing a content: string prop and the child deciding how to render it, the parent decides exactly what JSX to render, and the child decides only where and inside what wrapper it goes. This is strictly more flexible — a content prop can only ever be text or a fixed shape, while children can be any JSX tree the caller wants, including other components.
Because children is just a regular prop under the hood, it follows every rule from the Props module: it can be typed as React.ReactNode, it can be optional (children?: React.ReactNode), and a component can read it, transform it, or ignore it entirely.
Layout and wrapper components
A layout component's whole job is to arrange its children — it supplies structure (spacing, alignment, a grid) without caring what content flows through it. <Stack gap={8}><Header /><Body /><Footer /></Stack> is a layout component: Stack knows how to lay three things out vertically with 8px of gap between them, and it does not know or care that those three things are a Header, a Body, and a Footer.
A wrapper component adds behavior or presentation around its children without changing their content — <Tooltip text="Click to save"><Button /></Tooltip> wraps a Button with hover-triggered tooltip behavior, or <ErrorBoundary><Dashboard /></ErrorBoundary> wraps a subtree with crash-catching behavior. The distinguishing feature of both layout and wrapper components is that they never need to know the concrete type of what's nested inside them — they only need children: React.ReactNode.
Card composition is the classic example: a Card component typically composes named sub-sections purely through nesting, without a single 'content' prop trying to hold header text, body text, and footer actions all at once:
Compound components and render props (introduced briefly)
A compound component is a set of components designed to be used together, sharing implicit state through context so the caller can arrange the pieces freely — think <Tabs><Tabs.List><Tabs.Tab>...</Tabs.Tab></Tabs.List><Tabs.Panels>...</Tabs.Panels></Tabs>. Tabs supplies shared state (which tab is active) behind the scenes, and Tabs.Tab, Tabs.Panels, and the rest read it via context rather than through explicit props passed down from the caller. This is a deeper pattern with its own module much later in the course — for now, just recognize the shape when you see dot-notation components used together like this.
A render prop is a prop whose value is a function that returns JSX, letting the parent component control exactly what gets rendered while the component supplying the render prop controls when and with what data it's called — for example <MouseTracker render={(pos) => <p>{pos.x}, {pos.y}</p>} />. Render props were the standard way to share stateful logic between components before hooks existed, and they still show up in some libraries; they get a full treatment in a later module.
Both patterns exist to solve the same underlying problem as plain children: giving the caller control over rendering while the component itself controls behavior or shared state. Seeing them now, even briefly, makes it easier to recognize when a plain children prop is enough and when a codebase has reached for something more elaborate.
Component-as-prop and slot-like composition
Sometimes a single children slot isn't enough — a component needs several distinct 'slots' for different kinds of content, like a page layout that needs a sidebar, a main area, and a footer independently. One straightforward approach is component-as-prop: pass whole components (or elements) as named props instead of forcing everything through one children slot. <PageLayout sidebar={<Sidebar />} footer={<Footer />}>{mainContent}</PageLayout> gives PageLayout three independently placeable pieces of content, each typed as React.ReactNode.
This 'slot-like' composition mirrors how some templating systems offer named slots, and in React it requires nothing beyond props you already know how to type: type PageLayoutProps = { sidebar?: React.ReactNode; footer?: React.ReactNode; children: React.ReactNode }. Inside PageLayout, each prop is simply rendered in its designated spot in the JSX, giving the caller full control over the content of each slot while PageLayout controls the surrounding structure.
Choosing between a single children prop and multiple named slot props comes down to how many independently-positioned regions the component actually has. A Card with one flowing content area needs only children. A PageLayout with a sidebar, a main region, and a footer that must be arranged in a specific grid benefits from named slot props, because children alone can't tell the component which nested element belongs in which region.
Card composed from children, and a layout with named slots
CardHeader and CardFooter here are plain, independent components, not a true compound-component system with shared context — that pattern comes in a later module. Named slot props (sidebar, footer) are just regular React.ReactNode props; nothing special is needed to support them beyond typing and rendering.
type CardProps = { children: React.ReactNode };
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>;
}
function CardFooter({ children }: { children: React.ReactNode }) {
return <div className="card-footer">{children}</div>;
}
// Usage: composition, not configuration
<Card>
<CardHeader><h2>Order #4521</h2></CardHeader>
<p>3 items — $54.00</p>
<CardFooter><button>Track order</button></CardFooter>
</Card>;
// Named-slot layout for multiple independent regions
type PageLayoutProps = {
sidebar?: React.ReactNode;
footer?: React.ReactNode;
children: React.ReactNode;
};
function PageLayout({ sidebar, footer, children }: PageLayoutProps) {
return (
<div className="page">
{sidebar && <aside>{sidebar}</aside>}
<main>{children}</main>
{footer && <footer>{footer}</footer>}
</div>
);
}Code Examples
Stack is a pure layout component: it knows nothing about a heading, a paragraph, or a button — it only arranges whatever children it's given with consistent vertical spacing. Expected result: Renders a vertically stacked column with 12px of gap between each child.
Modal is a wrapper component: it adds backdrop and open/close behavior around whatever's passed as children, without ever needing to know whether that content is a confirmation dialog, a form, or an image gallery.
Common mistake: Hardcoding the modal's inner content (e.g. accepting a message: string prop) instead of accepting children would force a new prop for every new use case.
function Stack({ gap, children }: { gap: number; children: React.ReactNode }) {
return <div style={{ display: "flex", flexDirection: "column", gap }}>{children}</div>;
}
<Stack gap={12}>
<h1>Welcome</h1>
<p>Please sign in to continue.</p>
<button>Sign in</button>
</Stack>;type ModalProps = {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
};
function Modal({ isOpen, onClose, children }: ModalProps) {
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
// Any content at all can be dropped in — Modal never needs to change
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<h2>Delete item?</h2>
<p>This action cannot be undone.</p>
<button onClick={handleDelete}>Delete</button>
</Modal>;Card composed of independent sub-sections
Each section is placed by the caller through nesting (or named slot props), not configured through a single flat prop list on Card.
Card
├── Header
├── Content
└── FooterImportant Rules
Use children: React.ReactNode for a component with one flowing content area.
Use named slot props (typed as React.ReactNode) when a component has multiple independently-positioned regions that a single children can't distinguish.
Layout and wrapper components should depend only on children/slot props, never on assumptions about the concrete content passed in.
Recognize compound components (dot-notation, shared context) and render props (a function-valued prop returning JSX) by shape, even before studying them in depth.
Where this shows up
Nearly every UI library — modals, cards, accordions, layout grids — is built around children-based composition so consumers can drop in arbitrary content without the library author anticipating every possible use case in advance. A well-designed Modal or Card component rarely needs to change once shipped, precisely because it never hardcodes what goes inside it.
type CardProps = { headerText: string; bodyText: string; footerText: string };
type CardProps = { children: React.ReactNode };
Hardcoding three text props locks Card into rendering only plain text in three fixed slots; children lets any JSX — including other components — be composed inside instead.
<PageLayout content={<Sidebar />} content2={mainContent} /> <PageLayout sidebar={<Sidebar />}>{mainContent}</PageLayout>
Numbered or vaguely-named slot props are confusing and undiscoverable; a descriptively named slot (sidebar) plus children for the primary region reads clearly at every call site.
[multiple-choice] What does the children prop represent in a component like <Card><p>Hi</p></Card>? A. A required string prop named children B. Whatever JSX is nested between Card's opening and closing tags C. The component's internal state D. A list of Card's own child components defined inside Card's function
B. Whatever JSX is nested between Card's opening and closing tags — children is a special prop React automatically populates with whatever is nested between a component's opening and closing JSX tags — here, the <p>Hi</p> element.
[multiple-choice] When should a component use multiple named slot props instead of a single children prop? A. Never — children should always be used exclusively B. When it has multiple independently-positioned regions that children alone can't distinguish C. Only when using TypeScript D. When the component has more than one line of JSX
B. When it has multiple independently-positioned regions that children alone can't distinguish — A single children prop can't tell a component which nested piece belongs in which region; named slot props (like sidebar and footer) let the caller place content into specific, independently-arranged regions.
[true-false] True or False: Render props and compound components are both ways to give the caller control over what gets rendered while the component retains control over behavior or shared state. A. True B. False
A. True — Both patterns solve the same underlying tension as children-based composition — letting the calling code decide the rendered output while the component manages behavior, timing, or shared state internally.
Build a Panel layout component that accepts a title: string prop and a children: React.ReactNode prop, rendering the title in a header region and the children in a body region below it.
type PanelProps = { title: string; children: React.ReactNode }; function Panel({ title, children }: PanelProps) { return ( <div className="panel"> <div className="panel-header">{title}</div> <div className="panel-body">{children}</div> </div> ); }
Type the props as { title: string; children: React.ReactNode } and render both inside a wrapping div.