Slots
Loading "Intro to Slots"
Run locally for transcripts
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:
- Correctness
- 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 id
s 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.