# Component Design

> Component design rules - Atomic Design (atoms, molecules, organisms, templates, pages), compound components, props API design, render props, controlled vs uncontrolled, slots pattern, composition vs inheritance, Context for component communication, polymorphic components (as prop), headless vs styled components, useControllableState, React.memo/useMemo/useCallback, Storybook, component documentation, component testing, performance optimization

- Skill: `14bryanespinoza/component-design` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 14bryanespinoza/component-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/14bryanespinoza/component-design/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: 14BryanEspinoza (https://skillmd.com/u/14bryanespinoza)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/14bryanespinoza/component-design

---


# Component Design — Rules and Conventions

---

## 1. Philosophy

1. **Composition over inheritance** — Build UI by combining small, focused components.
2. **Props as API** — Design props like a public API: minimal, explicit, type-safe.
3. **Controlled by default** — Prefer controlled components. Uncontrolled only for leaf inputs.
4. **Accessibility built-in** — Semantic HTML, ARIA when needed, keyboard navigation default.
5. **Performance conscious** — `React.memo`, `useMemo`, `useCallback` only when measured.

---

## 2. Atomic Design (Compact)

| Level         | Description                 | Example                                          |
| ------------- | --------------------------- | ------------------------------------------------ |
| **Atoms**     | Indivisible UI primitives   | `Button`, `Input`, `Label`, `Icon`               |
| **Molecules** | Simple compositions         | `FormField` (Label + Input + Error), `SearchBox` |
| **Organisms** | Complex sections            | `Header`, `Sidebar`, `DataTable`, `CardList`     |
| **Templates** | Page structure without data | `DashboardLayout`, `AuthLayout`                  |
| **Pages**     | Templates + real data       | `DashboardPage`, `SettingsPage`                  |

### Rules

- **Atoms only** in `components/atoms/` — no business logic
- **Molecules** compose atoms + minimal logic
- **Organisms** = business logic + data fetching (via props/hooks)
- **Templates/Pages** in `app/` or `pages/` (Next.js) /
  route files (TanStack Router)

---

## 3. Composition vs Inheritance

### Composition (Preferred)

```tsx
// ✅ Composition
interface CardProps {
  header?: React.ReactNode;
  children: React.ReactNode;
  footer?: React.ReactNode;
}

export function Card({ header, children, footer }: CardProps) {
  return (
    <article className="card">
      {header && <header className="card-header">{header}</header>}
      <div className="card-body">{children}</div>
      {footer && <footer className="card-footer">{footer}</footer>}
    </article>
  );
}

// Usage
<Card header={<CardTitle />}>
  <CardContent />
</Card>;
```

### Rules Composition vs Inheritance

- **Slots over props** — `children` + named slots (`header`, `footer`) over `renderProp` functions
- **No inheritance** — no `extends BaseComponent`
- **Props drilling max 2 levels** — use Context for deeper

---

## 4. Slots Pattern

```tsx
// components/Modal.tsx
interface ModalProps {
  trigger: React.ReactNode;
  title: React.ReactNode;
  children: React.ReactNode;
  footer?: React.ReactNode;
}

export function Modal({ trigger, title, children, footer }: ModalProps) {
  const [open, setOpen] = useState(false);

  return (
    <>
      {typeof trigger === "function" ? (
        trigger({ onClick: () => setOpen(true) })
      ) : (
        <button onClick={() => setOpen(true)}>{trigger}</button>
      )}
      {open && (
        <Dialog open={open} onOpenChange={setOpen}>
          <DialogContent>
            <DialogHeader>{title}</DialogHeader>
            <DialogBody>{children}</DialogBody>
            {footer && <DialogFooter>{footer}</DialogFooter>}
          </DialogContent>
        </Dialog>
      )}
    </>
  );
}
```

### Rules Slot Pattern

- **Named slots** — `header`, `footer`, `children` over `renderProp={...}`
- **Default slots** — optional with sensible defaults
- **Type-safe** — `React.ReactNode` for slots

---

## 5. Props API Design

### Interface Design

```tsx
// ✅ Good: explicit, typed, documented
interface ButtonProps {
  /** Visual variant */
  variant: 'primary' | 'secondary' | 'ghost' | 'destructive'
  /** Size variant */
  size?: 'sm' | 'md' | 'lg'
  /** Disables interaction */
  disabled?: boolean
  /** Click handler */
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
  /** Button content */
  children: React.ReactNode
  /** Additional CSS classes */
  className?: string
}

// ❌ Bad: vague, loose types
interface BadButtonProps {
  type?: string
  onClick?: () => void
  children?: any
  className?: string
  ...rest: any
}
```

### Rules Props API Design

- **Required props first** — required before optional
- **Descriptive names** — `onSubmit` not `onClick` for forms
- **Single event handler type** — `React.MouseEvent<HTMLButtonElement>`
- **`className` last** — for composition
- **No `any`** — ever
- **JSDoc for non-obvious props** — `@param` for complex props

---

## 6. Compound Components

```tsx
// components/Select.tsx
interface SelectContextValue {
  value: string
  onChange: (value: string) => void
  disabled: boolean
}

const SelectContext = createContext<SelectContextValue | null>(null)

function Select({ value, onChange, disabled, children }: SelectProps) {
  return (
    <SelectContext.Provider value={{ value, onChange, disabled }}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  )
}

function SelectTrigger({ children, className }: { children: React.ReactNode; className?: string }) {
  const ctx = useContext(SelectContext)
  return <button className={className} disabled={ctx?.disabled} onClick={() => setOpen(true)}>
    {children}
  </button>
}

function SelectOptions({ children }: { children: React.ReactNode }) {
  return <ul role="listbox">{children}</ul>
}

function SelectOption({ value, children }: { value: string; children: React.ReactNode }) {
  const ctx = useContext(SelectContext)
  return (
    <li role="option" aria-selected={ctx?.value === value} onClick={() => ctx?.onChange(value)}>
      {children}
    </li>
  )
}

Select.Trigger = SelectTrigger
Select.Options = SelectOptions
Select.Option = SelectOption

// Usage
<Select value={value} onChange={setValue}>
  <Select.Trigger>Select...</Select.Trigger>
  <Select.Options>
    <Select.Option value="a">Option A</Select.Option>
    <Select.Option value="b">Option B</Select.Option>
  </Select.Options>
</Select>
```

### Rules Compound Components

- **Implicit state sharing** — via Context
- **Type-safe subcomponents** — `Select.Trigger`, `Select.Option`
- **Flexible composition** — consumer controls layout

---

## 7. Polymorphic Components (`as` prop)

```tsx
// components/Box.tsx
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>['ref']

type PolymorphicProps<C extends React.ElementType, Props = {}> =
  Props & { as?: C } & Omit<React.ComponentPropsWithoutRef<C>, keyof Props>

type BoxProps<C extends React.ElementType = 'div'> = PolymorphicProps<C, {
  children: React.ReactNode
  padding?: 'none' | 'sm' | 'md' | 'lg'
}>

export function Box<C extends React.ElementType = 'div'>({
  as,
  children,
  padding,
  className,
  ...props
}: BoxProps<C>) {
  const Component = as || 'div'
  return <Component className={cn('box', padding && `p-${padding}`, className)} {...props}>
    {children}
  </Component>
}

// Usage
<Box as="section" padding="md">Content</Box>
<Box as={Link} href="/about" padding="sm">About</Box>
```

### Rules Polymorphic Components

- **Default `as='div'`** — semantic default
- **Forward ref** — `React.ComponentPropsWithRef`
- **Omit conflicting props** — `Omit<ComponentPropsWithoutRef<C>, keyof Props>`

---

## 8. Controlled vs Uncontrolled

### Controlled (Preferred)

```tsx
interface InputProps {
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
}

export function Input({ value, onChange, ...props }: InputProps) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      {...props}
    />
  );
}
```

### Uncontrolled (Leaf Inputs Only)

```tsx
interface UncontrolledInputProps {
  defaultValue?: string;
  onChange?: (value: string) => void;
  ref?: React.Ref<HTMLInputElement>;
}

export function UncontrolledInput({
  defaultValue,
  onChange,
  ref,
  ...props
}: UncontrolledInputProps) {
  const internalRef = useRef<HTMLInputElement>(null);
  const forwardedRef = ref || internalRef;
  return (
    <input
      defaultValue={defaultValue}
      onChange={(e) => onChange?.(e.target.value)}
      ref={forwardedRef}
      {...props}
    />
  );
}
```

### Rules Controlled vs Uncontrolled

- **Default to controlled** — React owns state
- **Uncontrolled only for**: `<input type="file">`,
  leaf inputs without validation
- **`defaultValue` not `value`** — for uncontrolled
- **`ref` for imperative access** — `focus()`, `select()`

---

## 9. Headless vs Styled Components

### Headless (Logic Only)

```tsx
// hooks/useToggle.ts
export function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  return { on, toggle: () => setOn((o) => !o), setOn };
}

// Consumer provides ALL UI
function MyToggle() {
  const { on, toggle } = useToggle();
  return (
    <label>
      <input type="checkbox" checked={on} onChange={toggle} />
      <span>{on ? "ON" : "OFF"}</span>
    </label>
  );
}
```

### Styled (Opinionated)

```tsx
// components/Toggle.tsx
export function Toggle({
  checked,
  onChange,
  label,
}: {
  checked: boolean;
  onChange: (v: boolean) => void;
  label: string;
}) {
  return (
    <label className="toggle">
      <input
        type="checkbox"
        checked={checked}
        onChange={(e) => onChange(e.target.checked)}
      />
      <span className="toggle-thumb" />
      <span className="toggle-label">{label}</span>
    </label>
  );
}
```

### Rules Headless vs Styled Components

- **Headless for design system primitives** — maximum flexibility
- **Styled for app-specific components** — consistent look
- **Export both** — headless hook + styled component

---

## 10. `useControllableState`

```tsx
// hooks/useControllableState.ts
export function useControllableState<T>({
  value: controlledValue,
  defaultValue,
  onChange,
}: {
  value?: T;
  defaultValue: T;
  onChange?: (value: T) => void;
}) {
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
  const isControlled = controlledValue !== undefined;
  const value = isControlled ? controlledValue : uncontrolledValue;

  const setValue = useCallback(
    (next: T | ((prev: T) => T)) => {
      const nextValue =
        typeof next === "function" ? (next as (prev: T) => T)(value) : next;
      if (!isControlled) setUncontrolledValue(nextValue);
      onChange?.(nextValue);
    },
    [isControlled, value, onChange],
  );

  return [value, setValue] as const;
}
```

---

## 10. Performance Optimization

### Memoization (Only When Measured)

```tsx
// ✅ Measured bottleneck → memoize
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
  return (
    <ul>
      {items.map((item) => (
        <ListItem key={item.id} item={item} onSelect={onSelect} />
      ))}
    </ul>
  );
});

// Stable callback for memoized children
function Parent() {
  const handleSelect = useCallback((id) => {
    /* ... */
  }, []);
  return <ExpensiveList items={items} onSelect={handleSelect} />;
}
```

### Rules Performance Optimization

- **Profile first** — React DevTools Profiler
- **`React.memo`** — shallow prop comparison
- **`useCallback`/`useMemo`** — stable references for memoized children
- **Don't over-memoize** — cost of comparison > render cost for small components

---

## 11. Accessibility (Reference)

> **Full accessibility rules**: see `accessibility` skill.

### Essentials

- **Semantic HTML** — `<button>`, `<nav>`, `<main>`, `<article>`
- **Labels** — `<label htmlFor>`, `aria-label`, `aria-labelledby`
- **Focus management** — visible focus, logical order, focus trap in modals
- **ARIA** — roles, states, properties when native HTML insufficient

---

## 12. Testing Components (Reference)

> **Full testing patterns**: see `testing` skill.

### Essentials Accessibility

```tsx
// Test behavior, not implementation
it("opens modal on button click", async () => {
  render(<Modal trigger={<button>Open</button>}>Content</Modal>);
  await userEvent.click(screen.getByRole("button", { name: /open/i }));
  expect(screen.getByRole("dialog")).toBeInTheDocument();
});
```

---

## 13. Storybook (Reference)

> **Storybook patterns**: see `testing` skill.

```tsx
// Button.stories.tsx
export default { component: Button, tags: ["autodocs"] };
export const Primary = { args: { variant: "primary", children: "Button" } };
export const Secondary = { args: { variant: "secondary", children: "Button" } };
```

---

## 14. Documentation

````tsx
/**
 * A flexible button component supporting multiple variants and sizes.
 *
 * @example
 * ```tsx
 * <Button variant="primary" onClick={handleClick}>Submit</Button>
 * ```
 *
 * @param variant - Visual style variant
 * @param size - Size variant
 * @param disabled - Disables interaction
 * @param onClick - Click handler
 * @param children - Button content
 */
export function Button({ variant = 'primary', size = 'md', disabled, onClick, children }: ButtonProps) { ... }
````

---

## 15. Methodology

Before using ANY component pattern not documented in
this skill:

1. **MCP Context7** (priority): `context7_resolve-library-id` +
   `context7_query-docs` for React, Radix UI, Headless UI.
2. **Official docs**: react.dev, radix-ui.com — verify current APIs.
3. **Project config**: `components/`, `tsconfig.json`,
   `package.json` — verify against actual setup.
4. **HARD RULE**: If not in this skill AND cannot be verified against
   2 authoritative sources → DO NOT USE IT. Document as assumption or risk in
   report to orchestrator.

---

## 16. Prohibitions

- ❌ Do not use inheritance — composition only
- ❌ Do not pass `any` in props — strict typing
- ❌ Do not skip `React.memo` on leaf components with stable props
- ❌ Do not use `children` as function (render props) — slots pattern preferred
- ❌ Do not mix headless logic with styled UI in same component — separate
- ❌ Do not skip accessibility — semantic HTML, labels, focus
- ❌ Do not use `className` for variant logic — use `variant` prop
- ❌ Do not expose internal state — encapsulate via props

---

## 17. References

> **Note:** For React patterns, see [React](../reactjs/SKILL.md)
> **Note:** For TypeScript rules, see
> [TypeScript](../typescript/SKILL.md)
> **Note:** For JavaScript conventions, see
> [JavaScript](../javascript/SKILL.md)
> **Note:** For HTML conventions, see [HTML](../html/SKILL.md)
> **Note:** For CSS conventions, see [CSS](../css/SKILL.md)
> **Note:** For Accessibility, see
> [Accessibility](../accessibility/SKILL.md)
> **Note:** For Testing patterns, see
> [Testing](../testing/SKILL.md)
> **Note:** For Performance, see
> [Performance](../performance/SKILL.md)

---

Last updated: 2026-08

