Welcome back, future design system architect! In the previous chapter, we laid the crucial groundwork for our design system by setting up our development environment and defining our foundational design tokens. Now, it’s time to bring those tokens to life and start building the tangible pieces of our UI: our very first components!
This chapter is all about getting hands-on. We’ll dive into creating two fundamental UI elements – a Button and an Input field. These might seem simple, but mastering their construction will teach you core principles applicable to every component in your system. We’ll focus on structure, styling with our design tokens, ensuring basic accessibility, and documenting our work with Storybook.
By the end of this chapter, you’ll have a clear understanding of how to translate design concepts into robust, reusable, and well-documented React components. Get ready to write some code and see your design system take its first visual steps!
The Anatomy of a Great Component
Before we start coding, let’s establish what makes a “great” component within a design system. It’s more than just a piece of UI; it’s a building block designed for reusability, consistency, and maintainability across your entire product ecosystem.
What is a Component, Really?
At its core, a component is an isolated, reusable piece of UI. Think of it like a LEGO brick. You can use the same brick in many different models, and each brick has a predictable shape and function.
In React, components are functions or classes that return JSX (JavaScript XML), describing what the UI should look like. They accept inputs called “props” (properties) and can manage their own internal “state.”
Key Characteristics of Design System Components:
- Encapsulation: A component should manage its own logic and styling, minimizing external dependencies and side effects.
- Reusability: It should be generic enough to be used in various contexts without modification, promoting consistency.
- Configurability (via Props): Its appearance and behavior should be customizable through clearly defined props, like
size,variant,onClick, ordisabled. - Accessibility: It must be usable by everyone, regardless of ability. This means using semantic HTML, ARIA attributes when necessary, and ensuring robust keyboard navigation.
- Testability: Easy to test in isolation to ensure it behaves as expected under different conditions.
- Documentation: Clear instructions on how to use it, what props it accepts, and illustrative examples, often provided via tools like Storybook.
Choosing a Styling Strategy
How we style our components is a crucial decision for a design system. We need a method that integrates well with React, supports design tokens, and allows for consistent, maintainable styles that scale.
Today, many modern React projects opt for CSS-in-JS libraries. These libraries allow you to write CSS directly within your JavaScript components, bringing styling closer to the component logic and enabling dynamic, token-driven styles.
For this guide, we’ll use Styled Components, a popular and powerful CSS-in-JS library.
⚡ Real-world insight: Styled Components (or similar libraries like Emotion) are widely adopted in production design systems because they offer:
- Dynamic Styling: Easily apply styles based on props or theme values, making components highly adaptable.
- Scoped Styles: Styles are automatically scoped to the component, preventing global CSS conflicts and ensuring component isolation.
- Theming Support: Seamless integration with design tokens via a
ThemeProvider, allowing for easy theme switching and consistent application of design language. - Developer Experience: Intuitive syntax and good tooling support, enhancing productivity and readability.
Setting Up Our Component Development Environment
Before we write our Button component, let’s ensure our project is ready. We’ll need a couple of packages:
styled-components: Our chosen CSS-in-JS library.storybook: For documenting and showcasing our components.
First, let’s install styled-components and its types for TypeScript.
# As of 2026-05-07, styled-components v6.1.1 is a stable and widely used version.
npm install styled-components@6.1.1
# Install types for TypeScript. Ensure the major version matches styled-components.
npm install --save-dev @types/styled-components@6.1.11
Next, ensure Storybook is set up. If you followed the initial setup in a previous chapter, you might already have it. If not, you can initialize it:
npx storybook@latest init
This command will detect your project setup (e.g., React, Vite, TypeScript) and install the necessary Storybook packages and configurations.
Project Structure for Components
A well-organized file structure is vital for scalability and maintainability within a growing design system. We’ll create a components directory inside our src folder. Each component will have its own sub-directory, containing its logic, styles, and stories.
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx # Component logic and definition
│ │ ├── Button.styles.ts # Component-specific styles
│ │ └── Button.stories.tsx # Storybook documentation and examples
│ ├── Input/
│ │ ├── Input.tsx
│ │ ├── Input.styles.ts
│ │ └── Input.stories.tsx
│ └── index.ts # Central export for all components
└── theme/
├── index.ts
└── types.ts
Let’s create these directories and initial files now:
mkdir -p src/components/Button src/components/Input
touch src/components/Button/Button.tsx src/components/Button/Button.styles.ts src/components/Button/Button.stories.tsx
touch src/components/Input/Input.tsx src/components/Input/Input.styles.ts src/components/Input/Input.stories.tsx
touch src/components/index.ts
Integrating Design Tokens into Styled Components
Before we dive into component code, let’s ensure our styled-components theme is correctly typed and accessible. This is how we’ll bridge our design tokens with our component styles.
First, your src/theme/types.ts should extend styled-components’ DefaultTheme interface. This tells TypeScript what properties to expect on your theme object.
// src/theme/types.ts
import 'styled-components'; // Required to extend the module
declare module 'styled-components' {
export interface DefaultTheme {
colors: {
primary: string;
secondary: string;
success: string;
warning: string;
danger: string;
text: string;
background: string;
border: string;
// Add more specific color tokens as needed
};
spacing: {
xxsmall: string;
xsmall: string;
small: string;
medium: string;
large: string;
xlarge: string;
xxlarge: string;
};
typography: {
fontFamily: string;
fontSize: {
small: string;
medium: string;
large: string;
// Add more specific font sizes
};
fontWeight: {
light: number;
normal: number;
bold: number;
};
};
borderRadius: {
small: string;
medium: string;
large: string;
full: string;
};
// Add other token categories like shadows, z-index, breakpoints
}
}
Next, your src/theme/index.ts will export the actual defaultTheme object, populated with concrete values. These are the design tokens we defined in the previous chapter.
// src/theme/index.ts
import { DefaultTheme } from 'styled-components';
export const defaultTheme: DefaultTheme = {
colors: {
primary: '#007bff', // Blue
secondary: '#6c757d', // Gray
success: '#28a745', // Green
warning: '#ffc107', // Yellow
danger: '#dc3545', // Red
text: '#212529', // Dark gray
background: '#ffffff', // White
border: '#ced4da', // Light gray border
},
spacing: {
xxsmall: '4px',
xsmall: '8px',
small: '12px',
medium: '16px',
large: '24px',
xlarge: '32px',
xxlarge: '48px',
},
typography: {
fontFamily: '"Inter", sans-serif', // Assuming Inter font is imported
fontSize: {
small: '0.875rem', // 14px
medium: '1rem', // 16px
large: '1.125rem', // 18px
},
fontWeight: {
light: 300,
normal: 400,
bold: 700,
},
},
borderRadius: {
small: '4px',
medium: '8px',
large: '12px',
full: '9999px',
},
};
With our theme setup complete, our components can now effortlessly access these tokens through props.theme when using styled-components.
Step-by-Step Implementation: Building Our First Components
Now for the exciting part: writing code! We’ll start with the Button, then move to the Input, ensuring each step is clear and explained.
Building Our First Component: The Button
Our Button component will be versatile, supporting different visual styles (variants) and sizes, making it adaptable to various UI needs.
1. Defining Button Props and Basic Structure (Button.tsx)
Let’s start with src/components/Button/Button.tsx. We’ll define the props our button can accept, leveraging TypeScript for robust type safety and clearer API documentation.
// src/components/Button/Button.tsx
import React from 'react';
import { StyledButton } from './Button.styles';
// 📌 Key Idea: Define component props using TypeScript interfaces for clarity and type safety.
// Extending React.ButtonHTMLAttributes<HTMLButtonElement> automatically includes
// standard HTML button attributes like onClick, type, etc.
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Defines the visual style of the button.
* @default 'primary'
*/
variant?: 'primary' | 'secondary' | 'ghost';
/**
* Defines the size of the button.
* @default 'medium'
*/
size?: 'small' | 'medium' | 'large';
/**
* If true, the button will be disabled and non-interactive.
* @default false
*/
disabled?: boolean;
/**
* The content to be displayed inside the button (e.g., text, icon).
*/
children: React.ReactNode;
}
/**
* A versatile button component for user interactions, supporting various styles and sizes.
*/
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
children,
...props
}) => {
return (
<StyledButton variant={variant} size={size} {...props}>
{children}
</StyledButton>
);
};
Explanation:
- We import
Reactand ourStyledButton(which we’ll create next). ButtonPropsextendsReact.ButtonHTMLAttributes<HTMLButtonElement>. This is a powerful trick! It means ourButtoncomponent automatically accepts all standard HTML button attributes likeonClick,type,aria-label, etc., without us having to list them manually. This is crucial for maintaining HTML semantic correctness and accessibility.- We then add our custom props:
variant,size,disabled, andchildren. - Default values (
primary,medium) are set forvariantandsizedirectly in the functional component’s arguments, providing sensible defaults. - The
childrenprop is where the button’s text or icon will go. - Finally, we render
StyledButton, passing our custom props and spreading...propsto ensure all standard HTML attributes are passed down to the underlyingbuttonelement.
2. Styling the Button with Design Tokens (Button.styles.ts)
Now, let’s create src/components/Button/Button.styles.ts using styled-components and integrate our design tokens.
// src/components/Button/Button.styles.ts
import styled, { css, DefaultTheme } from 'styled-components';
import { ButtonProps } from './Button'; // Import ButtonProps for type safety
// A simple helper to slightly darken a hex color by mixing it with black.
// In a real-world design system, you might use a dedicated color utility library
// or generate these derived colors as part of your design tokens.
const darkenHexColor = (hex: string, percentage: number): string => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const factor = 1 - percentage / 100;
const newR = Math.max(0, Math.floor(r * factor));
const newG = Math.max(0, Math.floor(g * factor));
const newB = Math.max(0, Math.floor(b * factor));
return `#${((1 << 24) + (newR << 16) + (newG << 8) + newB).toString(16).slice(1).padStart(6, '0')}`;
};
// Helper function to get variant styles
const getVariantStyles = (variant: ButtonProps['variant'], theme: DefaultTheme) => {
switch (variant) {
case 'primary':
return css`
background-color: ${theme.colors.primary};
color: ${theme.colors.background}; /* White text on primary */
border: 1px solid ${theme.colors.primary};
&:hover {
background-color: ${darkenHexColor(theme.colors.primary, 10)};
border-color: ${darkenHexColor(theme.colors.primary, 10)};
}
`;
case 'secondary':
return css`
background-color: ${theme.colors.secondary};
color: ${theme.colors.background};
border: 1px solid ${theme.colors.secondary};
&:hover {
background-color: ${darkenHexColor(theme.colors.secondary, 10)};
border-color: ${darkenHexColor(theme.colors.secondary, 10)};
}
`;
case 'ghost':
return css`
background-color: transparent;
color: ${theme.colors.primary};
border: 1px solid ${theme.colors.primary};
&:hover {
background-color: rgba(0, 123, 255, 0.1); /* Light primary tint */
}
`;
default:
return getVariantStyles('primary', theme); // Fallback to primary
}
};
// Helper function to get size styles
const getSizeStyles = (size: ButtonProps['size'], theme: DefaultTheme) => {
switch (size) {
case 'small':
return css`
padding: ${theme.spacing.xsmall} ${theme.spacing.small};
font-size: ${theme.typography.fontSize.small};
`;
case 'medium':
return css`
padding: ${theme.spacing.small} ${theme.spacing.medium};
font-size: ${theme.typography.fontSize.medium};
`;
case 'large':
return css`
padding: ${theme.spacing.medium} ${theme.spacing.large};
font-size: ${theme.typography.fontSize.large};
`;
default:
return getSizeStyles('medium', theme); // Fallback to medium
}
};
export const StyledButton = styled.button<ButtonProps>`
/* Base styles that apply to all buttons */
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: ${props => props.theme.borderRadius.medium};
font-weight: ${props => props.theme.typography.fontWeight.normal};
font-family: ${props => props.theme.typography.fontFamily}; /* Ensure font consistency */
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out;
text-decoration: none; /* Important for accessibility if button acts like a link */
/* Apply variant styles dynamically based on props */
${({ variant, theme }) => getVariantStyles(variant, theme)}
/* Apply size styles dynamically based on props */
${({ size, theme }) => getSizeStyles(size, theme)}
/* Disabled state styling */
&:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none; /* Prevent click events on disabled buttons */
}
`;
Explanation:
- We import
styled,css, andDefaultThemefromstyled-components.cssallows us to write reusable CSS snippets, andDefaultThemeprovides type safety for our theme object. - The
darkenHexColorhelper is a basic utility to programmatically adjust color for hover states. In a larger system, you might pre-define these hover colors as tokens or use a more sophisticated color library. - We define
getVariantStylesandgetSizeStyleshelper functions. These functions take thevariantorsizeprop and thethemeobject (now correctly typed asDefaultTheme), returning specific CSS based on those values. This keeps our mainStyledButtondefinition cleaner and more modular. - Inside
StyledButton, we define base styles that apply to all buttons (e.g.,display,border-radius,font-weight,transition). We also addedfont-familyto ensure it uses our token, andtext-decoration: nonefor visual consistency. - We then use template literals and destructuring (
({ variant, theme })) to apply our variant and size-specific styles dynamically. - The
&:disabledpseudo-class handles the styling for the disabled state, which is automatically applied when thedisabledprop is passed to the underlying HTML button. We also addedpointer-events: none;to ensure clicks are truly ignored.
3. Documenting the Button with Storybook (Button.stories.tsx)
Now that our Button component is ready, let’s create a Storybook story for it. This will allow us to visualize, test, and document its different states and props in an isolated environment.
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { ThemeProvider } from 'styled-components';
import { defaultTheme } from '../../theme';
// 🧠 Important: Storybook's Meta defines the component and its default args.
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'], // Enables automatic documentation generation
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost'],
description: 'Defines the visual style of the button.',
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Defines the size of the button.',
},
disabled: {
control: 'boolean',
description: 'If true, the button will be disabled.',
},
onClick: {
action: 'clicked', // Logs a click event in Storybook's actions panel for debugging
description: 'Callback fired when the button is clicked.',
},
children: {
control: 'text',
description: 'The content to be displayed inside the button.',
},
},
decorators: [ // Decorators wrap stories, useful for context providers like ThemeProvider
(Story) => (
<ThemeProvider theme={defaultTheme}>
<Story />
</ThemeProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof Button>;
// ⚡ Quick Note: Each export in a Storybook file creates a "story" for the component,
// showcasing different prop combinations.
export const Primary: Story = {
args: {
variant: 'primary',
size: 'medium',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
size: 'medium',
children: 'Secondary Button',
},
};
export const Ghost: Story = {
args: {
variant: 'ghost',
size: 'medium',
children: 'Ghost Button',
},
};
export const Small: Story = {
args: {
variant: 'primary',
size: 'small',
children: 'Small Button',
},
};
export const Large: Story = {
args: {
variant: 'primary',
size: 'large',
children: 'Large Button',
},
};
export const Disabled: Story = {
args: {
variant: 'primary',
disabled: true,
children: 'Disabled Button',
},
};
To see your Button in action, run Storybook from your project’s root directory:
npm run storybook
This will typically open a browser window at http://localhost:6006 (or a similar port), where you can navigate to your Button component and interact with its various controls and stories.
Building Our Second Component: The Input
Let’s apply the same principles to build an Input component. This will also be highly configurable and accessible, serving as a fundamental form element.
1. Defining Input Props and Basic Structure (Input.tsx)
// src/components/Input/Input.tsx
import React from 'react';
import { StyledInputWrapper, StyledInput, StyledLabel } from './Input.styles';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
/**
* Optional label for the input field. Crucial for accessibility.
*/
label?: string;
/**
* Defines the size of the input field.
* @default 'medium'
*/
size?: 'small' | 'medium' | 'large';
/**
* If true, the input will be disabled and non-interactive.
* @default false
*/
disabled?: boolean;
}
/**
* A reusable input component for text entry, supporting a label and various sizes.
*/
export const Input: React.FC<InputProps> = ({
label,
id, // Important for accessibility: link label to input
size = 'medium',
disabled = false,
...props
}) => {
// If no ID is provided, generate a unique one for accessibility.
// React.useId() is available from React 18 and ensures unique IDs even in SSR environments.
const inputId = id || React.useId();
return (
<StyledInputWrapper>
{label && <StyledLabel htmlFor={inputId}>{label}</StyledLabel>}
<StyledInput id={inputId} size={size} disabled={disabled} {...props} />
</StyledInputWrapper>
);
};
Explanation:
- Similar to
Button,InputPropsextendsReact.InputHTMLAttributes<HTMLInputElement>to inherit standard HTML input attributes, ensuring flexibility and adherence to web standards. - We add
labelandsizeas custom props. - Accessibility: We use
React.useId()(available in React 18+) to ensure a uniqueidfor the input. Thisidis crucial for linking thelabelto theinputusing thehtmlForattribute. This is a fundamental accessibility pattern for form controls. If a customidis passed, we prioritize that. - The component renders a
StyledInputWrapper(for layout and spacing), an optionalStyledLabel, and theStyledInputitself.
2. Styling the Input with Design Tokens (Input.styles.ts)
// src/components/Input/Input.styles.ts
import styled, { css, DefaultTheme } from 'styled-components';
import { InputProps } from './Input';
const getSizeStyles = (size: InputProps['size'], theme: DefaultTheme) => {
switch (size) {
case 'small':
return css`
padding: ${theme.spacing.xsmall} ${theme.spacing.small};
font-size: ${theme.typography.fontSize.small};
height: 32px; // Fixed height for consistency
`;
case 'medium':
return css`
padding: ${theme.spacing.small} ${theme.spacing.medium};
font-size: ${theme.typography.fontSize.medium};
height: 40px;
`;
case 'large':
return css`
padding: ${theme.spacing.medium} ${theme.spacing.large};
font-size: ${theme.typography.fontSize.large};
height: 48px;
`;
default:
return getSizeStyles('medium', theme); // Fallback to medium
}
};
export const StyledInputWrapper = styled.div`
display: flex;
flex-direction: column;
gap: ${props => props.theme.spacing.xxsmall}; // Space between label and input
width: 100%; // Inputs often take full width of their container
`;
export const StyledLabel = styled.label`
font-size: ${props => props.theme.typography.fontSize.small};
font-weight: ${props => props.theme.typography.fontWeight.normal};
color: ${props => props.theme.colors.text};
`;
export const StyledInput = styled.input<InputProps>`
width: 100%;
border: 1px solid ${props => props.theme.colors.border};
border-radius: ${props => props.theme.borderRadius.medium};
color: ${props => props.theme.colors.text};
background-color: ${props => props.theme.colors.background};
font-family: ${props => props.theme.typography.fontFamily};
font-weight: ${props => props.theme.typography.fontWeight.normal};
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
/* Apply size styles dynamically based on props */
${({ size, theme }) => getSizeStyles(size, theme)}
&:focus {
outline: none; /* Remove default browser outline */
border-color: ${props => props.theme.colors.primary};
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); /* Custom focus ring for accessibility */
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: #f8f9fa; /* Lighter background for disabled state */
}
&::placeholder {
color: ${props => props.theme.colors.secondary}; /* Lighter color for placeholder text */
opacity: 1; /* Ensure placeholder is visible in Firefox, which can have lower default opacity */
}
`;
Explanation:
- We reuse the
getSizeStylespattern for our input, ensuring consistency in howsizeprops are handled across components. StyledInputWrapperprovides a flexible container for the label and input, managing their vertical layout and spacing using theme tokens.StyledLabelapplies basic typography from our theme, ensuring labels are legible and visually consistent.StyledInputdefines the core styling for the input field, including borders, padding, font, and colors from our design tokens.- Focus State: The
:focuspseudo-class is critical for accessibility. It provides a clear visual indicator when the input is active, allowing keyboard users to see where they are currently interacting. We remove the default browser outline and apply a custom, branded focus ring. - Disabled State: Similar to the button,
:disabledhandles the styling when the input is not interactive, providing a visual cue to the user. - Placeholder Styling:
&::placeholderensures the placeholder text is styled appropriately, using a secondary color from our tokens for subtlety.
3. Documenting the Input with Storybook (Input.stories.tsx)
// src/components/Input/Input.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Input } from './Input';
import { ThemeProvider } from 'styled-components';
import { defaultTheme } from '../../theme';
const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
tags: ['autodocs'],
argTypes: {
label: {
control: 'text',
description: 'Optional label for the input field.',
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Defines the size of the input field.',
},
disabled: {
control: 'boolean',
description: 'If true, the input will be disabled.',
},
placeholder: {
control: 'text',
description: 'Placeholder text for the input.',
},
value: {
control: 'text',
description: 'The current value of the input.',
},
onChange: {
action: 'changed', // Logs change events in Storybook's actions panel
description: 'Callback fired when the input value changes.',
},
type: {
control: 'text',
description: 'The type of input (e.g., text, password, email).',
},
},
decorators: [
(Story) => (
<ThemeProvider theme={defaultTheme}>
<Story />
</ThemeProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof Input>;
export const Default: Story = {
args: {
label: 'Email Address',
placeholder: 'Enter your email',
type: 'email',
},
};
export const WithValue: Story = {
args: {
label: 'Username',
value: 'john.doe',
onChange: () => {}, // Provide an empty function to make it controlled without actual state
},
};
export const SmallInput: Story = {
args: {
label: 'Search Query',
size: 'small',
placeholder: 'Search...',
},
};
export const LargeInput: Story = {
args: {
label: 'Description',
size: 'large',
placeholder: 'Tell us more...',
},
};
export const DisabledInput: Story = {
args: {
label: 'Disabled Field',
placeholder: 'You cannot type here',
disabled: true,
},
};
Run npm run storybook again, and you’ll now see both your Button and Input components, fully styled and interactive within the Storybook environment!
4. Exporting Components
Finally, let’s create src/components/index.ts to provide a clean and organized way to import all our components from other parts of our application. This central export point simplifies imports for consuming applications.
// src/components/index.ts
export * from './Button/Button';
export * from './Input/Input';
// Add more component exports here as you build them
Now, from any file in your project, you can import components like this:
import { Button, Input } from '../components'; // Or from '@your-design-system/components' in a monorepo setup
Mini-Challenge: Enhancing Your Button
You’ve built a solid foundation for your Button and Input. Now, let’s add a common feature to the Button component to make it even more robust and user-friendly.
Challenge: Add a loading state to your Button component. When the loading prop is true, the button should:
- Display a visual loading indicator (e.g., simple text like “Loading…”, or an SVG spinner if you feel adventurous).
- Automatically be
disabledto prevent further interactions. - Optionally, prevent the
onClickhandler from firing while loading.
Hint:
- Add a
loading?: boolean;prop to yourButtonPropsinterface inButton.tsx. - Inside the
Buttonfunctional component, use conditional rendering to showchildrenor your loading indicator based on theloadingprop’s value. - Modify the
disabledprop passed toStyledButtonso it’strueif either thedisabledprop is explicitly set, OR ifloadingistrue. - Consider using
pointer-events: none;in theStyledButtonfor the loading state to ensure no clicks register. - You might also want to update your Storybook stories to showcase the new
loadingstate!
What to observe/learn: This exercise reinforces conditional rendering, prop handling, and how to manage component states effectively. It also highlights the importance of thinking about user feedback (like loading states) in component design and how to combine multiple props to control behavior.
Common Pitfalls & Troubleshooting
Building components for a design system comes with its own set of challenges. Here are a few common mistakes and how to avoid them:
- Forgetting Accessibility: It’s easy to overlook crucial accessibility features like
labelandidfor inputs, or correct keyboard navigation and ARIA attributes for interactive elements.- Solution: Integrate accessibility checks into your development workflow from day one. Use semantic HTML whenever possible, and consult ARIA guidelines for complex components. Storybook addons like
@storybook/addon-a11ycan help catch issues early. Regular manual testing with keyboard navigation and screen readers is also invaluable.
- Solution: Integrate accessibility checks into your development workflow from day one. Use semantic HTML whenever possible, and consult ARIA guidelines for complex components. Storybook addons like
- Inconsistent Styling (Bypassing Tokens): Developers might be tempted to hardcode colors, spacing, or font sizes (e.g., using direct hex codes or pixel values) instead of consistently using design tokens.
- Solution: Enforce strict use of the
themeobject and its tokens. Implement linting rules that flag hardcoded style values. Educate your team on the “why” behind design tokens (consistency, scalability, theming) and conduct thorough code reviews.
- Solution: Enforce strict use of the
- Over-engineering vs. Starting Simple: Trying to account for every possible prop, variant, or edge case from the very start can lead to overly complex, unmaintainable code that is difficult to onboard new developers to.
- Solution: Start with the most common and essential use cases for each component. Build variants and add features iteratively as real-world needs and feedback emerge. Remember, a design system is a living product that evolves over time, not a one-time, static build.
- Prop Drilling: While less common for the simple, atomic components we’re building now, as your application grows, passing props down through many layers of components can become cumbersome and lead to messy code.
- Solution: For design system components, this is less of an issue as they are typically atomic “leaf” components. However, for larger application contexts, consider React’s Context API or state management libraries (like Redux, Zustand) for global or shared data that many components need, reducing the need to pass props through intermediate components.
Summary
Phew! You’ve just built your first two fundamental components for your design system, integrating them with design tokens and documenting them with Storybook. This is a huge milestone! Here are the key takeaways from this chapter:
- Component Structure: Well-designed components are reusable, configurable via clearly defined props, focused on a single responsibility, and accessible by default.
- Styling with CSS-in-JS: Libraries like Styled Components provide a powerful and efficient way to style React components, enabling dynamic and theme-driven styles directly within your JavaScript.
- Design Token Integration: We successfully used our
themeobject to apply consistent colors, spacing, and typography to our components, ensuring visual harmony across the system. - Accessibility First: Basic accessibility practices, such as linking labels to inputs with
idandhtmlFor, and providing clear focus states, are crucial and should be baked into component design from the start. - Storybook for Documentation: Storybook is an invaluable tool for developing, showcasing, and comprehensively documenting components in isolation, facilitating collaboration and understanding.
You’ve taken a significant step from abstract design principles to concrete, interactive UI elements. This hands-on experience is foundational. In the next chapter, we’ll explore how to expand our component library further, manage state within more complex components, and consider different ways to compose UI patterns.
References
- Styled Components Documentation
- Storybook Documentation
- React Documentation:
useId - MDN Web Docs: Accessibility
- Primer Design System Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.