| name | distilling-components |
| description | Component design patterns - compound components, composition, props API |
| user-invocable | true |
| allowed-tools | Read, Write, Glob, Grep, Edit |
Distilling Components
Best practices for designing React components that are flexible, composable, and maintainable.
Trigger
/distill
Overview
This skill provides guidance on component architecture and API design. Use it when:
- Creating reusable component libraries
- Designing component APIs and props
- Implementing compound component patterns
- Balancing flexibility with simplicity
Quick Decision Tree
Does the component have multiple related parts sharing state?
└── YES → Use compound components
Is the structure fixed with few variations?
└── YES → Use simple props
Do consumers need to control element rendering?
└── YES → Use asChild pattern
Does the component wrap a DOM element?
└── YES → Forward refs and spread props
Workflow
Phase 1: Choose Component Pattern
Compound Components
Use when a component has multiple related parts sharing implicit state:
<Dialog>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Description>This action cannot be undone.</Dialog.Description>
<Dialog.Close>Cancel</Dialog.Close>
</Dialog.Content>
</Dialog>
<Dialog
trigger="Open"
title="Are you sure?"
description="This action cannot be undone."
closeText="Cancel"
/>
When to use compound components:
- Multiple related elements sharing implicit state
- Components with slots (header, body, footer)
- Components where order/presence of children varies
- When you need flexible composition
When NOT to use:
- Simple components with fixed structure
- Components with 1-3 props
- When the structure never changes
Simple Props
For straightforward components with minimal variation:
<Button variant="primary" size="md">
Click me
</Button>
Phase 2: Design Props API
The Goldilocks Principle
Find the balance between too rigid and too flexible:
<Button>Click me</Button>
<Button
backgroundColor="#000"
hoverBackgroundColor="#333"
activeBackgroundColor="#111"
borderRadius={4}
paddingX={16}
paddingY={8}
fontSize={14}
fontWeight={500}
// ... 30 more props
>
Click me
</Button>
<Button variant="primary" size="md" className="custom-override">
Click me
</Button>
Customization Layers
- Variants - Predefined options (primary, secondary, destructive)
- Size - Predefined sizes (sm, md, lg)
- className - Escape hatch for one-off customizations
- asChild - Render as different element (Radix pattern)
Consistent Naming
<Input disabled />
<Button disabled />
<Select disabled />
<Input disabled />
<Button isDisabled />
<Select readonly />
Boolean Props
Use positive names, avoid double negatives:
<Input disabled />
<Modal open />
<Input notEnabled />
<Modal isNotClosed />
Event Handler Naming
Prefix with on, use past tense for after-the-fact:
<Input onChange={} onBlur={} />
<Dialog onOpenChange={} onClose={} />
<Input handleChange={} blurHandler={} />
Phase 3: Implement Composition
Prefer Composition Over Configuration
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>Content here</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
<Card
header={{ title: "Title", description: "Description" }}
content="Content here"
footer={{ actions: [{ label: "Action", onClick: () => {} }] }}
/>
Slot Pattern
For components with optional sections:
function Card({ children, header, footer }) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-content">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
<Card
header={<h2>Title</h2>}
footer={<Button>Save</Button>}
>
Main content
</Card>
Render Props vs Children
Prefer children for simple cases, render props for complex:
<Card>
<CardContent />
</Card>
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
/>
Phase 4: Add Flexibility Patterns
The asChild Pattern
Allow rendering as a different element:
<Button>Click me</Button>
<Button asChild>
<a href="/page">Click me</a>
</Button>
<Button asChild>
<Link href="/page">Click me</Link>
</Button>
Implementation using Radix Slot:
import { Slot } from "@radix-ui/react-slot";
function Button({ asChild, ...props }) {
const Comp = asChild ? Slot : "button";
return <Comp {...props} />;
}
Forwarding Refs
Always forward refs for components wrapping DOM elements:
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, ...props }, ref) => {
return (
<button ref={ref} {...props}>
{children}
</button>
);
}
);
Spread Remaining Props
Allow passing arbitrary HTML attributes:
function Button({ variant, size, className, ...props }) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}
<Button data-testid="submit" aria-label="Submit form">
Submit
</Button>
Phase 5: Handle State
Controlled vs Uncontrolled
Support both patterns:
function Input({
value: controlledValue,
defaultValue,
onChange,
...props
}) {
const [internalValue, setInternalValue] = useState(defaultValue ?? "");
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
function handleChange(e) {
if (!isControlled) {
setInternalValue(e.target.value);
}
onChange?.(e);
}
return <input value={value} onChange={handleChange} {...props} />;
}
<Input defaultValue="hello" />
<Input value={value} onChange={setValue} />
Default Props
Use sensible defaults that work for 80% of cases:
function Button({
variant = "primary",
size = "md",
type = "button", // Not "submit" - safer default
...props
}) {
}
Component Organization
File Structure
components/
├── button/
│ ├── button.tsx # Main component
│ ├── button.test.tsx # Tests
│ └── index.ts # Public exports
├── card/
│ ├── card.tsx
│ ├── card-header.tsx
│ ├── card-content.tsx
│ └── index.ts
Export Patterns
export { Card } from "./card";
export { CardHeader } from "./card-header";
export { CardContent } from "./card-content";
export { Card, CardHeader, CardContent } from "./card";
Anti-Patterns to Avoid
Prop Explosion
<Button
leftIcon={<Icon />}
rightIcon={<Arrow />}
iconSpacing={8}
iconSize={16}
>
Click
</Button>
<Button>
<Icon /> Click <Arrow />
</Button>
Boolean Prop Variants
<Button primary large rounded>Click</Button>
<Button variant="primary" size="lg" radius="full">Click</Button>
Premature Abstraction
Don't create a component until you've copy-pasted it 2-3 times. Wait until patterns emerge.
Quick Reference Card
PATTERN SELECTION:
├── Multiple parts + shared state → Compound components
├── Fixed structure, few props → Simple props
├── Need custom element → asChild pattern
└── Wraps DOM element → Forward refs
PROPS API:
├── Use variants for predefined options
├── Use className as escape hatch
├── Consistent naming (disabled, not isDisabled)
├── Boolean props = positive names
└── Event handlers = on + verb (onChange)
COMPOSITION:
├── Prefer children over config objects
├── Use slots for optional sections
├── Render props for complex data
└── Spread remaining props
ALWAYS:
├── Forward refs on DOM wrappers
├── Support controlled + uncontrolled
├── Sensible defaults (80% case)
└── Export from index.ts
Checklist
API Design:
├── [ ] Props use consistent naming
├── [ ] Boolean props are positive
├── [ ] Event handlers prefixed with on
├── [ ] Sensible defaults provided
└── [ ] className escape hatch available
Flexibility:
├── [ ] asChild pattern if element varies
├── [ ] Refs forwarded for DOM wrappers
├── [ ] Remaining props spread
└── [ ] Both controlled/uncontrolled supported
Organization:
├── [ ] Clear file structure
├── [ ] Clean exports from index
├── [ ] Compound components properly composed
└── [ ] No premature abstraction