In UI development, flexibility is everything. You want to give developers control over what content they render while maintaining how it's structured and styled.
Traditional component APIs often fall into the trap of prop bloat — passing specific props like headerContent
, footerActions
, or leftIcon
. As components grow, these APIs become rigid and cumbersome.
A better pattern? Slot-Based APIs.
What Are Slots in UI Components?
A Slot is a named placeholder within a component's structure where developers can inject custom content.
It’s like giving developers controlled "hooks" inside your component's layout:
“Here’s a spot where you can put custom content — and it will render in the right place, with consistent styling.”
Think of slots as the content injection points of a component.
Why Slot-Based APIs Are Powerful
Problem | Slot-Based API Solution |
---|---|
Components with too many specific props | Replace with flexible named slots |
Rigid structure, hard to customize content | Allow arbitrary JSX to be injected in slots |
Props don’t scale with complex components | Slots decouple structure from content |
Need for layout consistency with content flexibility | Slots enforce structure but let content be dynamic |
Real-World Example: Card Component
Props-Driven API (Rigid & Bloated)
<Card
title="User Profile"
description="User details"
headerIcon={<UserIcon />}
footerActions={<Button>Edit</Button>}
/>
Slot-Based API (Flexible & Scalable)
<Card>
<Card.Header>
<UserIcon />
<h2>User Profile</h2>
</Card.Header>
<Card.Body>
User details go here.
</Card.Body>
<Card.Footer>
<Button>Edit</Button>
</Card.Footer>
</Card>
How to Implement Slot-Based APIs in React
1. Compound Components + Context (Recommended)
const CardContext = createContext({});
function Card({ children }: { children: React.ReactNode }) {
return <CardContext.Provider value={{}}>{children}</CardContext.Provider>;
}
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>;
}
function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>;
}
function CardFooter({ children }: { children: React.ReactNode }) {
return <div className="card-footer">{children}</div>;
}
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
2. React.Children & cloneElement (Positional Slots)
function Modal({ children }: { children: React.ReactNode }) {
const header = React.Children.toArray(children).find(child => React.isValidElement(child) && child.type === ModalHeader);
const footer = React.Children.toArray(children).find(child => React.isValidElement(child) && child.type === ModalFooter);
const body = React.Children.toArray(children).filter(child => child !== header && child !== footer);
return (
<div className="modal">
{header}
<div className="modal-body">{body}</div>
{footer}
</div>
);
}
function ModalHeader({ children }: { children: React.ReactNode }) {
return <div className="modal-header">{children}</div>;
}
function ModalFooter({ children }: { children: React.ReactNode }) {
return <div className="modal-footer">{children}</div>;
}
3. Advanced Slot Components (Radix UI's Slot and React Aria's Slot Patterns)
Radix UI's Slot Component
Radix provides a Slot component that forwards props, refs, and classNames to its child element.
import { Slot } from '@radix-ui/react-slot';
function IconButton({ children }: { children: React.ReactNode }) {
return (
<button className="icon-button">
<Slot className="button-icon" />
{children}
</button>
);
}
Example: asChild
API
function Button({ asChild = false, ...props }: { asChild?: boolean } & React.ComponentProps<'button'>) {
const Comp = asChild ? Slot : 'button';
return <Comp className="button" {...props} />;
}
export default function Example() {
return (
<Button asChild>
<a href="/contact">Contact</a>
</Button>
);
}
Handling Event Handlers in Radix Slot
Radix Slot merges event handlers, prioritizing the child’s handler.
<Button asChild onClick={(e) => {
console.log('from Slot');
if (e.defaultPrevented) return;
}}>
<a href="/contact" onClick={(e) => {
e.preventDefault();
console.log('from Child');
}}>Contact</a>
</Button>
React Aria's Slot System (Slot Prop Injection)
React Aria Components use a slot prop pattern to inject ARIA roles and behaviors.
import { Dialog, Heading, Button } from 'react-aria-components';
function ExampleDialog() {
return (
<Dialog>
<Heading slot="title">Settings</Heading>
<p>Dialog content here</p>
<Button slot="close">Close</Button>
</Dialog>
);
}
Props and accessibility attributes are merged internally based on the slot
prop.
Slot Component: Radix UI vs React Aria
Library | Slot Mechanism | Purpose |
---|---|---|
Radix UI |
<Slot> component that forwards props, refs, and classNames to its child. |
Flexible composition, element-level prop merging. |
React Aria | Context-based slot props injection system (no visual component). | Accessibility-focused, named prop injection into structured components. |
Best Practices
- Name slots meaningfully (
Header
,Footer
, etc.). - Avoid prop bloat by favoring compositional slots.
- Ensure slot content inherits design tokens.
- Document slot usage in your design system.
References
- Radix UI Slot Documentation: https://www.radix-ui.com/primitives/docs/utilities/slot
- React Aria Slots Pattern (Advanced Composition Guide): https://react-spectrum.adobe.com/react-aria/advanced.html#slots
Do you use Slot-Based APIs in your projects? Share your patterns and thoughts below!
Top comments (0)