name: slots
description: 'Use when: building, modifying, or wrapping compound components that use child-component "slots" in Primer React. Covers when to add a __SLOT__ marker, the useSlots hook, the isSlot helper, the asSlot wrapper helper, naming conventions for slot symbols, and common pitfalls.'
Slots in Primer React
Primer React uses a lightweight, SSR-compatible slot system to let compound components extract specific child elements (e.g. <ActionList.Item> extracting <ActionList.LeadingVisual> out of its children). This skill documents the conventions and the public API surface.
TL;DR
- Building a compound component that needs to find specific children? Use
useSlots(children, config). - Building a slot sub-component? Add a
__SLOT__ = Symbol('Parent.Slot')marker so it can be wrapped by consumers. - Wrapping an existing slot component? Use
asSlot(wrapper, ParentSlotComponent)to copy the marker. - Branching on whether a child is a slot, outside of
useSlots? UseisSlot(child, SlotComponent).
All four primitives are exported publicly from @primer/react.
Architecture
| File | Purpose |
|---|---|
hooks/useSlots.ts |
Hook that extracts named slot children from children |
utils/is-slot.ts |
isSlot(element, slot) predicate |
utils/as-slot.ts |
asSlot(component, source) marker-copy helper |
utils/types/Slots.ts |
SlotMarker, WithSlotMarker<T>, FCWithSlotMarker<P> types |
Public exports
import {useSlots, isSlot, asSlot, type SlotMarker, type WithSlotMarker, type FCWithSlotMarker} from '@primer/react'
// or
import {useSlots} from '@primer/react/hooks'
When to use slots
Slots are the right tool when:
- A parent compound component needs to extract specific named children and render them in particular positions (e.g.
Dialog.Header,Dialog.Body,Dialog.Footer). - The extraction must survive SSR (you can't rely on refs or rendered DOM).
- Consumers should be able to wrap your slot sub-components without breaking the parent's matching.
Slots are not the right tool when:
- You just want to render
childrenas-is. - You need to find all children of a given type (
useSlotsis single-match-per-key — see "Limitations"). - You need to inspect grandchildren or do per-child transforms with side effects (see
ActionMenufor the pattern when you have to).
Building a slot consumer (parent component)
Use useSlots. It returns [slots, rest], where slots is keyed by your config and rest is everything else (in encounter order).
import {useSlots} from '@primer/react/hooks'
function MyCompound({children}: {children: React.ReactNode}) {
const [slots, rest] = useSlots(children, {
header: MyCompound.Header,
footer: MyCompound.Footer,
})
return (
<div>
{slots.header}
<div className="body">{rest}</div>
{slots.footer}
</div>
)
}
Matcher styles
useSlots(children, {
// 1. Component reference (most common)
header: Header,
// 2. Component + props test fn (variant matching)
block: [Description, props => props.variant === 'block'],
})
Each slot key produces ReactElement | undefined. Slots are single-match: the first matching child wins; duplicates emit a dev-mode warning.
Building a slot sub-component (child component)
Add a __SLOT__ symbol after the component declaration so wrappers and parent scanners can recognise it. Use Parent.Slot for the symbol description and the WithSlotMarker / FCWithSlotMarker types for the export.
import type {FCWithSlotMarker} from '@primer/react'
export const MyHeader: FCWithSlotMarker<MyHeaderProps> = (props) => { ... }
MyHeader.displayName = 'MyCompound.Header'
MyHeader.__SLOT__ = Symbol('MyCompound.Header')
For forwardRef'd or otherwise non-FC components, assert with WithSlotMarker<typeof Component>:
;(MyHeader as WithSlotMarker<typeof MyHeader>).__SLOT__ = Symbol('MyCompound.Header')
Naming conventions for Symbol(...) descriptions
The Symbol description shows up in React DevTools and crash logs. Keep it predictable:
| Component shape | Convention | Example |
|---|---|---|
| Sub-component of a compound | Symbol('Parent.Slot') |
Symbol('ActionList.LeadingVisual') |
| Root component used as a child of another component | Symbol('Component') |
Symbol('FormControl') (FormControl is scanned as a child by CheckboxGroup) |
| Deeply nested slot | Symbol('Parent.Slot.SubSlot') |
Symbol('ActionList.GroupHeading.TrailingAction') |
Don't:
- Use lifecycle annotations (
Symbol('DEPRECATED_X')) — handle deprecation via other means. - Use camelCase without separators (
Symbol('ActionListItem')instead ofSymbol('ActionList.Item')). - Add a
__SLOT__marker to a root component that no other component scans for — the marker is dead weight and confusing for anyone searching the codebase.
Should I add a __SLOT__ marker at all?
Add it when either:
- A parent in this codebase scans for it (use
grep "isSlot.*ComponentName"to check), or - The component is a documented compound sub-component (e.g.
Dialog.Header) that consumers may want to wrap. Sub-component markers are intentionally generous because wrapping is a common downstream pattern.
Don't add it to top-level root components unless they're explicitly used as a child somewhere (e.g. FormControl is, because CheckboxGroup scans for it).
Wrapping a slot component (asSlot)
When consumers wrap a Primer slot component, the wrapper must carry the same __SLOT__ marker for the parent scanner to recognise it. Use asSlot:
import {asSlot, ActionList} from '@primer/react'
const ColoredLeadingVisual = asSlot(
function ColoredLeadingVisual({color, children}: {color: string; children: React.ReactNode}) {
return (
<ActionList.LeadingVisual>
<span style={{color}}>{children}</span>
</ActionList.LeadingVisual>
)
},
ActionList.LeadingVisual,
)
// Now ActionList.Item recognises ColoredLeadingVisual as a leading visual slot.
<ActionList.Item>
<ColoredLeadingVisual color="red"><CheckIcon /></ColoredLeadingVisual>
Approved
</ActionList.Item>
asSlot is the preferred replacement for the older cast-heavy pattern:
// ❌ Old, footgun-prone
;(ColoredLeadingVisual as typeof ColoredLeadingVisual & {__SLOT__?: symbol}).__SLOT__ =
ActionList.LeadingVisual.__SLOT__
// ✅ New, typed, dev-warns if the source has no marker
const ColoredLeadingVisual = asSlot(ColoredLeadingVisual, ActionList.LeadingVisual)
Branching on slot membership (isSlot)
If you need to check whether an element matches a particular slot outside of useSlots (e.g. inside an existing Children.map with side effects), use isSlot:
import {isSlot} from '@primer/react'
React.Children.map(children, child => {
if (React.isValidElement(child) && isSlot(child, Tooltip)) {
// ...
}
})
isSlot matches by comparing __SLOT__ markers (on the element or its type), so it recognises both the original slot component and any wrapper created with asSlot. It does not compare component identity (child.type === Tooltip) directly — that works incidentally only because the original component carries the same marker symbol.
Limitations and follow-ups
useSlotsis single-match. It cannot collect all children of a given type into an array. Consumers that need that pattern (e.g.UnderlinePanelscollecting allTabchildren,CheckboxGroupcollecting allFormControlchildren) currently hand-roll the logic. A future extension touseSlotsmay add a multi-match option.- Per-child classification / side-effect transforms (e.g.
ActionMenurewriting Tooltip-wrapped Anchors) are not a slot pattern — keep them as bespokeChildren.maploops withisSlotpredicates. - Avoid calling
useSlotspurely for dev-mode warnings in render-hot paths. AlthoughuseSlotsis implemented as a plain function (no internal React hooks), running it on every render just to compute a dev-only assertion is wasteful — call it in the regular render flow when you also need its output. - Don't call
useSlotsinsideuseMemo,useCallback,useEffect, or any other callback. Thereact-hooks/rules-of-hookslint rule enforces this based on theuse*naming convention, even thoughuseSlotsitself doesn't call any React hooks. Always call it at the top level of your component or custom hook. If you need to extract slots before a memoised computation, calluseSlotsfirst and then reference the result insideuseMemo.
Dev-mode warnings emitted by useSlots
- Duplicate slot: if two children match the same slot key, only the first is rendered and a warning fires.
displayNamemismatch: if a child'sdisplayNamematches a slot component'sdisplayNamebut the child is missing the__SLOT__marker, a warning fires suggestingasSlot. This catches the most common wrapping footgun.
Quick checklist for a new compound component
- Define the sub-components (
MyCompound.Header, etc.) withdisplayNameset and__SLOT__ = Symbol('MyCompound.Header')applied. - In the parent, call
useSlots(children, {header: MyCompound.Header, ...})once and renderslots.header/restin the right places. - Export
MyCompoundwithObject.assign(Root, {Header, Footer, ...}). - Don't add
__SLOT__to the root unless another component scans for it. - If the new component will be wrapped downstream, document that consumers should use
asSlot(or call out which slot to pass).