Component Design
When to use: Building new UI components. Reviewing component architecture. Refactoring existing components.
The test: Can this component be used in isolation, understood in 30 seconds, and tested without the rest of the app?
Creation Checklist
Run this for every new component:
- Named export -- No anonymous exports (
export default function Card, notexport default () =>) - TypeScript interface -- Props defined with explicit types
- Semantic HTML --
<button>,<article>,<nav>, not<div>soup - Design tokens -- No arbitrary color/spacing values (use theme variables)
- Destructured props -- With default values at the function signature
- Single responsibility -- Does one thing well (under 150 lines)
- Independently testable -- Works in isolation without parent state
Component Types
| Type | Purpose | Data Source | Location |
|---|---|---|---|
| Presentational | Display UI | Props only | Reusable library |
| Structural | Compose layout | Props, maps data | Pages/views |
| Stateful | Manage state | API, context | App-specific |
Placement Rule
- Bottom of tree -- Presentational (most reusable)
- Middle of tree -- Structural (compose others)
- Top of tree -- Stateful (app-specific)
Props Design
| Check | Threshold | How to Verify |
| ------------------ | ---------------------------------------------- | ---------------------------- | ---- | ----- |
| Prop count | 7 or fewer (split component if more) | Count interface properties |
| Required props | Only truly required props lack ? | Review TypeScript interface |
| Boolean props | No boolean for complex multi-state values | Prefer union types: "sm" | "md" | "lg" |
| Children preferred | Use children over render props when possible | Check for renderX props |
| Defaults provided | Every optional prop has a sensible default | Check destructuring defaults |
Good Example
interface CardProps {
title: string;
description: string;
variant?: "default" | "highlighted";
onAction?: () => void;
}
function Card({ title, description, variant = "default", onAction }: CardProps) {
return (
<article className={`card card--${variant}`}>
<h3>{title}</h3>
<p>{description}</p>
{onAction && <button onClick={onAction}>Action</button>}
</article>
);
}
File Organization
src/components/
├── design-system/ # Presentational (most reusable)
│ ├── Button.tsx
│ ├── Typography.tsx
│ └── Card.tsx
├── features/ # Structural (feature-specific)
│ ├── auth/
│ └── dashboard/
└── pages/ # Stateful (app-specific)
└── HomePage.tsx
| Rule | Threshold |
|---|---|
| One component per file | Exceptions only for tightly-coupled sub-components |
| Co-located tests | Component.test.tsx next to Component.tsx |
| Co-located styles | CSS modules or styled file next to component |
| Index files for directories | Re-export public API only |
Conditional Rendering
| Pattern | When to Use |
|---|---|
| Early return | Loading, error, empty states |
Logical && | Simple presence check (items.length > 0 && <List />) |
| Ternary | Two mutually exclusive states |
| Named variable | Complex condition (extract to const isVisible = ...) |
Good Example
function UserProfile({ user, isLoading, error }) {
if (isLoading) return <Skeleton />;
if (error) return <ErrorState error={error} />;
if (!user) return null;
return <Profile user={user} />;
}
Anti-Patterns
- No nested ternaries
- No inline complex conditions in JSX
- No
indexaskeyin lists (use stable IDs) - No inline object/array literals as props (creates new reference each render)
- No side effects in render body (use
useEffect)
Accessibility Checklist
Every component must meet these:
- Semantic HTML element used (
<button>not<div onClick>) - Keyboard operable (Enter/Space for buttons, arrow keys for groups)
- Focus management correct (focus moves logically, returns on close)
- ARIA attributes present when semantic HTML is insufficient
- Color contrast passes 4.5:1 for text, 3:1 for UI elements
- Screen reader announces purpose (test with VoiceOver/NVDA)
Testing Checklist
| Test Type | What to Verify | Tool |
|---|---|---|
| Render test | Mounts without crashing | Jest/Vitest + Testing Library |
| Props test | Each prop produces expected output | Snapshot or assertion |
| Event test | Callbacks fire on interaction | fireEvent or userEvent |
| Accessibility test | Zero axe violations | jest-axe or @axe-core/playwright |
| Visual test | Looks correct across states | Storybook + Chromatic or Percy |
Bad Component Signals
| Signal | Problem | Fix |
|---|---|---|
| Over 7 props | Too many responsibilities | Split into smaller components |
| Breaks when moved | Too coupled to parent | Accept data via props, no document.querySelector |
| Hard to test | Hidden dependencies | Inject dependencies via props or context |
| Prop drilling 3+ levels | Wrong tree structure | Use context or restructure component tree |
| Over 200 lines | Doing too much | Extract sub-components |
| Requires specific parent | Tight coupling | Use composition and children |
Quick Diagnosis
| Symptom | Likely Cause | Fix |
|---|---|---|
| Re-renders constantly | New object/array references in props | Memoize or extract to stable reference |
| Breaks in isolation | Depends on parent state or DOM | Accept all data via props |
| Hard to style differently | Hardcoded styles | Accept className prop, use design tokens |
| Duplicated across features | Not extracted to design system | Move to design-system/ directory |
| Tests are fragile | Testing implementation details | Test behavior and output, not internals |
Context
- Visual Design -- Design tokens and spacing scale
- Interaction + Accessibility -- Accessible component patterns
- Data Interfaces -- Components for CRUD operations