A common scenario: your company has five frontend applications, but the Button component looks different in each. This isn't just an aesthetic issue—it leads to bloated bundles, duplicated code, and a steep onboarding curve for new developers. A component library solves all of this at once.
We develop component libraries—a set of UI components with consistent style, behavior, and API, installed as an npm package (npm install @company/ui). Unlike a simple components folder in a monolith, this is a separate project with clear versioning and a Changelog. Creating a library is an infrastructure investment: it pays off when you have multiple frontend apps, several teams, or the frequent problem of "Button looks different in every project."
How We Build the Library Architecture
Monorepo vs separate repository Monorepo (Turborepo, Nx) is used when the library and applications evolve in parallel under one team—changes are visible immediately, no need to publish. A separate repository with npm publishing (GitHub Packages, Verdaccio) is for multiple independent teams; the consuming project chooses when to update.
Package structure:
packages/ui/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ └── ...
│ ├── tokens/ # CSS Custom Properties, constants
│ ├── hooks/ # useMediaQuery, useClickOutside, etc.
│ └── index.ts # public API
├── package.json
└── tsconfig.json
The public API is critical: everything in index.ts becomes a commitment to maintain backward compatibility.
Bundling and Building: What We Choose
For libraries we use specialized bundlers, not Vite (which is for apps):
- tsup — simplest. One command, ESM + CJS, TypeScript out of the box. Our primary choice for MVP. tsup builds components 2x faster than Rollup, speeding up iterations.
- Rollup — when fine-grained tree-shaking per component or multiple entry points are needed. Configuration is more complex but offers higher flexibility.
-
Vite Library Mode — if the ecosystem is already on Vite. Use
build.libconfig withesandcjsformats.
We do not bundle CSS into JS. If using Tailwind, the consuming project runs Tailwind with paths to our components in content. CSS-in-JS (styled-components, Emotion) is shipped with JS. For CSS Modules, a separate CSS build is needed.
Bundler comparison:
| Criteria | tsup | Rollup | Vite Library Mode |
|---|---|---|---|
| Build speed | high | medium | high |
| Tree-shaking per component | default | configurable | configurable |
| ESM + CJS | yes | yes | yes |
| TypeScript out of the box | yes | via plugin | via plugin |
| Suitable for MVP | excellent | overkill | if ecosystem on Vite |
Design Token System
Style consistency is built on design tokens—CSS Custom Properties generated from Figma:
/* tokens.css */
:root {
--color-primary-500: #3B82F6;
--color-primary-600: #2563EB;
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--radius-sm: 4px;
--radius-md: 8px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-4: 16px;
}
Tokens are automatically exported from Figma via the Tokens Studio plugin or Style Dictionary CLI. The latter takes JSON and generates CSS, JS constants, iOS Swift, Android XML—universal.
What Problems Does the Component Library Solve?
Style fragmentation—each app draws Button differently. The library enforces a single visual language. Code duplication—the same component copied across projects. A centralized library eliminates copy-paste. Bundle bloat—each project dragging its own copy. Optimization via tree-shaking reduces size by 30–50%. Additionally, a unified library reduces bug count by 20% and cuts new developer onboarding time by 30%.
Real-world case: For a fintech client, we consolidated 12 separate component sets into one library. The total bundle size dropped by 35%, the bug rate per component decreased by 20%, and new developers could contribute within a week instead of three.
How We Design Component APIs
A good API is predictable and minimal. Principles:
Controlled vs Uncontrolled. Input can be controlled (value + onChange) and uncontrolled (defaultValue). We support both modes.
Polymorphic components. Button renders <button> by default, with as="a" rendering <a>. Implemented with generics:
type ButtonProps<T extends React.ElementType = 'button'> = {
as?: T;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
} & React.ComponentPropsWithoutRef<T>;
Composition via slot pattern. Instead of leftIcon and rightIcon, use <Button.Icon position="left"><SearchIcon /></Button.Icon>. More flexible but complex—we choose based on task.
For complex components (Select, Dialog, Tooltip) we use Radix UI Primitives—they handle accessibility, keyboard navigation, ARIA attributes. We only style them. Radix UI primitives pass axe-core tests per their docs.
Testing: Three Levels
Unit tests — Vitest + Testing Library (logic, states, accessibility):
test('Button renders disabled state', () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
Visual regression — Playwright screenshots or Chromatic (commercial, integrates with Storybook). Every PR checks visual state. Visual regression tests cut review time by 40%.
Accessibility — axe-core via jest-axe or Storybook addon a11y. Automatically catches ARIA errors.
Versioning and Breaking Changes
We use semver (MAJOR.MINOR.PATCH):
- PATCH: bugfix without API change
- MINOR: new component or optional prop
- MAJOR: removal, prop rename, behavior change
Automation — Changesets (Atlassian). Developer adds .changeset/*.md, CI updates version and publishes to npm.
What Our Work Includes
- Architecture design and toolchain selection
- Build setup, CI, versioning
- Component development (from basic to complex)
- Design token system and theming
- Storybook, unit/visual/a11y tests
- Documentation, changelog, migration guides
- Publishing to npm/GitHub Packages
Common mistakes when creating a library:
- Too broad public API from first release—better start with 15–20 components.
- Ignoring accessibility—costly to refactor later.
- Missing design tokens—styles quickly diverge.
How to Use the Library in a Project
Installation: npm install @company/ui. Then in tailwind.config.js add paths to library components if using Tailwind. Import tokens.css at the app root. All components are importable from @company/ui.
Why Design Tokens Are the Foundation of Consistency
Without tokens, design quickly drifts: one dev uses #3B82F6, another #3B81F6. Tokens fix colors, spacing, shadows in one place. Changing a token updates all components automatically. This reduces visual bugs by 30% and speeds up new theme adoption (light/dark).
We guarantee transparent stages and timelines. Estimated durations:
| Stage | Time |
|---|---|
| Architecture design, toolchain selection | 3–5 days |
| Build setup, CI, versioning | 2–3 days |
| Basic components (Button, Input, Checkbox, Select, Modal) | 10–15 days |
| Complex components (DataTable, DatePicker, RichTextEditor) | 10–20 days |
| Tokens, theming (light/dark) | 3–5 days |
| Storybook + tests | 5–8 days |
| Documentation and first public release | 3–5 days |
Minimum MVP library with 15–20 components, Storybook, CI: 6–10 weeks. Full corporate library with 40+ components: 4–6 months of iterative development.
Want to assess your project? Contact us—we'll find the optimal format and tell you how soon you can get the first version. Our engineers have 5+ years of experience in design systems and component libraries. Request a consultation to discuss technical details and timelines.







