Next.js Design Systems
Introduction
Design systems are collections of reusable components, guided by clear standards, that can be assembled to build any number of applications. In Next.js projects, implementing a design system helps maintain consistency across your application while enabling teams to build features faster with pre-built, tested components.
This guide will walk you through creating and implementing a design system in your Next.js application, from basic concepts to practical implementation. You'll learn how design systems can streamline your development process, ensure UI consistency, and make your codebase more maintainable.
What is a Design System?
A design system consists of:
- UI Components - Reusable building blocks like buttons, forms, and cards
- Design Tokens - Variables for colors, spacing, typography, etc.
- Documentation - Guidelines for using components and maintaining the system
- Patterns - Solutions to common design problems
In Next.js applications, design systems help bridge the gap between design and development, ensuring that both teams work from a single source of truth.
Setting Up a Design System in Next.js
Step 1: Setting Up the Project Structure
Let's start by organizing our design system files:
/components
/design-system
/atoms
Button.js
Input.js
/molecules
Form.js
Card.js
/organisms
Header.js
Footer.js
/styles
/tokens
colors.js
spacing.js
typography.js
This structure follows the Atomic Design methodology, which breaks interfaces down into fundamental building blocks that combine to form more complex components.
Step 2: Creating Design Tokens
Design tokens are variables that store design decisions, making it easy to maintain consistency:
// styles/tokens/colors.js
export const colors = {
primary: {
100: '#E6F7FF',
200: '#BAE7FF',
500: '#1890FF',
700: '#0050B3',
900: '#003A8C',
},
neutral: {
100: '#FFFFFF',
200: '#F5F5F5',
300: '#E8E8E8',
500: '#8C8C8C',
900: '#000000',
},
success: '#52C41A',
warning: '#FAAD14',
error: '#FF4D4F',
};
// styles/tokens/spacing.js
export const spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px',
};
// styles/tokens/typography.js
export const typography = {
fontFamily: {
primary: "'Inter', sans-serif",
monospace: "'Roboto Mono', monospace",
},
fontSize: {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
md: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
xxl: '1.5rem', // 24px
},
fontWeight: {
regular: 400,
medium: 500,
bold: 700,
}
};
Step 3: Creating Basic Components
Let's create a simple button component using our design tokens:
// components/design-system/atoms/Button.js
import React from 'react';
import styled from 'styled-components';
import { colors, spacing, typography } from '../../../styles/tokens';
const StyledButton = styled.button`
background-color: ${props => props.variant === 'primary' ? colors.primary[500] : colors.neutral[100]};
color: ${props => props.variant === 'primary' ? colors.neutral[100] : colors.primary[500]};
border: ${props => props.variant === 'primary' ? 'none' : `1px solid ${colors.primary[500]}`};
padding: ${spacing.sm} ${spacing.lg};
border-radius: 4px;
font-family: ${typography.fontFamily.primary};
font-size: ${typography.fontSize.md};
font-weight: ${typography.fontWeight.medium};
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: ${props => props.variant === 'primary' ? colors.primary[700] : colors.primary[100]};
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;
export const Button = ({
children,
variant = 'primary',
disabled = false,
onClick,
...props
}) => {
return (
<StyledButton
variant={variant}
disabled={disabled}
onClick={disabled ? undefined : onClick}
{...props}
>
{children}
</StyledButton>
);
};
Now, let's create a simple input component:
// components/design-system/atoms/Input.js
import React from 'react';
import styled from 'styled-components';
import { colors, spacing, typography } from '../../../styles/tokens';
const StyledInputWrapper = styled.div`
display: flex;
flex-direction: column;
margin-bottom: ${spacing.md};
`;
const StyledLabel = styled.label`
margin-bottom: ${spacing.xs};
font-family: ${typography.fontFamily.primary};
font-size: ${typography.fontSize.sm};
color: ${colors.neutral[900]};
`;
const StyledInput = styled.input`
padding: ${spacing.sm};
border: 1px solid ${props => props.error ? colors.error : colors.neutral[300]};
border-radius: 4px;
font-family: ${typography.fontFamily.primary};
font-size: ${typography.fontSize.md};
&:focus {
outline: none;
border-color: ${colors.primary[500]};
box-shadow: 0 0 0 3px ${colors.primary[100]};
}
`;
const ErrorText = styled.span`
color: ${colors.error};
font-size: ${typography.fontSize.sm};
margin-top: ${spacing.xs};
`;
export const Input = ({
label,
id,
error,
...props
}) => {
return (
<StyledInputWrapper>
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
<StyledInput id={id} error={error} {...props} />
{error && <ErrorText>{error}</ErrorText>}
</StyledInputWrapper>
);
};
Step 4: Combining Components into Molecules
Now we can combine our atoms into more complex components (molecules):
// components/design-system/molecules/Form.js
import React from 'react';
import styled from 'styled-components';
import { spacing } from '../../../styles/tokens';
import { Button } from '../atoms/Button';
const FormContainer = styled.form`
display: flex;
flex-direction: column;
width: 100%;
max-width: 400px;
`;
const ButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-top: ${spacing.md};
gap: ${spacing.sm};
`;
export const Form = ({
children,
onSubmit,
submitText = 'Submit',
cancelText = 'Cancel',
onCancel,
loading = false,
}) => {
return (
<FormContainer onSubmit={(e) => {
e.preventDefault();
onSubmit && onSubmit();
}}>
{children}
<ButtonContainer>
{onCancel && (
<Button
type="button"
variant="secondary"
onClick={onCancel}
disabled={loading}
>
{cancelText}
</Button>
)}
<Button type="submit" variant="primary" disabled={loading}>
{loading ? 'Loading...' : submitText}
</Button>
</ButtonContainer>
</FormContainer>
);
};
Step 5: Usage in Next.js Components
Here's how you'd use these components in a Next.js page:
// pages/signup.js
import React, { useState } from 'react';
import { Input } from '../components/design-system/atoms/Input';
import { Button } from '../components/design-system/atoms/Button';
import { Form } from '../components/design-system/molecules/Form';
export default function SignUpPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
const handleSubmit = async () => {
// Form validation
const newErrors = {};
if (!email) newErrors.email = 'Email is required';
if (!password) newErrors.password = 'Password is required';
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setLoading(true);
// Simulate API call
setTimeout(() => {
console.log('Submitted:', { email, password });
setLoading(false);
// Redirect or show success
}, 1500);
};
return (
<div style={{ padding: '2rem' }}>
<h1>Sign Up</h1>
<Form
onSubmit={handleSubmit}
submitText="Create Account"
loading={loading}
onCancel={() => console.log('Canceled')}
cancelText="Back"
>
<Input
label="Email Address"
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={errors.email}
placeholder="[email protected]"
/>
<Input
label="Password"
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={errors.password}
placeholder="••••••••"
/>
</Form>
</div>
);
}
Using Component Libraries and Design Systems
While building your own design system is educational, most production applications leverage existing component libraries. Here are some popular options that work well with Next.js:
Chakra UI
Chakra UI is a simple, modular component library that gives you building blocks to create accessible React applications:
First, install Chakra UI:
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
Set up the ChakraProvider in your _app.js
:
// pages/_app.js
import { ChakraProvider } from '@chakra-ui/react'
import theme from '../styles/theme'
function MyApp({ Component, pageProps }) {
return (
<ChakraProvider theme={theme}>
<Component {...pageProps} />
</ChakraProvider>
)
}
export default MyApp
Create a custom theme:
// styles/theme.js
import { extendTheme } from '@chakra-ui/react'
const theme = extendTheme({
colors: {
brand: {
100: "#f7fafc",
900: "#1a202c",
},
},
fonts: {
heading: "'Inter', sans-serif",
body: "'Inter', sans-serif",
},
})
export default theme
Use Chakra components in your pages:
// pages/index.js
import {
Box,
Button,
Heading,
Input,
FormControl,
FormLabel,
Stack,
} from '@chakra-ui/react'
export default function Home() {
return (
<Box p={8}>
<Heading mb={6}>Welcome to our App</Heading>
<Stack spacing={4} maxW="md">
<FormControl id="email">
<FormLabel>Email address</FormLabel>
<Input type="email" />
</FormControl>
<Button colorScheme="blue">Sign In</Button>
</Stack>
</Box>
)
}
Material UI
Material UI (MUI) is a popular React UI framework implementing Google's Material Design:
npm install @mui/material @emotion/react @emotion/styled
Set up the theme provider:
// pages/_app.js
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
});
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
Use MUI components:
// pages/index.js
import {
Button,
TextField,
Container,
Box,
Typography
} from '@mui/material';
export default function Home() {
return (
<Container maxWidth="sm">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Material UI Example
</Typography>
<TextField
fullWidth
label="Username"
margin="normal"
variant="outlined"
/>
<TextField
fullWidth
label="Password"
type="password"
margin="normal"
variant="outlined"
/>
<Button
variant="contained"
color="primary"
fullWidth
sx={{ mt: 2 }}
>
Login
</Button>
</Box>
</Container>
);
}
Creating a Component Documentation System
A crucial part of any design system is documentation. Here's how to create simple documentation for your components using Storybook:
# In your Next.js project directory
npx sb init
Create stories for your components:
// stories/Button.stories.js
import React from 'react';
import { Button } from '../components/design-system/atoms/Button';
export default {
title: 'Design System/Atoms/Button',
component: Button,
argTypes: {
variant: {
control: { type: 'select', options: ['primary', 'secondary'] },
},
},
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
variant: 'primary',
children: 'Primary Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
variant: 'secondary',
children: 'Secondary Button',
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
children: 'Disabled Button',
};
Run Storybook:
npm run storybook
Best Practices for Design Systems in Next.js
-
Design tokens over hard-coded values: Always use design tokens for colors, spacing, typography, etc.
-
Component composition: Build complex components by composing simpler ones.
-
Consistent prop naming: Use consistent prop names across components (e.g., always use
onClick
, nothandleClick
oronPress
). -
Accessibility first: Ensure all components are accessible by default.
-
Responsive design: Make components responsive by default using relative units.
-
Performance: Consider the performance impact of your components, especially for animations and complex layouts.
-
Documentation: Document usage, props, and examples for all components.
-
Testing: Test components for functionality, accessibility, and visual regression.
Summary
Design systems in Next.js provide a systematic way to build consistent user interfaces through reusable components. Whether you decide to build your own design system from scratch or leverage existing libraries like Chakra UI or Material UI, the core principles remain the same:
- Extract design decisions into tokens
- Build a component hierarchy from atoms to organisms
- Ensure components are composable and reusable
- Document how to use the components
- Test components for functionality and accessibility
By implementing a well-structured design system, you'll reduce development time, improve UI consistency, and make your codebase more maintainable as your Next.js application grows.
Additional Resources
- Atomic Design by Brad Frost
- Storybook Documentation
- Chakra UI Documentation
- Material UI Documentation
- Design Systems Handbook
Exercises
- Create a design token system for colors, spacing, and typography for your Next.js project.
- Build a Button component with primary, secondary, and disabled states.
- Create a Form component that uses the Button component and handles form submission.
- Implement a dark mode toggle for your design system.
- Set up Storybook and create documentation for your components.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)