How to build a design system in React
Building a design system in React is about more than just a component library; it is about creating a single source of truth for your UI logic and visual identity. With over 25 years of experience in software development and as the creator of CoreUI, I’ve architected dozens of design systems that power enterprise-level applications. The most efficient approach involves using design tokens for constants, the React Context API for theming, and a component-driven architecture to ensure modularity. By following these principles, you can create a robust system that improves both developer velocity and design consistency across your organization.
Use a combination of design tokens, React Context for theming, and atomic component architecture to build a scalable design system.
1. Defining Design Tokens
Design tokens are the visual atoms of your system. They represent values like colors, spacing, and typography that remain constant across your application. By abstracting these values, you ensure that a change in the brand color only needs to be updated in one place.
const tokens = {
colors: {
primary: '#321fdb',
success: '#2eb85c',
danger: '#e55353',
background: '#f8f9fa',
text: '#ffffff',
textDark: '#303c54'
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px'
},
typography: {
fontFamily: "'Segoe UI', Roboto, sans-serif",
fontSize: {
base: '16px',
lg: '20px',
sm: '14px'
}
}
}
export default tokens
By using tokens, you avoid hardcoding hex codes or pixel values inside your components. This is the same philosophy we use in the React Dashboard Template to maintain a cohesive look across hundreds of pages.
2. Implementing the Theme Provider
To make your design tokens accessible throughout the React component tree, you should use the Context API. This allows any component to subscribe to theme changes or access shared variables without prop-drilling.
import React, { createContext, useContext } from 'react'
import tokens from './tokens'
const ThemeContext = createContext(tokens)
export const ThemeProvider = ({ children }) => {
return (
<ThemeContext.Provider value={tokens}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
This setup provides a centralized mechanism for managing visual state. If you ever need to implement a dark mode, you simply swap the tokens object passed to the provider based on the user’s preference.
3. Creating Atomic Components
The core of your design system consists of atomic components like buttons, inputs, and badges. These should be highly reusable and agnostic of business logic. A well-built button component handles various states (loading, disabled, primary) while adhering to the theme tokens.
import React from 'react'
import { useTheme } from './ThemeProvider'
export const CustomButton = ({ variant = 'primary', children, ...props }) => {
const { colors, spacing } = useTheme()
const styles = {
backgroundColor: colors[variant],
padding: `${spacing.sm} ${spacing.md}`,
color: colors.text,
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}
return (
<button style={styles} {...props}>
{children}
</button>
)
}
For more complex interactive elements, I recommend leveraging CoreUI Buttons which already include built-in support for accessibility and various framework-specific optimizations.
4. Standardizing Form Inputs
Forms are the most frequent point of interaction for users. A design system must provide consistent input styling and validation feedback. Using a base input component ensures that every field in your app looks and behaves predictably.
import React from 'react'
import { useTheme } from './ThemeProvider'
export const BaseInput = ({ label, ...props }) => {
const { spacing, colors } = useTheme()
return (
<div style={{ marginBottom: spacing.md }}>
<label style={{ display: 'block', marginBottom: spacing.xs }}>
{label}
</label>
<input
style={{
width: '100%',
padding: spacing.sm,
borderColor: colors.primary,
borderStyle: 'solid',
borderWidth: '1px'
}}
{...props}
/>
</div>
)
}
When building enterprise forms, you might find it easier to use the CoreUI Input Group components, which handle prefix/suffix icons and layout alignment automatically.
5. Layout and Spacing Components
A common mistake in design systems is neglecting the space between components. By creating a “Box” or “Stack” component, you can control the layout using your spacing tokens, preventing developers from adding arbitrary margins in CSS.
import React from 'react'
import { useTheme } from './ThemeProvider'
export const Stack = ({ children, gap = 'md', direction = 'column' }) => {
const { spacing } = useTheme()
const style = {
display: 'flex',
flexDirection: direction,
gap: spacing[gap]
}
return <div style={style}>{children}</div>
}
This component-driven layout approach ensures that the white space in your application remains consistent and predictable across every screen.
6. Composition with CoreUI Components
One of the most effective ways to build a design system is to wrap and extend established libraries. Instead of building a modal or a complex data table from scratch, you can wrap CoreUI components with your design tokens to provide a custom API for your team.
import React from 'react'
import { CModal, CModalHeader, CModalTitle, CModalBody } from '@coreui/react'
export const AppModal = ({ visible, title, children, onClose }) => {
return (
<CModal visible={visible} onClose={onClose} alignment='center'>
<CModalHeader onClose={onClose}>
<CModalTitle>{title}</CModalTitle>
</CModalHeader>
<CModalBody>
{children}
</CModalBody>
</CModal>
)
}
By using the CoreUI Modal as a foundation, you gain professional features like keyboard navigation and focus trapping immediately, while still maintaining the ability to style it according to your brand tokens.
7. Documenting Component APIs
A design system is only as good as its documentation. In React, using TypeScript or PropTypes is essential for defining the interface of your components. This acts as living documentation that tells other developers how to use the system correctly.
import PropTypes from 'prop-types'
CustomButton.propTypes = {
variant: PropTypes.oneOf(['primary', 'success', 'danger']),
children: PropTypes.node.isRequired,
onClick: PropTypes.func
}
BaseInput.propTypes = {
label: PropTypes.string.isRequired,
type: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func
}
Stack.propTypes = {
children: PropTypes.node.isRequired,
gap: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
direction: PropTypes.oneOf(['row', 'column'])
}
Defining clear prop types prevents common bugs and makes the design system discoverable through IDE autocomplete.
Best Practice Note:
Consistency is the primary goal of any design system. Always prioritize composition over configuration; it is better to have three specific button components (Primary, Secondary, Ghost) than one button with fifty conditional props. This is the same approach we take with CoreUI components — we provide a robust base that you can easily extend or restrict based on your project’s specific needs. For large projects, consider using a tool like Storybook to visualize your components in isolation during the development phase.
Related answers
- How to create a theme provider in React
- How to implement dark mode in React
- How to create reusable components in React



