Skip to content

Component Development

This guide covers the patterns for creating components in the design-system package. All UI components follow these conventions.

Components are organized by complexity:

CategoryLocationExamplesWhen to use
atoms/Primitive UIButton, Text, Badge, LinkSingle-purpose, no composition
molecules/Composed UICardCombines atoms, has subcomponents
layout/StructureStack, ContainerControls spacing and arrangement

Every design-system component follows this structure:

import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import { cn } from '../../utils/cn';
// 1. Export variant types
export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';
// 2. Props interface extends HTML attributes
export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
}
// 3. Create style maps for variants
const 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 forwardRef
export 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 displayName
Button.displayName = 'Button';

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} />;
}
);

Always set displayName for better debugging in React DevTools:

Button.displayName = 'Button';
// For compound components:
CardTitle.displayName = 'Card.Title';

Extend the appropriate HTML attributes type:

// For buttons
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
// For divs
interface CardProps extends HTMLAttributes<HTMLDivElement>
// For inputs
interface InputProps extends InputHTMLAttributes<HTMLInputElement>

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>;

For complex components like Card, use the Object.assign pattern:

// Define root and subcomponents
const CardRoot = forwardRef<HTMLDivElement, CardProps>(...);
const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(...);
const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(...);
// Set displayNames
CardRoot.displayName = 'Card';
CardTitle.displayName = 'Card.Title';
CardBody.displayName = 'Card.Body';
// Combine with Object.assign
export 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>

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>

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
)}

Add new components to the barrel exports:

components/atoms/index.ts
export { Button, type ButtonProps, type ButtonVariant } from './Button';
// components/index.ts
export * from './atoms';
export * from './molecules';
export * from './layout';

Before submitting a new component:

  • Uses forwardRef
  • Has displayName set
  • Props interface extends HTML attributes
  • Variant types exported separately
  • Uses cn() for className composition
  • Accepts and spreads className prop
  • Exported from index.ts
  • Added to component hierarchy in Design System docs