HomeAbout Me

Advanced React Patterns 4: Slots

By Daniel Nguyen
Published in React JS
June 24, 2025
3 min read
Advanced React Patterns 4: Slots

Slots

**One liner:** Slots allow you to specify an element which takes on a particular role in the overall collection of components.

This pattern is particularly useful for situations where you’re building a UI library with a lot of components that need to work together. It’s a way to provide a flexible API for your components that allows them to be used in a variety of contexts.

If you’re building a component library, you have to deal with two competing interests:

  1. Correctness
  2. Flexibility

You want to make sure people don’t mess up things like accessibility, but you also want to give them the flexibility to build things the way their diverse needs require. Slots can help with this.

Here’s a quick example of a component that uses slots (from the react-aria docs):

<CheckboxGroup>
<Label>Pets</Label>
<MyCheckbox value="dogs">Dogs</MyCheckbox>
<MyCheckbox value="cats">Cats</MyCheckbox>
<MyCheckbox value="dragons">Dragons</MyCheckbox>
<Text slot="description">Select your pets.</Text>
</CheckboxGroup>

The slot="description" prop is letting the Text component know that it needs to look for special props that are meant to be used as a description. Those special props will be provided by the CheckboxGroup component.

Essentially, the CheckboxGroup component will say: “here’s a bucket of props for any component that takes on the role of a description.” The Text component will then say: “Oh look, I’ve got a slot prop that matches the description slot, so I’ll use these props to render myself.”

All of this is built using context.

What this enables is a powerfully flexible capability to have components which are highly reusable. The Text component can be used in many different contexts, and it can adapt to the needs of the parent component. For example, it’s also used in react-aria’s ComboBox components. Here’s the anatomy of a react-aria ComboBox component:

<ComboBox>
<Label />
<Input />
<Button />
<Text slot="description" />
<FieldError />
<Popover>
<ListBox>
<ListBoxItem>
<Text slot="label" />
<Text slot="description" />
</ListBoxItem>
<Section>
<Header />
<ListBoxItem />
</Section>
</ListBox>
</Popover>
</ComboBox>

This can be used to apply appropriate aria- attributes as well as ids and event handlers. You might think about it as a way to implement compound components in a way that doesn’t require an individual component for every single use case.

Implementation

Folks tend to struggle with this one a bit more than the rest, but it’s simpler than it seems.

The basic concept is your root component creates collections of props like so:

function NumberField({ children }: { children: React.ReactNode }) {
// setup state/events/etc
const slots = {
label: { htmlFor: inputId },
decrement: { onClick: decrement },
increment: { onClick: increment },
description: { id: descriptionId },
input: { id: inputId, 'aria-describedby': descriptionId },
}
return <SlotContext value={slots}>{children}</SlotContext>
}

Then the consuming components use the use(SlotContext) to get access to the slots object and pluck off the props they need to do their job:

function Input(props) {
props = useSlotProps(props, 'input')
return <input {...props}>
}

The useSlotProps hook is responsible for taking the props that have been specified and combining it with those from the SlotContext for the named slot.

Slot Context

👨‍💼 It’s a tale as old as time. Our label and input are not properly associated in this form and so clicking the label will not focus the input as expected (in addition to other accessibility issues).

But we don’t want developers to be able to make this mistake. So we’ve made a TextField component which will generate the id for the relationship (if one is not provided). The tricky bit is we want people to be able to structure their label and input however they want, so we can’t render the input and label for them. Instead, we want to be able to provide the id and htmlFor props to the label and input.

So what we want you to do is first create a SlotContext and useSlotProps hook in

, then use those in the Label and Input components to retrieve the necessary props.

The useSlotProps hook should accept a props object and a slot name and return the props to be applied to the element for that slot. It should merge the props it’s been given with the props from the SlotContext for that slot.

Once you’ve finished that, then render the SlotContext provider in the TextField component in

to provide slot props for the label and input.

When you’re finished, the label and input should be properly associated and clicking the label should focus the input.

Generic Slot Components

👨‍💼 You’ll notice our party mode toggle button is using useId to properly associate the toggle button with its label. We’d like to make that implicit and reuse the Label component for the Toggle as well.

Please update the Toggle component in

to render a SlotContext provider (in addition to the ToggleContext provider it’s already rendering) so it can provide props for a label slot (the slot name for a Label). You’ll also want to put the id in the ToggleContext so the ToggleButton can grab it.

Once you’re finished with that, you can remove the manual id/htmlFor props in the

file and the id should be provided automatically.

Slot Prop

👨‍💼 We have ToggleOn and ToggleOff components, but really we could make those components a simple Text component that accepts a slot prop. Then the Toggle component could define the props that the individual Text components should have based on which slot they’re taking.

In fact, we could do this with the Switch as well!

🧝‍♂️ I’ve added Text and Switch components to the

file for you to use. These are both already wired up to consume a slot named text and switch. You can
check the diff
for details.

What we want to do in this exercise is add a slot prop to each of our slot components so the slot they’re taking can be defined by the parent component.

Then you’ll need to update Toggle to get rid of the ToggleContext provider and instead use the SlotProvider for all the components it wants to send props to:

  • label - htmlFor
  • onText - hidden (undefined if isOn is true, and true if isOn is false)
  • offText - hidden (undefined if isOn is false, and true if isOn is true)
  • switch - id, on, and onClick

So by the end of all of this, here’s what I want the API to be like:

<Toggle>
<Label>Party mode</Label>
<Switch />
<Text slot="onText">Let's party 🥳</Text>
<Text slot="offText">Sad town 😭</Text>
</Toggle>

Once that’s been updated, you can delete the useToggle hook and the ToggleOn, ToggleOff, and ToggleButton components.

Reusability FTW!

app.tsx

import { Input, Label, Switch, Text } from './slots.tsx'
import { TextField } from './text-field.tsx'
import { Toggle } from './toggle.tsx'
export function App() {
return (
<div>
<div>
<Toggle>
<Label>Party mode</Label>
<Switch />
<Text slot="onText">Let's party 🥳</Text>
<Text slot="offText">Sad town 😭</Text>
</Toggle>
</div>
<hr />
<div>
<TextField>
<Label>Venue</Label>
<Input />
</TextField>
</div>
</div>
)
}

slot.tsx

import { createContext, use } from 'react'
import { Switch as BaseSwitch } from '#shared/switch'
type Slots = Record<string, Record<string, unknown>>
export const SlotContext = createContext<Slots>({})
function useSlotProps<Props>(
props: Props & { slot?: string },
defaultSlot?: string,
): Props {
const slot = props.slot ?? defaultSlot
if (!slot) return props
const slots = use(SlotContext)
// a more proper "mergeProps" function is in order here
// to handle things like merging event handlers better.
// we'll get to that a bit in a later exercise.
return { ...slots[slot], slot, ...props } as Props
}
export function Label(
props: React.ComponentProps<'label'> & { slot?: string },
) {
props = useSlotProps(props, 'label')
return <label {...props} />
}
export function Input(
props: React.ComponentProps<'input'> & { slot?: string },
) {
props = useSlotProps(props, 'input')
return <input {...props} />
}
export function Text(props: React.ComponentProps<'span'> & { slot?: string }) {
props = useSlotProps(props, 'text')
return <span {...props} />
}
type SwitchProps = Omit<React.ComponentProps<typeof BaseSwitch>, 'on'> & {
slot?: string
}
export function Switch(props: SwitchProps) {
return (
<BaseSwitch
{...(useSlotProps(props, 'switch') as React.ComponentProps<
typeof BaseSwitch
>)}
/>
)
}

textfield.tsx

import { useId } from 'react'
import { SlotContext } from './slots'
export function TextField({
id,
children,
}: {
id?: string
children: React.ReactNode
}) {
const generatedId = useId()
id ??= generatedId
const slots = {
label: { htmlFor: id },
input: { id },
}
return <SlotContext value={slots}>{children}</SlotContext>
}

toggle.tsx

import { useId, useState } from 'react'
import { SlotContext } from './slots'
export function Toggle({
id,
children,
}: {
id?: string
children: React.ReactNode
}) {
const [on, setOn] = useState(false)
const generatedId = useId()
id ??= generatedId
const toggle = () => setOn(!on)
const labelProps = { htmlFor: id }
const onTextProps = { hidden: on ? undefined : true }
const offTextProps = { hidden: on ? true : undefined }
const switchProps = { id, on, onClick: toggle }
return (
<SlotContext
value={{
label: labelProps,
onText: onTextProps,
offText: offTextProps,
switch: switchProps,
}}
>
{children}
</SlotContext>
)
}

Tags

#React

Share

Previous Article
Advanced React Patterns 3: Compound Components

Table Of Contents

1
Slots
2
Slot Context
3
Generic Slot Components
4
Slot Prop

Related Posts

React Testing 8: Testing custom hook
September 09, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media