Production engineering guide for design system engineering covering patterns, implementation strategies, and operational best practices.
Design systems unify the look, behavior, and implementation of user interfaces across teams and products. Design System Engineering is the practice of building, maintaining, and evolving a shared component library, style guide, and tooling stack that enables consistent, scalable frontend development. It matters most when multiple teams ship UIs independently but must align on a cohesive experience, especially when those teams are distributed, evolving at different paces, and managing different parts of a product ecosystem.
A component library is the core artifact of a design system. It contains reusable UI elements—buttons, forms, modals, cards—each with defined props, states, and interactivity. The library must be versioned, documented, and consumable via a package manager.
To scaffold a new component library, use a standard toolchain: npm for package management, TypeScript for type safety, Jest for testing, and Storybook for documentation and interaction. The project layout follows a predictable structure:
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.stories.tsx
│ │ ├── Button.test.tsx
│ │ └── Button.css
├── tokens/
│ ├── colors.ts
│ ├── spacing.ts
│ └── typography.ts
├── utils/
│ └── theme.ts
package.json
tsconfig.json
Each component is built as a standalone React component with explicit props and default values. For example:
// src/components/Button/Button.tsx
import React from 'react';
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
}
export const Button = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
}: ButtonProps) => {
return (
<button
className={`btn btn-${variant} btn-${size} ${disabled ? 'disabled' : ''}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
The package.json includes a build script and publish script to version and release the library:
{
"scripts": {
"build": "tsc",
"test": "jest",
"prepare": "npm run build",
"publish": "npm run build && npm run test && npm publish"
},
"files": [
"dist",
"README.md"
],
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
}
Publishing requires a CHANGELOG.md that tracks breaking, feature, and patch changes. The npm publish command uploads the package to the public registry with a version bump via npm version.
Design tokens are the foundational values that define a system’s visual language: colors, spacing, typography, shadows, and more. They are typically exported as JSON, YAML, or as code constants.
Tokens are authored in a shared tokens/ directory and compiled into multiple formats—CSS variables, JavaScript objects, and JSON files—for consumption across frontend, backend, and design tools.
To generate CSS custom properties from tokens:
// tokens/colors.json
{
"primary": "#0070f3",
"secondary": "#6c6c6c",
"success": "#33cc33",
"error": "#ff3333"
}
Use a simple build script to generate css/tokens.css:
npx json2css tokens/colors.json --output css/tokens.css --prefix --format=css
This produces:
:root {
--color-primary: #0070f3;
--color-secondary: #6c6c6c;
--color-success: #33cc33;
--color-error: #ff3333;
}
Tokens are versioned alongside the component library. When a team updates tokens/spacing.ts, they must run npm run build-tokens to update all formats.
A common failure mode is mismatched token values between the design tool and the frontend. A button’s padding might be 16px in Figma but 1.25rem in CSS. The solution is to generate a tokens-visual.md file that renders each token as a swatch and a code example:
### Spacing
| Value | CSS | Figma | Example |
|-------|-----|-------|---------|
| 8px | `0.5rem` | `8` |  |
| 16px | `1rem` | `16`|  |
This file is published alongside Storybook and used by design and engineering teams to verify alignment.
A design system fails when components behave differently across teams or when they don’t follow the system’s conventions. Engineering ensures consistency through automated tooling and standardized workflows.
All code must pass eslint and prettier. A package.json script enforces this:
{
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"format": "prettier --write src/**/*.{ts,tsx,js,jsx,json,md}",
"check": "npm run lint && npm run format"
}
}
A .eslintrc.json config enforces rules across the organization:
{
"extends": ["eslint:recommended", "plugin:react/recommended"],
"plugins": ["react", "jest"],
"rules": {
"react/prop-types": "error",
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-undef": "error",
"semi": ["error", "always"],
"quotes": ["error", "single"]
},
"settings": {
"react": {
"version": "detect"
}
}
}
Team members run npm run check before committing, and CI pipelines fail if any linting or formatting errors exist.
Each component must have unit tests and visual regression tests. Storybook integrates with jest and playwright to automate testing.
A test/visual.ts script runs visual tests using playwright:
import { test, expect } from '@playwright/test';
import { loadStory } from './utils';
test('Button - primary, large, disabled', async ({ page }) => {
await loadStory(page, 'Button', 'Primary Large Disabled');
await expect(page).toHaveScreenshot('button-primary-large-disabled.png');
});
Running the visual test suite:
npx playwright test --config=playwright.config.ts
The playwright.config.ts file configures browser context and screenshot options:
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: 'test',
timeout: 30_000,
expect: {
toHaveScreenshot: {
threshold: 0.1,
maxDiffPixels: 100,
},
},
use: {
viewport: { width: 1280, height: 720 },
screenshot: 'only-on-failure',
},
};
export default config;
A failure in visual regression produces a diff.png with red overlays showing pixel-level differences between expected and actual screenshots. Engineers review these locally using playwright show-coverage.
Design systems must be integrated into the daily workflow of frontend teams. This involves publishing, consuming, and tracking component usage.
The component library is published to a private registry (e.g., npm with registry.npmjs.org, or Artifactory) and versioned using semantic versioning. A CHANGELOG.md is updated with each release:
## 2.3.1 (2024-06-12)
### Fixed
- Button: fix `disabled` state style mismatch with Figma design.
### Added
- Modal: added `closeOnEscape` prop.
### Breaking
- Theme: renamed `colorScheme` to `themeMode`.
A release is triggered via a GitHub Actions workflow:
name: Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: npm run build
- run: npm run test
- run: npm run publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Teams consume the design system via npm install and import components directly:
npm install @company/ui
In their src/index.ts, they import and register the theme:
import { ThemeProvider } from '@company/ui';
import { theme } from '@company/ui/tokens';
const App = () => (
<ThemeProvider theme={theme}>
<Button variant="primary" size="large">
Click me
</Button>
</ThemeProvider>
);
The @company/ui package includes index.css, theme.css, and tokens.css to ensure consistent styling across apps.
The design system tracks usage via analytics and telemetry. Components emit component-usage events when rendered:
window.dispatchEvent(new CustomEvent('component-usage', {
detail: {
name: 'Button',
props: { variant: 'primary', size: 'large' },
path: '/products/123',
timestamp: Date.now(),
},
}));
These events are collected and stored in a database. A SQL query surfaces the most-used components:
SELECT
component_name,
COUNT(*) as usage_count,
JSONB_AGG(DISTINCT props) as common_props
FROM component_usage
GROUP BY component_name
ORDER BY usage_count DESC
LIMIT 10;
This data guides future development: a button used in 80% of pages but rarely with size="small" signals the need for a small variant.
A design system is not static. It evolves with the product, and engineering ensures that changes are predictable and non-breaking.
When introducing breaking changes, engineers must:
CHANGELOG.md with a BREAKING CHANGE section.export function Button({ variant, size, ...props }) {
if (variant === 'primary' && props.size === 'small') {
console.warn(
'Button: `size="small"` is deprecated. Use `size="small"` with `variant="primary"` instead.'
);
}
// ...
}
@deprecated tags.npm version major).When multiple teams use different versions of the design system, a package.json might include conflicting versions of @company/ui.
To resolve this, teams use npm dedupe and npm-force-resolutions:
{
"resolutions": {
"@company/ui": "2.3.0"
},
"scripts": {
"prepare": "npm dedupe && npm run resolve-dependencies"
}
}
The resolve-dependencies script ensures all apps use the same version of the design system, reducing bundle size and runtime inconsistencies.
16px spacing in Figma is 1rem in CSS, but the rem unit is not defined in the root. The solution is to generate a css/typography.css file that includes font-size: 1rem and line-height: 1.5 for all text.stories field in package.json is missing. The fix is to ensure stories: ['src/**/*.stories.tsx'].playwright test runs but the diff.png is not uploaded. The solution is to use playwright show-coverage and npx playwright open to inspect failures.TypeScript 5, but the consuming app uses TypeScript 4. The solution is to pin tsconfig.json to target: "es2020" and module: "esnext".Engineering a design system is not just about shipping components—it’s about orchestrating consistency, quality, and evolution across a distributed ecosystem of teams and applications.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy