Component Development
This guide covers the patterns for creating components in the design-system package. All UI components follow these conventions.
Atomic Design Structure
Section titled “Atomic Design Structure”Components are organized by complexity:
| Category | Location | Examples | When to use |
|---|---|---|---|
atoms/ | Primitive UI | Button, Text, Badge, Link | Single-purpose, no composition |
molecules/ | Composed UI | Card | Combines atoms, has subcomponents |
layout/ | Structure | Stack, Container | Controls spacing and arrangement |
Component Template
Section titled “Component Template”Every design-system component follows this structure:
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';import { cn } from '../../utils/cn';
// 1. Export variant typesexport type ButtonVariant = 'primary' | 'secondary' | 'ghost';export type ButtonSize = 'sm' | 'md' | 'lg';
// 2. Props interface extends HTML attributesexport interface ButtonProps extends HTMLAttributes<HTMLButtonElement> { variant?: ButtonVariant; size?: ButtonSize; children: ReactNode;}
// 3. Create style maps for variantsconst variantStyles: Record<ButtonVariant, string> = { primary: 'bg-cosmic-500 text-white hover:bg-cosmic-600', secondary: 'bg-slate-700 text-slate-100', ghost: 'bg-transparent text-slate-300',};
// 4. Use forwardRefexport const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ variant = 'primary', size = 'md', className, children, ...props }, ref) => { return ( <button ref={ref} className={cn(variantStyles[variant], className)} {...props} > {children} </button> ); });
// 5. Set displayNameButton.displayName = 'Button';Required Patterns
Section titled “Required Patterns”forwardRef
Section titled “forwardRef”All components use forwardRef to allow parent components to access the DOM node:
export const Button = forwardRef<HTMLButtonElement, ButtonProps>( (props, ref) => { return <button ref={ref} {...props} />; });displayName
Section titled “displayName”Always set displayName for better debugging in React DevTools:
Button.displayName = 'Button';// For compound components:CardTitle.displayName = 'Card.Title';Props Interface
Section titled “Props Interface”Extend the appropriate HTML attributes type:
// For buttonsinterface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
// For divsinterface CardProps extends HTMLAttributes<HTMLDivElement>
// For inputsinterface InputProps extends InputHTMLAttributes<HTMLInputElement>Polymorphic Components
Section titled “Polymorphic Components”Some components allow changing the rendered element with an as prop:
type TextElement = 'h1' | 'h2' | 'h3' | 'p' | 'span';
interface TextProps extends HTMLAttributes<HTMLElement> { as?: TextElement; variant?: TextVariant;}
// Usage<Text variant="h1" as="h2">Renders as h2</Text>Implementation:
const defaultElements: Record<TextVariant, TextElement> = { h1: 'h1', h2: 'h2', body: 'p',};
const Component = (as ?? defaultElements[variant]) as ElementType;return <Component ref={ref} {...props}>{children}</Component>;Compound Components
Section titled “Compound Components”For complex components like Card, use the Object.assign pattern:
// Define root and subcomponentsconst CardRoot = forwardRef<HTMLDivElement, CardProps>(...);const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(...);const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(...);
// Set displayNamesCardRoot.displayName = 'Card';CardTitle.displayName = 'Card.Title';CardBody.displayName = 'Card.Body';
// Combine with Object.assignexport const Card = Object.assign(CardRoot, { Title: CardTitle, Body: CardBody, Footer: CardFooter,});Usage:
<Card variant="elevated"> <Card.Title>Title</Card.Title> <Card.Body>Content here</Card.Body></Card>Context (When Needed)
Section titled “Context (When Needed)”Use React Context sparingly, only for compound components that need shared state:
interface CardContextValue { variant: CardVariant;}
const CardContext = createContext<CardContextValue>({ variant: 'default' });
// In CardRoot<CardContext.Provider value={{ variant }}> {children}</CardContext.Provider>Styling with cn()
Section titled “Styling with cn()”The cn() utility combines clsx and tailwind-merge:
import { cn } from '../../utils/cn';
className={cn( // Base styles 'rounded-lg font-medium', // Variant styles variantStyles[variant], // Conditional styles fullWidth && 'w-full', interactive && 'cursor-pointer hover:border-slate-600', // Allow override from props className)}Export Pattern
Section titled “Export Pattern”Add new components to the barrel exports:
export { Button, type ButtonProps, type ButtonVariant } from './Button';
// components/index.tsexport * from './atoms';export * from './molecules';export * from './layout';Checklist
Section titled “Checklist”Before submitting a new component:
- Uses
forwardRef - Has
displayNameset - Props interface extends HTML attributes
- Variant types exported separately
- Uses
cn()for className composition - Accepts and spreads
classNameprop - Exported from
index.ts - Added to component hierarchy in Design System docs