In this lab, you'll build components for a mini design system using atomic design principles from Brad Frost's methodology. You'll experience how small, reusable pieces (atoms) combine to form more complex components (molecules)—the core insight of component-driven development.
Your repository already includes design tokens (colors, spacing, typography) and two starter atoms (Icon, Text). You'll create:
- A Button atom — an interactive element that uses design tokens
- An Alert molecule — a component that composes Icon, Text, and Button atoms together
⏱️ Estimated Time: 60-90 minutes
Prerequisites:
- Completed Labs 1-3 (Vitest, TDD, React Testing Library)
- Week 4 readings completed (Atomic Design Chapters 1-2)
- Node.js 20+ installed
Important
Windows Users: We recommend using PowerShell rather than Command Prompt. Where commands differ between operating systems, both versions are provided.
By the end of this lab, you will be able to:
- Explain the atomic design hierarchy (atoms, molecules, organisms, templates, pages)
- Use design tokens to ensure consistent styling across components
- Create an atom component that serves as a reusable building block
- Compose multiple atoms into a molecule that functions as a cohesive unit
- Test component composition using React Testing Library
This lab directly applies concepts from your Week 4 readings:
- The problem with pages: Frost argues that "pages" assume a "uniform, isolated unit when the web is actually fluid and interdependent." By building an Alert molecule from reusable atoms, you'll see how the same components can be used in different contexts without modification.
- "We're not designing pages, we're designing systems of components": This Stephen Hay quote captures what you'll practice today.
- Atoms: Frost defines atoms as "basic HTML elements like form labels, inputs, buttons, and others that can't be broken down any further without ceasing to be functional." You'll create a Button atom that fits this definition.
- Molecules: Frost describes molecules as "relatively simple groups of UI elements functioning together as a unit." Your Alert molecule will combine Icon, Text, and Button atoms into something more useful than any individual part.
- Design tokens: The starter code includes tokens for colors, spacing, and typography—the abstract values that ensure consistency across your design system.
Frost quotes Dave Rupert: "Responsive deliverables should look a lot like tiny Bootstraps." Your design tokens and components form exactly this—a custom, purpose-built system rather than a one-size-fits-all framework.
After accepting the GitHub Classroom assignment, clone your repository:
git clone <your-repository-url>
cd lab-4-<your-username>npm installYour repository includes a complete foundation:
Design Tokens (src/tokens/):
colors.ts— Semantic colors for alerts (success, warning, error, info) and neutral colorsspacing.ts— Consistent spacing scale based on 4px unitstypography.ts— Font families, sizes, weights, and line heightsindex.ts— Exports all tokens for easy importing
Starter Atoms (src/components/atoms/):
Icon.tsx— SVG icons for check, warning, error, info, and closeText.tsx— Styled text component with size and weight optionsindex.ts— Exports the provided atoms (you'll add Button here)
Configuration:
package.json— All dependencies pre-installedtsconfig.json— TypeScript configurationvitest.config.ts— Testing configuration with 90% coverage thresholdsrc/setupTests.ts— Jest DOM matchers configured
Take a few minutes to read through the token files and the Icon/Text components to understand how they work.
✅ Checkpoint: Run npm run typecheck — it should complete with no errors.
🤔 Reflection Question: Look at src/tokens/colors.ts. How does organizing colors by semantic meaning (success, warning, error, info) differ from organizing them by visual property (green, yellow, red, blue)? What advantages does the semantic approach offer?
Now you'll create your first atom—a Button component that uses design tokens for consistent styling.
Create src/components/atoms/Button.tsx:
import React from 'react';
import { colors, spacing, typography } from '../../tokens';
export interface ButtonProps {
/** Button content */
children: React.ReactNode;
/** Click handler */
onClick?: () => void;
/** Button variant */
variant?: 'primary' | 'secondary' | 'ghost';
/** Button size */
size?: 'sm' | 'md' | 'lg';
/** Disabled state */
disabled?: boolean;
/** Accessible label */
'aria-label'?: string;
/** Button type */
type?: 'button' | 'submit' | 'reset';
}
/**
* Button atom - interactive button element.
*
* Atoms like buttons are functional on their own,
* but designed to be composed into larger patterns.
*/
export function Button({
children,
onClick,
variant = 'primary',
size = 'md',
disabled = false,
'aria-label': ariaLabel,
type = 'button',
}: ButtonProps) {
// Size styles using spacing and typography tokens
const sizeStyles: Record<string, React.CSSProperties> = {
sm: {
padding: `${spacing.xs} ${spacing.sm}`,
fontSize: typography.fontSize.sm,
},
md: {
padding: `${spacing.sm} ${spacing.md}`,
fontSize: typography.fontSize.base,
},
lg: {
padding: `${spacing.md} ${spacing.lg}`,
fontSize: typography.fontSize.lg,
},
};
// Variant styles using color tokens
const variantStyles: Record<string, React.CSSProperties> = {
primary: {
backgroundColor: colors.info.icon,
color: colors.neutral.white,
border: 'none',
},
secondary: {
backgroundColor: colors.neutral.white,
color: colors.neutral.gray800,
border: `1px solid ${colors.neutral.gray300}`,
},
ghost: {
backgroundColor: 'transparent',
color: colors.neutral.gray600,
border: 'none',
},
};
const baseStyle: React.CSSProperties = {
fontFamily: typography.fontFamily.base,
fontWeight: typography.fontWeight.medium,
borderRadius: '4px',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.6 : 1,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: spacing.xs,
transition: 'background-color 0.2s, opacity 0.2s',
...sizeStyles[size],
...variantStyles[variant],
};
return (
<button
type={type}
style={baseStyle}
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel}
>
{children}
</button>
);
}Update src/components/atoms/index.ts to export your Button:
export { Icon, type IconProps } from './Icon';
export { Text, type TextProps } from './Text';
export { Button, type ButtonProps } from './Button';Create src/__tests__/Button.test.tsx:
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../components/atoms';
describe('Button', () => {
it('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick} disabled>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
it('applies aria-label when provided', () => {
render(<Button aria-label="Close dialog">X</Button>);
expect(screen.getByRole('button', { name: 'Close dialog' })).toBeInTheDocument();
});
it('applies correct background color for primary variant', () => {
render(<Button variant="primary">Primary</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyle({ backgroundColor: '#17a2b8' });
});
it('applies correct background color for secondary variant', () => {
render(<Button variant="secondary">Secondary</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyle({ backgroundColor: '#ffffff' });
});
it('applies transparent background for ghost variant', () => {
render(<Button variant="ghost">Ghost</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyle({ backgroundColor: 'rgba(0, 0, 0, 0)' });
});
it('applies reduced opacity when disabled', () => {
render(<Button disabled>Disabled</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyle({ opacity: '0.6' });
});
});✅ Checkpoint: Run npm test — your Button tests should pass.
🤔 Reflection Question: Notice how the Button component uses spacing.xs, spacing.sm, etc. instead of hardcoded values like '4px', '8px'. What would you need to change if your design team decided to increase all spacing by 2px?
Now you'll create a molecule—a component that combines multiple atoms into a cohesive unit.
Create src/components/molecules/Alert.tsx:
import React from 'react';
import { Icon, Text, Button } from '../atoms';
import { colors, spacing, AlertVariant } from '../../tokens';
export interface AlertProps {
/** The alert variant determines colors and default icon */
variant: AlertVariant;
/** The main message to display */
message: string;
/** Optional title for the alert */
title?: string;
/** Whether the alert can be dismissed */
dismissible?: boolean;
/** Callback when dismiss button is clicked */
onDismiss?: () => void;
}
/**
* Alert molecule - combines Icon, Text, and Button atoms.
*
* As Brad Frost describes, molecules are "relatively simple groups
* of UI elements functioning together as a unit." This Alert
* combines our atoms to create something more useful than
* any individual part.
*/
export function Alert({
variant,
message,
title,
dismissible = false,
onDismiss,
}: AlertProps) {
// Map variant to icon name
const iconNames: Record<AlertVariant, 'check' | 'warning' | 'error' | 'info'> = {
success: 'check',
warning: 'warning',
error: 'error',
info: 'info',
};
const variantColors = colors[variant];
const containerStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'flex-start',
gap: spacing.md,
padding: spacing.lg,
backgroundColor: variantColors.background,
border: `1px solid ${variantColors.border}`,
borderRadius: '6px',
};
const contentStyle: React.CSSProperties = {
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: spacing.xs,
};
return (
<div role="alert" style={containerStyle}>
{/* Icon atom */}
<Icon
name={iconNames[variant]}
variant={variant}
size={24}
aria-label={`${variant} alert`}
/>
{/* Content using Text atoms */}
<div style={contentStyle}>
{title && (
<Text weight="bold" color={variantColors.text}>
{title}
</Text>
)}
<Text color={variantColors.text}>{message}</Text>
</div>
{/* Dismiss Button atom */}
{dismissible && (
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
aria-label="Dismiss alert"
>
<Icon name="close" size={16} color={variantColors.text} />
</Button>
)}
</div>
);
}Update src/components/molecules/index.ts:
export { Alert, type AlertProps } from './Alert';Create src/__tests__/Alert.test.tsx:
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Alert } from '../components/molecules';
describe('Alert', () => {
it('renders with message', () => {
render(<Alert variant="info" message="This is an info message" />);
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText('This is an info message')).toBeInTheDocument();
});
it('renders with title when provided', () => {
render(
<Alert
variant="success"
title="Success!"
message="Your action was completed."
/>
);
expect(screen.getByText('Success!')).toBeInTheDocument();
expect(screen.getByText('Your action was completed.')).toBeInTheDocument();
});
it('shows dismiss button when dismissible', () => {
render(
<Alert variant="warning" message="Warning message" dismissible />
);
expect(screen.getByRole('button', { name: 'Dismiss alert' })).toBeInTheDocument();
});
it('does not show dismiss button when not dismissible', () => {
render(<Alert variant="error" message="Error message" />);
expect(screen.queryByRole('button', { name: 'Dismiss alert' })).not.toBeInTheDocument();
});
it('calls onDismiss when dismiss button is clicked', async () => {
const user = userEvent.setup();
const handleDismiss = vi.fn();
render(
<Alert
variant="info"
message="Dismissible message"
dismissible
onDismiss={handleDismiss}
/>
);
await user.click(screen.getByRole('button', { name: 'Dismiss alert' }));
expect(handleDismiss).toHaveBeenCalledTimes(1);
});
it('displays correct icon for variant', () => {
render(<Alert variant="success" message="Success" />);
expect(screen.getByRole('img', { name: 'success alert' })).toBeInTheDocument();
});
it('applies correct background color for success variant', () => {
render(<Alert variant="success" message="Success" />);
const alert = screen.getByRole('alert');
expect(alert).toHaveStyle({ backgroundColor: '#d4edda' });
});
it('applies correct background color for error variant', () => {
render(<Alert variant="error" message="Error" />);
const alert = screen.getByRole('alert');
expect(alert).toHaveStyle({ backgroundColor: '#f8d7da' });
});
});✅ Checkpoint: Run npm test — all tests should pass.
✅ Checkpoint: Run npm run test:coverage — you should have at least 90% coverage.
🤔 Reflection Question: The Alert molecule imports and uses Icon, Text, and Button atoms. If you needed to update how all buttons look across your entire application, how many files would you need to change? How does this demonstrate Frost's point about the value of atomic design?
npm run typecheck
npm run test:coverageBoth commands should pass with no errors and at least 90% coverage.
Update the README.md in your project root to include:
-
Your Name and Date
-
Reflection Section (minimum 150 words) answering:
- How does composing the Alert molecule from Icon, Text, and Button atoms demonstrate the value of atomic design?
- What role do design tokens play in maintaining consistency across your components?
- If you needed to add a "dark mode" to this design system, what would you need to change?
-
Key Concepts section listing 3-5 things you learned about atomic design
Your submission should include:
lab-4-<your-username>/
├── src/
│ ├── components/
│ │ ├── atoms/
│ │ │ ├── Icon.tsx # Provided
│ │ │ ├── Text.tsx # Provided
│ │ │ ├── Button.tsx # Your creation
│ │ │ └── index.ts # Updated to export Button
│ │ ├── molecules/
│ │ │ ├── Alert.tsx # Your creation
│ │ │ └── index.ts # Updated to export Alert
│ │ └── index.ts
│ ├── tokens/ # Provided
│ │ ├── colors.ts
│ │ ├── spacing.ts
│ │ ├── typography.ts
│ │ └── index.ts
│ ├── __tests__/
│ │ ├── Button.test.tsx # Your tests
│ │ └── Alert.test.tsx # Your tests
│ └── setupTests.ts
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── README.md # Your reflection
- Button atom implemented with variants (primary, secondary, ghost) and sizes (sm, md, lg)
- Alert molecule implemented, composing Icon, Text, and Button atoms
- Minimum 16 passing tests (8 Button + 8 Alert)
- Minimum 90% code coverage
- README.md with reflection (150+ words) and key concepts
- TypeScript compiles without errors
| Criteria | Points |
|---|---|
| Button atom implemented correctly (variants, sizes, tokens) | 25 |
| Alert molecule implemented correctly (composition, variants) | 30 |
| Tests complete and passing (16+ tests, 90% coverage) | 25 |
| README with reflection (150+ words) and key concepts | 20 |
| Total | 100 |
Finished early? Try these optional extensions:
-
Add a Badge atom: Create a small label component (like "New", "3", "Beta") with color variants matching your alert system.
-
Add an AlertWithAction molecule: Extend Alert to include a primary action button (e.g., "Undo", "Retry", "View Details") in addition to the dismiss button.
-
Add a NotificationCenter organism: Create a component that displays and manages a stack of multiple Alert molecules, with the ability to dismiss individual alerts.
-
Add hover states: Enhance the Button component with hover and focus styles using CSS-in-JS patterns.
Make sure your import paths are correct relative to the file location:
- From
src/components/atoms/Button.tsx: use../../tokens - From
src/components/molecules/Alert.tsx: use../../tokens
Use the exact color values from the tokens. For example:
// colors.info.icon is '#17a2b8'
expect(button).toHaveStyle({ backgroundColor: '#17a2b8' });Run npm run test:coverage and open coverage/index.html in a browser to see which lines aren't covered. Common misses:
- Different button variants not tested
- Alert without title not tested
- Disabled button state not tested
- Push your code to your GitHub repository
- Verify GitHub Actions passes all checks
- Submit your repository URL via Canvas
Due: Monday, February 2, 2026 at 11:59 PM