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.
72
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
useSlots(children, config).__SLOT__ = Symbol('Parent.Slot') marker so it can be wrapped by consumers.asSlot(wrapper, ParentSlotComponent) to copy the marker.useSlots? Use isSlot(child, SlotComponent).All four primitives are exported publicly from @primer/react.
| 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 |
import {useSlots, isSlot, asSlot, type SlotMarker, type WithSlotMarker, type FCWithSlotMarker} from '@primer/react'
// or
import {useSlots} from '@primer/react/hooks'Slots are the right tool when:
Dialog.Header, Dialog.Body, Dialog.Footer).Slots are not the right tool when:
children as-is.useSlots is single-match-per-key — see "Limitations").ActionMenu for the pattern when you have to).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>
)
}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.
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')Symbol(...) descriptionsThe 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:
Symbol('DEPRECATED_X')) — handle deprecation via other means.Symbol('ActionListItem') instead of Symbol('ActionList.Item')).__SLOT__ marker to a root component that no other component scans for — the marker is dead weight and confusing for anyone searching the codebase.__SLOT__ marker at all?Add it when either:
grep "isSlot.*ComponentName" to check), orDialog.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).
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)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.
useSlots is single-match. It cannot collect all children of a given type into an array. Consumers that need that pattern (e.g. UnderlinePanels collecting all Tab children, CheckboxGroup collecting all FormControl children) currently hand-roll the logic. A future extension to useSlots may add a multi-match option.ActionMenu rewriting Tooltip-wrapped Anchors) are not a slot pattern — keep them as bespoke Children.map loops with isSlot predicates.useSlots purely for dev-mode warnings in render-hot paths. Although useSlots is 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.useSlots inside useMemo, useCallback, useEffect, or any other callback. The react-hooks/rules-of-hooks lint rule enforces this based on the use* naming convention, even though useSlots itself 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, call useSlots first and then reference the result inside useMemo.useSlotsdisplayName mismatch: if a child's displayName matches a slot component's displayName but the child is missing the __SLOT__ marker, a warning fires suggesting asSlot. This catches the most common wrapping footgun.MyCompound.Header, etc.) with displayName set and __SLOT__ = Symbol('MyCompound.Header') applied.useSlots(children, {header: MyCompound.Header, ...}) once and render slots.header / rest in the right places.MyCompound with Object.assign(Root, {Header, Footer, ...}).__SLOT__ to the root unless another component scans for it.asSlot (or call out which slot to pass).b111781
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.