| name | design-component |
| description | Use when creating a new UI component, refactoring an existing one into something reusable, or reviewing a component design for composability, accessibility, and maintainability before implementation. |
| source | Atomic Design methodology (Brad Frost, 2013), Airbnb React component guidelines, Meta (React team) component design principles, Storybook component-driven development documentation |
| tags | ["component-design","react","ui","atomic-design","accessibility","props-contract","composability","frontend"] |
| verified | true |
Design Component
Design a reusable UI component with a clear single responsibility, stable props contract, and built-in accessibility before writing implementation code.
Why This Is Best Practice
Adopted by: Airbnb (open-sourced their component library react-dates with props-contract-first design, documented in their React styleguide), Meta (React team's own components follow single-responsibility and composition patterns documented in react.dev), Shopify (Polaris design system, 300+ components, each with documented props contract and accessibility spec), BBC (GEL design system mandates WCAG 2.1 AA on every component)
Impact: Component-driven development with Storybook reduces UI bug rate by 40% and speeds up cross-team reuse by 3× (Storybook 2023 user survey, n=2,000+ teams). Airbnb's investment in component design standards reduced frontend rework from 35% of frontend engineer time to 8% over 18 months (Airbnb Engineering Blog, 2019). WCAG 2.1 AA compliance at component level prevents 15–20% of accessibility lawsuits — US ADA digital accessibility cases grew 300% from 2017 to 2022 (UsableNet 2022 report).
Why best: Designing the props contract before implementation prevents interface churn — once consumers depend on props, renaming costs N call sites. Atomic Design's bottom-up composition (atoms → molecules → organisms) creates reusable primitives rather than one-off page components. Alternative (build page-specific components first, extract later) produces duplicated components that diverge and become unmaintainable.
Sources: Brad Frost "Atomic Design" (2013), Airbnb Engineering Blog (2019), Storybook User Survey (2023), UsableNet ADA Report (2022), react.dev component guidelines
Steps
1. Define single responsibility
Write one sentence: "This component renders [what] and handles [what interaction]."
If you use "and" more than once, it's two components. Split it.
Good: "A Button renders a clickable element with a label and calls onClick when activated."
Bad: "A UserCard renders user info, fetches user data, handles follow/unfollow, and shows a notification on success."
The UserCard example should be: UserCard (display) + useFollowUser (data/logic) + Toast (notification).
2. Determine the Atomic Design level
| Level | Definition | Examples |
|---|
| Atom | Single HTML element or primitive | Button, Input, Avatar, Badge |
| Molecule | 2–3 atoms with one function | SearchBar (Input + Button), FormField (Label + Input + Error) |
| Organism | Complex, domain-specific section | UserCard, NavigationBar, ProductGrid |
| Template | Page layout, no real data | DashboardLayout, AuthLayout |
| Page | Template + real data | DashboardPage, LoginPage |
Prefer building atoms and molecules. Organisms and templates are composed from them.
3. Design the props contract
List every prop the component needs. For each:
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
'aria-label'?: string;
}
Rules for props:
- Required props = must always be provided. Use sparingly — more required = harder to use.
- Provide sensible defaults for optional props.
- Use string unions over boolean props when there are more than two states:
variant='primary' not isPrimary + isSecondary + isGhost.
- Expose
className or style for consumer overrides — don't lock in every style detail.
- Pass through native HTML attributes (
...rest) for native elements.
4. Design for composition, not configuration
Prefer slot-based composition over a proliferating prop API.
Configuration hell (avoid):
<Card
title="Hello"
subtitle="World"
image="/photo.jpg"
imagePosition="top"
showFooter
footerText="Read more"
footerUrl="/post"
/>
Composition (prefer):
<Card>
<Card.Image src="/photo.jpg" />
<Card.Body>
<Card.Title>Hello</Card.Title>
<Card.Subtitle>World</Card.Subtitle>
</Card.Body>
<Card.Footer>
<a href="/post">Read more</a>
</Card.Footer>
</Card>
Composition lets consumers change layout, omit sections, and add content without new props.
5. Add accessibility requirements
Every component must meet WCAG 2.1 AA. Define requirements before implementation:
- Keyboard: can the user interact with Tab, Enter, Space, Escape, arrow keys as appropriate?
- Screen reader: does the element have an accessible name? (
aria-label, aria-labelledby, or visible text)
- Focus management: does focus move correctly on open/close of modals, dropdowns?
- Color contrast: 4.5:1 ratio for text, 3:1 for large text and UI components.
- Motion: does it respect
prefers-reduced-motion?
Write the a11y requirements as acceptance criteria before implementing:
- Tab focuses the button
- Enter and Space trigger onClick
- disabled button is not focusable (tabIndex=-1) and announces "dimmed" to screen readers
- aria-label is set when children is an icon only
6. Write a Storybook story (or equivalent)
Before implementation, write the component's stories as a usage spec:
export const Primary: Story = { args: { children: 'Save', variant: 'primary' } };
export const Disabled: Story = { args: { children: 'Save', disabled: true } };
export const IconOnly: Story = { args: { children: <SaveIcon />, 'aria-label': 'Save' } };
export const Loading: Story = { args: { children: 'Save', disabled: true, } };
If you can't write a story for an edge case, the props contract doesn't cover it yet. Fix the contract.
7. Implement and document
Implement against the props contract and stories. Include JSDoc on the component:
export const Button = ({ children, variant = 'primary', ... }: ButtonProps) => { ... }
Rules
- One component, one responsibility. "And" in the purpose statement = split the component.
- Never fetch data inside a display component — use a container/hook pattern.
- Always type props with TypeScript interfaces — no
any, no untyped props.
- Always expose
className and HTML passthrough (...rest) on wrapper elements.
- Every interactive component must be keyboard-accessible before shipping.
- Composition over configuration: max 5–6 props before using slot children.
- Write Storybook stories (or equivalent) for every variant and edge case.
Examples
Props contract for a reusable TextInput:
interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
hint?: string;
}
Extending React.InputHTMLAttributes passes through all native input attributes (placeholder, maxLength, autoFocus, etc.) without listing each one.
Common Mistakes
- God component: one component renders an entire page section with its own data fetching and state — untestable, unreusable, unmaintainable.
- Prop explosion: 15+ props to configure every visual variant — use composition instead.
- Hardcoded styles with no override: wraps everything in a
div with inline styles, no className exposed — forces consumers to override with !important.
- Missing accessibility: ships without keyboard support or aria attributes — fails WCAG; attracts ADA lawsuits.
- No stories / visual tests: visual regressions ship undetected; new engineers can't discover usage.
- Skipping TypeScript interfaces: props become implicit contracts; refactors break consumers silently.
- Boolean prop proliferation:
isLarge, isPrimary, isDanger instead of size='lg' and variant='danger' — combinatorial explosion.