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:
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.
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/etcconst 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.
👨💼 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
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
label
and input
.When you’re finished, the label and input should be properly associated and clicking the label should focus the input.
👨💼 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
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
id
should be provided
automatically.👨💼 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
slot
named text
and switch
. You
can 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!
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>)}
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 ?? defaultSlotif (!slot) return propsconst 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>)}/>)}
import { useId } from 'react'import { SlotContext } from './slots'export function TextField({id,children,}: {id?: stringchildren: React.ReactNode}) {const generatedId = useId()id ??= generatedIdconst slots = {label: { htmlFor: id },input: { id },}return <SlotContext value={slots}>{children}</SlotContext>}
import { useId, useState } from 'react'import { SlotContext } from './slots'export function Toggle({id,children,}: {id?: stringchildren: React.ReactNode}) {const [on, setOn] = useState(false)const generatedId = useId()id ??= generatedIdconst 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 (<SlotContextvalue={{label: labelProps,onText: onTextProps,offText: offTextProps,switch: switchProps,}}>{children}</SlotContext>)}
Quick Links
Legal Stuff
Social Media