Design and maintain a component library that multiple teams can actually use. Covers API design, composition patterns, theming architecture, versioning strategy, and the governance model that prevents design systems from becoming legacy code.
A component library architecture is the foundation of a design system that evolves with an organisation’s frontend complexity. It enables teams to share, version, and deploy UI elements consistently across applications, from small internal tools to large-scale customer-facing platforms. This architecture matters when multiple teams build independent features using the same visual language, and when the cost of inconsistency—rework, onboarding friction, and maintenance debt—starts to outweigh the benefits of isolated development.
A scalable component library must balance consistency, reusability, and developer experience. The key is to decouple the library’s implementation from its consumption. This means components are built once, tested thoroughly, and published to a package registry—such as npm, GitHub Packages, or Artifactory—where they are consumed by frontend applications via a standard dependency mechanism.
Components should be self-contained, versioned, and documented. Each component exposes a public API defined by props, slots, events, and styles. The library’s build process compiles these components into a set of files: a JavaScript bundle, a CSS file, and optionally a type declaration file (*.d.ts) for TypeScript consumers.
Each component lives in a directory under src/components/ or packages/components/. The structure is standardized across all components.
Button/
├── Button.tsx
├── Button.stories.mdx
├── Button.test.tsx
├── Button.css
├── Button.types.ts
├── Button.docs.md
└── index.ts
Button.tsx exports the React component using export default and includes all internal logic.Button.stories.mdx contains interactive documentation using Storybook’s syntax.Button.test.tsx includes unit and integration tests using Jest.Button.css contains scoped styles, optionally using CSS modules.Button.types.ts defines the component’s TypeScript interface.Button.docs.md is markdown content for documentation, used in Storybook or a static site.index.ts re-exports the component and its types for easy consumption.The index.ts file is critical. It must re-export all public APIs:
// packages/components/Button/index.ts
export { default as Button } from './Button';
export type { ButtonProps } from './Button.types';
export * from './Button.types';
Consumers import the component as:
import { Button } from '@company/ui';
This pattern allows teams to evolve the library without breaking consumers, provided they follow semantic versioning (SemVer) and maintain backward compatibility.
The library uses a standard build pipeline with tsc for TypeScript compilation and rollup or vite for bundling. The build output is published to a package registry.
// package.json (in the component library root)
{
"name": "@company/ui",
"version": "1.2.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsc && rollup -c",
"test": "jest",
"lint": "eslint src/",
"publish": "npm run build && npm publish"
},
"devDependencies": {
"typescript": "^5.0.0",
"rollup": "^4.0.0",
"rollup-plugin-typescript2": "^0.32.0",
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}
The Rollup configuration (rollup.config.js) defines how to generate both CommonJS and ES modules:
// rollup.config.js
import typescript from 'rollup-plugin-typescript2';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/index.esm.js',
format: 'es',
sourcemap: true
}
],
plugins: [
nodeResolve({ browser: true }),
typescript({
tsconfig: 'tsconfig.build.json'
}),
terser()
]
};
This configuration ensures that consumers using Webpack, Vite, or plain <script> tags can consume the library with minimal setup.
Versioning is critical. The library follows SemVer: breaking changes in the public API increment the major version, new features increment the minor, and bug fixes increment the patch.
When a new version is published, consumers can update via:
npm install @company/ui@latest
or pin to a specific version:
npm install @company/[email protected]
The package.json of a consuming app lists the component library as a dependency:
{
"dependencies": {
"@company/ui": "^1.2.0"
}
}
Consumers can also use peerDependencies to ensure consistent versions of shared dependencies like React, ReactDOM, and styled-system.
{
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
"styled-components": "^5.0.0"
}
}
This prevents multiple copies of React in the final bundle and reduces bundle size.
Storybook serves as the living documentation and development environment for the component library. It is configured using storybook.config.js and main.js.
// storybook.config.js
import { mergeConfig } from '@storybook/react';
import path from 'path';
export default mergeConfig({
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.tsx'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-controls',
'@storybook/addon-links'
],
webpackFinal: async (config) => {
config.resolve.modules.unshift(path.resolve(__dirname, '../src'));
return config;
}
});
This config enables automatic discovery of stories, supports controls for props, and allows developers to interact with components in isolation.
When a component changes, Storybook can be built and served via:
npm run build-storybook
npx serve -s storybook-static
The generated static site is hosted on a CDN, providing a public API reference and interactive playground.
A scalable library supports theming via a global configuration object. This is passed to the library’s root component or via a context provider.
import { ThemeProvider, Button } from '@company/ui';
const App = () => {
return (
<ThemeProvider
theme={{
colors: {
primary: '#0070f3',
secondary: '#6b46c1',
background: '#f9fafb'
},
spacing: {
small: '0.5rem',
medium: '1rem',
large: '2rem'
}
}}
>
<Button variant="primary">Click me</Button>
</ThemeProvider>
);
};
The theme object is validated at runtime using zod or io-ts, and the library provides a default theme that can be extended.
Theme values are compiled to CSS variables in the library’s CSS output:
:root {
--color-primary: #0070f3;
--color-secondary: #6b46c1;
--spacing-small: 0.5rem;
--spacing-medium: 1rem;
}
Consumers can override these variables in their global stylesheet:
body {
--color-primary: #1a73e8;
--spacing-medium: 1.2rem;
}
The library’s CI pipeline ensures that every commit to main triggers a build, test, and publish workflow.
name: Publish Component Library
on:
push:
branches: [ main ]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Build library
run: npm run build
- name: Run tests
run: npm test
- name: Publish to npm
run: npm run publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
This workflow runs on every push to main, ensuring that the library is always up-to-date.
1. Missing index.ts re-exports: Consumers import Button from @company/ui/Button, but the component is not exported from index.ts. This causes Cannot find module '@company/ui/Button' in the browser.
2. Incorrect main and module fields: A library uses module for ES modules but consumers using CommonJS (e.g., Webpack) fail to load the component, resulting in TypeError: require is not a function.
3. Misconfigured peerDependencies: A consumer has React 17, but the library expects React 18. The app shows Warning: React version mismatch and may exhibit subtle rendering issues.
4. Storybook not picking up stories: The stories field in storybook.config.js is missing or incorrectly structured. Storybook shows no components, and developers must manually register each story.
5. CSS not scoped or not applied: The library’s Button.css is not imported in the component’s Button.tsx, or the CSS is not included in the final bundle. The component appears unstyled.
6. Theming not cascading: A ThemeProvider is nested, but the inner theme does not override the outer theme. The issue lies in the merge function not deep-merging the theme object.
7. Missing sourcemap: true in build: Consumers see minified, untraceable JavaScript in production, making debugging difficult. The sourceMappingURL is missing from the bundle.
8. Version mismatch in package.json: A library publishes @company/[email protected], but the package.json version is 1.1.9. This leads to confusion and inconsistency across teams.
Teams adopt the library incrementally. The first step is to include the library in a single app, then gradually replace existing components.
A migration script can help identify which components are used and which are underutilized.
npx @company/ui/migrate --output=migration-report.json
This command scans the app’s source code, identifies all Button usages, and suggests improvements: adding missing props, converting inline styles to theme variables, and updating to new API.
The library evolves via pull requests. A contributor adds a new Card component:
Card.tsx with CardProps.Card.stories.mdx with examples.Card.test.tsx with Jest.index.ts to include the new component.PR reviewers check for:
When merged, the library is built and published automatically.
A scalable component library architecture is not just a collection of components. It is a system of interlocking processes: a defined structure, a consistent build pipeline, a versioning strategy, and a documentation ecosystem. It enables teams to work independently, share knowledge, and evolve a unified design language across an organisation.
The architecture succeeds when a new developer can add a component, publish it, and see it in use across multiple applications—all with minimal friction and no surprises.
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