🎛️ Mastering the Slots Pattern in React for Component Flexibility
When building a component library, you’re constantly balancing two goals:
How can we ensure developers don’t break important things (like accessibility), but still offer composability?
That’s where Slots come in. 🎯
Slots allow child components to declare their role within a parent component. The parent can then provide role-specific props through context—without controlling layout or markup structure.
In HTML, consider this form element:
<CheckboxGroup><Label>Pets</Label><MyCheckbox value="dogs">Dogs</MyCheckbox><MyCheckbox value="cats">Cats</MyCheckbox><Text slot="description">Select your pets.</Text></CheckboxGroup>
The Text
component has slot="description"
, telling the parent:
“Hey, I’m the description.”
Then CheckboxGroup
supplies the appropriate props—like id
and aria-describedby
—based on the slot name.
This pattern is widely used in libraries like react-aria
, and it’s fantastic for decoupling layout from logic, enabling accessibility, and promoting reusability.
Here’s how we do it in React:
Your parent component (e.g. TextField
) generates the data and passes it through context.
function TextField({ children }) {const id = useId()const slots = {label: { htmlFor: id },input: { id },}return (<SlotContext.Provider value={slots}>{children}</SlotContext.Provider>)}
A hook merges the provided props with the default/slot-specific ones:
function useSlotProps(props, slotName) {const slotContext = useContext(SlotContext)const slotProps = slotContext?.[slotName] ?? {}return { ...slotProps, ...props }}
Now your components can declare which role they serve:
function Label(props) {const labelProps = useSlotProps(props, 'label')return <label {...labelProps} />}function Input(props) {const inputProps = useSlotProps(props, 'input')return <input {...inputProps} />}
✅ Accessibility props are applied ✅ Layout is customizable ✅ Logic is abstracted
Let’s look at a real use case.
Developers often forget to manually associate <label>
and <input>
using id
and htmlFor
, which breaks accessibility.
Using the slots pattern, your TextField
component can handle the logic behind the scenes:
<TextField><Label>Email</Label><Input /></TextField>
Behind the scenes, Label
and Input
receive automatically generated id
and htmlFor
props—ensuring correctness without extra work.
Let’s make our API even more powerful.
You might have separate components like:
<Toggle><ToggleOn>Party time 🎉</ToggleOn><ToggleOff>Sad town 😭</ToggleOff><ToggleButton /></Toggle>
Now let’s unify this using generic components + slots:
<Toggle><Label>Party mode</Label><Switch /><Text slot="onText">Let’s party 🥳</Text><Text slot="offText">Sad town 😭</Text></Toggle>
The Toggle
parent passes slot-specific props:
const slots = {label: { htmlFor: switchId },switch: { id: switchId, on: isOn, onClick: toggle },onText: { hidden: !isOn },offText: { hidden: isOn },}
Your Text
and Switch
components handle the rest:
function Text({ slot, ...props }) {const textProps = useSlotProps(props, slot)return <span {...textProps} />}function Switch({ slot, ...props }) {const switchProps = useSlotProps(props, slot)return <button {...switchProps}>{switchProps.on ? 'ON' : 'OFF'}</button>}
No more manually passing state. No custom hooks. No prop drilling. Just clean, declarative composition. ✨
Once this pattern is in place:
ToggleOn
, ToggleOff
, ToggleButton
)It’s especially helpful when building design systems, accessibility wrappers, and framework-agnostic UI kits.
The Slots Pattern is a step up from compound components:
Feature | Compound Components | Slots Pattern |
---|---|---|
State Sharing | Context | Context |
Layout Flexibility | Moderate | Very High |
Customization | Limited (custom API) | Unlimited (native props) |
Accessibility Support | Manual | Built-in |
If you’re building component libraries (especially reusable or accessible ones), slots are your secret weapon.
Quick Links
Legal Stuff
Social Media