@haitch-ui/react-slot
v0.1.2
Published
A strict, compositional Slot component for React that safely forwards props, events, and refs to exactly one valid child element.
Maintainers
Readme
Slot
Note - this package is now deprecated. Future releases will be included in @haitch-ui/react
A strict, compositional slot component for React that safely forwards props, events, and refs to exactly one valid child element.
Designed for design systems and headless UI components where you want to enhance or wrap a child element without adding extra DOM nodes, while keeping behavior predictable and explicit.
Table of Contents
Installation
npm i @haitch-ui/react-slotWhy Slot Exists
In many component libraries, you want to:
- Attach props to user-provided elements
- Avoid extra wrappers (
div,span, etc.) - Preserve refs and event handlers
- Maintain strict control over structure
Slot enforces exactly one valid React element child and clones it with deterministic, well-defined merging rules.
Core Guarantees
Slot guarantees:
- Exactly one non-whitespace child
- Child must be a valid React element
- Fragments are explicitly disallowed
- Props are merged deterministically
- Refs are safely composed
- Event handlers are composed, not overridden
- Whitespace-only JSX formatting is ignored
If any invariant is violated, Slot throws early with a clear error.
Basic Usage
<Slot className="btn-primary" onClick={handleClick}>
<button className="btn">Save</button>
</Slot>Resulting behavior:
className→"btn btn-primary"onClick→ child handler runs first, then slot handler- No wrapper elements added
- Refs still point to the
<button>
Child Requirements
Allowed
<Slot>
<button />
</Slot><Slot>
{"\n "}
<button />
{"\n"}
</Slot>Whitespace-only text nodes are ignored.
Disallowed
No child
<Slot />Multiple children
<Slot>
<button />
<button />
</Slot>Non-element child
<Slot>{"hello"}</Slot>
<Slot>{123}</Slot>Fragment
<Slot>
<>
<button />
</>
</Slot>Fragments are rejected because props and refs cannot be safely attached.
Prop Merging Rules
Default Behavior
- Slot props win over child props
- Child props are preserved unless explicitly overridden
<Slot id="slot-id">
<button id="child-id" />
</Slot>Result:
<button id="slot-id" />className
- Concatenated if both exist
- Order: child first, slot second
<Slot className="slot">
<button className="child" />
</Slot>Result:
"child slot"style
- Shallow merge
- Slot values win on conflicts
<Slot style={{ color: "red" }}>
<button style={{ color: "blue", margin: 4 }} />
</Slot>Result:
{
color: "red",
margin: 4
}Event Handler Composition
Supported events are composed, not overridden:
onClickonMouseDown / UponPointerDown / UponMouseEnter / LeaveonFocus / BluronKeyDown / Up
Execution Order
- Child handler
- Slot handler
<Slot onClick={() => console.log("slot")}>
<button onClick={() => console.log("child")} />
</Slot>Output:
child
slotIf only one side provides a handler, it is used unchanged.
Ref Behavior
Refs are safely composed using @haitch-ui/react-compose-refs.
Supported ref types:
- Object refs
- Callback refs
- Forwarded refs
All refs receive the same underlying DOM element.
const ref = useRef<HTMLButtonElement>(null);
<Slot ref={ref}>
<button />
</Slot>Intentional Constraints
These are deliberate design decisions:
| Constraint | Reason | | -------------------- | -------------------------------------- | | Single child only | Prevents ambiguous prop application | | No fragments | Fragments cannot receive refs or props | | No implicit wrapping | Preserves DOM structure | | Early runtime errors | Fail fast, easier debugging |
Testing Philosophy
The test suite validates behavioral guarantees, not implementation details.
Covered cases include:
- Child validation rules
- Whitespace handling
- Prop precedence
classNameandstylemerging- Event handler composition
- Ref composition (object + callback)
- Pass-through of
data-*andaria-*props
Tests use:
- Vitest
- React Testing Library
- @testing-library/jest-dom
When to Use Slot
Use Slot when:
- Building headless or unstyled components
- Designing APIs that accept “any element”
- You need strict structural guarantees
- Predictable prop + ref behavior matters
Avoid Slot when:
- Multiple children are required
- Fragments are unavoidable
- You want implicit wrapping
API Reference
<Slot />
const Slot: React.ForwardRefExoticComponent<
{
children: React.ReactNode;
} & Record<string, unknown> &
React.RefAttributes<HTMLElement>
>;Props
children (required)
children: React.ReactNode;Must resolve to exactly one non-whitespace React element
Whitespace-only text nodes are ignored
The following are not allowed:
- No children
- Multiple non-whitespace children
- Strings, numbers, or other primitives
React.Fragment
Violations throw a runtime error.
Forwarded Props
All additional props are forwarded to the child:
<Slot disabled aria-label="Save">
<button />
</Slot>Becomes:
<button disabled aria-label="Save" />Prop precedence
- Slot props override child props by default
- Exceptions:
className,style, event handlers
Event Handlers
If both child and slot define a handler:
(event) => {
childHandler(event);
slotHandler(event);
};If only one exists, it is used unchanged.
ref
ref?: React.Ref<HTMLElement>;forwardRefenabled- Child and slot refs are composed
- All refs receive the same element instance
Runtime Errors
| Condition | Error |
| ----------------- | ------------------------------------------------ |
| No valid child | Received 0 non-whitespace children |
| Multiple children | Received N non-whitespace children |
| Non-element child | expects a single valid React element child |
| Fragment child | must be a single element, not a React.Fragment |
TypeScript Notes
Slotdoes not infer child prop types- This is intentional for headless flexibility
- Consumers should rely on the child element’s typing
Summary
Slot is intentionally strict.
That strictness makes it:
- Predictable
- Safe
- Design-system ready
- Easy to reason about
If it renders, it behaves exactly how you expect.
