UI Component Library Development for Web Application
Component library is a set of UI components with unified style, behavior and API used by several teams or projects. Difference from component set in single repo: library is separate package installed as dependency (npm install @company/ui), has versioning and Changelog.
Decision to create library should be deliberate: it's serious infrastructure investment. Justified if you have multiple frontend apps, several teams, or recurring problem "in every project Button looks different".
Library Architecture
Monorepo vs Separate Repository
Monorepo (Turborepo, Nx) convenient if library and apps develop in parallel by one team. Component change immediately visible in all apps without package publishing.
Separate repository with npm publishing (or private Registry: GitHub Packages, Verdaccio) — better with several independent teams. Consuming team chooses version, 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
Public API — critical to control what exports from index.ts. Anything exported is public API with backward compatibility obligation.
Build and Bundling
For component library, bundler is not Vite (for apps), but specialized tools:
tsup — simplest. Single command, supports ESM + CJS simultaneously, TypeScript out of box:
{
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
Rollup — more control, but complex configuration. Used for complex cases (tree-shaking per-component, multiple entry points).
Vite Library Mode — if already using Vite. build.lib config, formats es + cjs.
Important: CSS not bundled inside JS bundle. If using Tailwind — consuming project must run Tailwind with paths to library components in content. If CSS-in-JS (styled-components, Emotion) — styles ship with JS. If CSS Modules — separate CSS build needed.
Token System
Design tokens — foundation of consistency. In code these are CSS Custom Properties:
/* 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 can be generated from Figma Variables via Tokens Studio plugin or CLI Style Dictionary (Amazon). Style Dictionary takes JSON with tokens and generates CSS, JS constants, iOS Swift, Android XML simultaneously.
Component API Design
Good component API is predictable and minimal. Principles:
Controlled vs Uncontrolled. Input can be controlled (managed externally via value + onChange) and uncontrolled (manages own state via defaultValue). Library components should support both.
Polymorphic components. Button should render <button> by default, but with as="a" — <a>. In TypeScript implemented via generic:
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 props — <Button.Icon position="left"><SearchIcon /></Button.Icon>. More flexible but complex API. Choice depends on component complexity.
Radix UI Primitives — recommend as base for complex components (Select, Dialog, Tooltip, Dropdown). They handle accessibility, keyboard navigation, ARIA attributes — your task only styling. shadcn/ui — example of wrapping Radix in Tailwind styles.
Testing
Three testing levels for components:
Unit tests — Vitest + Testing Library. Check logic, states, accessibility:
test('Button renders disabled state', () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
Visual regression — Chromatic (commercial, integrates with Storybook) or Playwright screenshots. Each PR checks components visual appearance unchanged.
Accessibility — axe-core via jest-axe or Storybook addon a11y. Automatically finds obvious ARIA violations.
Versioning and Breaking Changes
Semantic versioning (semver): MAJOR.MINOR.PATCH.
- PATCH: bugfix without API change
- MINOR: new component or new optional prop
- MAJOR: component removal, prop renaming, behavior change
Automation tools: Changesets (Atlassian) — developer adds .changeset/*.md file describing changes, CI automatically updates version and publishes to npm.
Documentation
Storybook — standard for UI libraries. Additionally:
- README.md for each component with usage examples
- Migration guides for MAJOR versions
- Changelog — automatically from Changesets
Timeline
| Stage | Time |
|---|---|
| Architecture design, toolchain choice | 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 and CI: 6–10 weeks. Full-featured enterprise-grade library with 40+ components, visual regression, A11y tests — from 4 to 6 months iterative development.







