Every frontend team eventually reaches the same inflection point. You started with shadcn/ui because it was fast, flexible, and gave you beautiful components out of the box. You copied them into your project, tweaked the colors, adjusted the border radius, and shipped your first feature. Then a second project started. And a third. Suddenly you have three codebases with three slightly different versions of your Button component. The design team is frustrated because the spacing on one app doesn't match the others. A new developer joins and asks "which Button component is the canonical one?" and nobody knows the answer. This is the moment you realize you need your own component library.
Building a production component library is one of the highest-leverage investments a frontend team can make. It standardizes your design language, accelerates development across projects, and ensures consistency in quality, accessibility, and user experience. But the gap between "I have some shadcn components in my project" and "I have a versioned, documented, tested component library that my whole organization uses" is enormous. This guide is about bridging that gap. At Spectrum UI, we have gone through this journey ourselves, and we are going to share everything we learned along the way.
This is not a quick tutorial. This is a comprehensive, 3000+ word guide covering every layer of the stack: monorepo setup, build tooling, component extension patterns, design tokens, testing, documentation, versioning, and distribution. Whether you are a solo developer building a component library for your side projects or a tech lead standardizing UI across a 50-person engineering team, this guide will give you a battle-tested roadmap to follow.
The Component Library Architecture
Before we write a single line of code, let's understand the architecture. A production component library is not just a folder of React components. It is a layered system where each layer builds on the one below it. At the foundation, you have headless UI primitives from Radix UI. On top of that, shadcn/ui adds default styling and composition patterns. Your library adds branding, custom variants, compound components, and domain-specific logic. Around all of this, you have design tokens for theming, a test suite for reliability, and documentation for adoption.
Component Library Architecture
══════════════════════════════════════════════════════════════════════
┌──────────────────────────────────────────────────────────────────┐
│ YOUR APPLICATIONS │
│ app-1 · app-2 · app-3 · marketing-site │
└──────────────────────────┬───────────────────────────────────────┘
│ imports
┌──────────────────────────▼───────────────────────────────────────┐
│ YOUR COMPONENT LIBRARY │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐ │
│ │ Branded │ │ Compound │ │ Custom │ │ Domain │ │
│ │ Variants │ │ Components│ │ Hooks │ │ Specific │ │
│ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ └─────┬─────┘ │
│ └───────────────┼───────────────┼───────────────┘ │
└──────────────────────────┬───────────────────────────────────────┘
│ extends
┌──────────────────────────▼───────────────────────────────────────┐
│ shadcn/ui COMPONENTS │
│ │
│ Button · Dialog · Dropdown · Input · Card · Table · ... │
│ styled with Tailwind CSS + cva │
└──────────────────────────┬───────────────────────────────────────┘
│ styles
┌──────────────────────────▼───────────────────────────────────────┐
│ RADIX UI PRIMITIVES │
│ │
│ Accessible · Keyboard Navigation · Focus Management · ARIA │
└──────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐
│ Design Tokens│ │ Test Suite │
│ colors │ │ unit tests │
│ spacing │ │ visual reg. │
│ typography │ │ a11y tests │
└──────────────┘ └──────────────┘
▲ ▲
└───── consumed by ──┘Planning Your Library
The biggest mistake teams make is jumping straight into code. Before you create a single file, you need to answer some fundamental questions. What is the scope of your library? Is it a thin branded wrapper around shadcn/ui, or does it include domain-specific components like data tables, charts, and form builders? Who are the consumers? Just your team, or multiple teams across an organization? How will it be distributed? As an npm package, a monorepo internal package, or a shadcn-style copy-paste registry?
Start with a component inventory. Audit your existing applications and list every component that is used in more than one place. Group them into tiers. Tier 1 is your primitives: Button, Input, Label, Badge, Card. These are the components that every application needs. Tier 2 is your composites: Dialog, Dropdown Menu, Data Table, Form. These combine primitives into more complex interactions. Tier 3 is your domain-specific components: PricingCard, UserAvatar, NotificationBell. These encode business logic and are specific to your product.
Define your API conventions early. At Spectrum UI, we follow a strict set of rules: every component acceptsclassName for style overrides, every interactive component supports both controlled and uncontrolled modes, every component forwards refs, and every variant is defined using cva. These conventions seem small, but they compound across dozens of components. A developer who learns the pattern from one component can apply it to every other component without checking the docs.
Monorepo Structure
A monorepo is the best way to develop, test, and publish a component library alongside the applications that consume it. It gives you instant feedback: change a component and see the effect in your apps immediately. No version bumps, no publishing, no waiting. Turborepo is the recommended tool for this. It handles caching, task orchestration, and parallel execution out of the box.
Monorepo Structure ══════════════════ my-design-system/ ├── apps/ │ ├── docs/ # Storybook or custom docs site │ │ ├── stories/ │ │ └── package.json │ ├── playground/ # Live testing environment │ │ └── package.json │ └── web/ # Your main application │ └── package.json ├── packages/ │ ├── ui/ # Core component library │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── button/ │ │ │ │ │ ├── button.tsx │ │ │ │ │ ├── button.test.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── dialog/ │ │ │ │ ├── input/ │ │ │ │ └── index.ts │ │ │ ├── hooks/ │ │ │ └── utils/ │ │ ├── tsconfig.json │ │ └── package.json │ ├── tokens/ # Design tokens package │ │ ├── src/ │ │ │ ├── colors.ts │ │ │ ├── spacing.ts │ │ │ └── typography.ts │ │ └── package.json │ └── tsconfig/ # Shared TypeScript config │ ├── base.json │ ├── react-library.json │ └── nextjs.json ├── turbo.json ├── package.json └── pnpm-workspace.yaml
Turborepo Configuration
Turborepo needs to understand your task dependency graph. When you build your docs app, it should first build the tokens package, then the ui package, then the docs app. When you run tests, it should run them in parallel across all packages. Here is a minimal turbo.json configuration that handles this correctly.
Package Configuration
Each package in your monorepo needs a properly configured package.json. The UI package is the most important one. It needs to export both ESM and CJS formats, include TypeScript declarations, and specify its peer dependencies correctly. Getting this right is critical for downstream consumers.
Build Pipeline with tsup
tsup is the recommended bundler for component libraries. It is built on esbuild, which makes it extremely fast. It handles TypeScript compilation, declaration generation, tree-shaking, and multiple output formats in a single configuration file. Unlike Rollup or Webpack, tsup requires almost zero configuration for the common case of bundling a React component library.
The "use client" banner is critical if your consumers use Next.js App Router. Without it, every component that uses React hooks or browser APIs will throw a server component error. By adding the directive at the bundle level, you ensure all components are treated as client components automatically. This saves your consumers from having to wrap every import in their own "use client" boundary.
Extending shadcn Components
Now for the core question: how do you build on top of shadcn/ui? There are two strategies, wrapping and forking, and choosing the right one for each component is critical. Wrapping means you import the shadcn component and add functionality around it. Forking means you copy the shadcn source code into your library and modify it directly. Each has trade-offs.
Wrapping is the safer choice for most components. It preserves the ability to update the underlying shadcn component when new versions are released. Your wrapper adds your brand's variants, default props, and any additional functionality. The downside is a thin layer of indirection that can make debugging slightly harder. Forking gives you full control but means you are responsible for maintaining the component going forward, including accessibility updates from Radix.
Adding Custom Variants with cva
The most common extension point is adding new variants. shadcn/ui uses class-variance-authority(cva) for all variant definitions. You can extend these with your own brand-specific variants while preserving the originals.
Compound Component Patterns
For more complex components, the compound component pattern is invaluable. It lets you create components that share implicit state through React Context while giving consumers full control over composition. This is the pattern that shadcn/ui uses for Dialog, Accordion, and other interactive components, and you should extend it for your own compound components.
The compound component pattern gives you the best of both worlds. The library manages complex state internally (step tracking, validation, transitions), but the consumer controls the structure and presentation. They can put step indicators at the top or bottom, add custom content between steps, or even build a completely custom UI using the useStepper hook. This flexibility is what separates a toy component from a production one.
The Build and Publish Pipeline
Getting your component library from source code to a published npm package involves several stages. Each stage has a specific job: compile TypeScript, bundle the output, generate type declarations, run quality checks, and finally publish. Here is the full pipeline visualized.
Build & Publish Pipeline
════════════════════════
Source Code (.tsx)
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ TypeScript │ │ ESLint │ │ Vitest │
│ Compiler │ │ Linter │ │ Tests │
│ (tsc) │ │ │ │ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Type Check │ │ Lint Pass │ │ Test Pass │
│ ✓ or ✗ │ │ ✓ or ✗ │ │ ✓ or ✗ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
│
All checks pass?
┌──────┴──────┐
│ YES │ NO ──▶ Fix & Retry
└──────┬──────┘
▼
┌──────────────────┐
│ tsup Build │
│ │
│ ├── index.js │
│ ├── index.mjs │
│ ├── index.d.ts │
│ └── globals.css │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Changesets │
│ Version Bump │
│ + Changelog │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ npm publish │
│ or │
│ GitHub Packages│
└──────────────────┘Design Token Integration
Design tokens are the atomic values that define your visual language: colors, spacing, typography, shadows, border radii. They are the bridge between your design team's Figma file and your code. Instead of hardcoding bg-blue-600 everywhere, you use semantic tokens likebg-primary that can be remapped to any color depending on the theme or brand.
shadcn/ui already uses CSS custom properties for theming, and you should build on top of that pattern. The key insight is to organize your tokens into three layers: primitive tokens (raw values like specific hex colors), semantic tokens (purpose-based names like "primary" or "destructive"), and component tokens (component-specific overrides). This three-layer architecture lets you swap entire themes by changing just the semantic layer.
Multi-Brand Support
One of the most powerful capabilities of a token-based architecture is multi-brand support. If your organization has multiple products with different visual identities, you can share the same component library and swap themes at the CSS level. The components are identical in structure, behavior, and accessibility. Only the visual tokens change. This is how large design systems like Atlassian's and IBM's Carbon work at scale.
To switch brands, you simply add a data-brand attribute to your root HTML element. No component code changes. No conditional logic. No separate builds. This is the power of a well-designed token architecture. It decouples what your components do from how they look, and it means your QA team only needs to test behavior once because the visual differences are purely cosmetic.
Testing Your Components
A component library without tests is a liability. Every component is used in multiple places across multiple applications. A bug in your Button component is a bug in every application that uses it. Testing is not optional; it is the foundation of trust. If your consumers do not trust that your library is reliable, they will stop using it and build their own components instead.
We recommend a three-layer testing strategy: unit tests for logic and interactions, visual regression tests for appearance, and accessibility tests for compliance. Each layer catches a different class of bugs, and together they provide comprehensive coverage.
Unit Testing with Vitest
Vitest is the recommended test runner for modern React libraries. It is fast, supports JSX out of the box, and has a Jest-compatible API so there is no learning curve. Combined with Testing Library, you can write tests that verify your components from the user's perspective.
Visual Regression Testing
Unit tests verify behavior, but they cannot catch visual regressions. A CSS change that subtly breaks your layout will pass every unit test. This is where visual regression testing comes in. Tools like Chromatic (which integrates with Storybook) take screenshots of every component in every state and compare them against a baseline. Any visual difference is flagged for review. This catches the kind of bugs that slip through code review because the change looked fine in isolation but broke something in a specific state or viewport size.
Accessibility Testing with axe
Accessibility is not something you check manually. It needs to be automated. The axe-corelibrary can be integrated into both your unit tests and your Storybook stories. It catches common accessibility violations like missing labels, insufficient color contrast, and incorrect ARIA roles. We run axe against every component in every variant as part of our CI pipeline.
Documentation
A component library without documentation is a component library that nobody uses. Documentation is not just about listing props. It is about showing developers how to use your components effectively. Good documentation includes live examples, prop tables, usage guidelines, accessibility notes, and migration guides. At Spectrum UI, we treat documentation as a first-class product, not an afterthought.
Storybook for Component Documentation
Storybook is the industry standard for component documentation. It provides an isolated environment where you can develop, test, and showcase your components. Each component gets a page with interactive controls, a prop table, and multiple stories showing different states. The best part is that your Storybook stories double as visual regression test cases when you integrate with Chromatic.
Auto-Generating Prop Tables
Manually maintaining prop documentation is a recipe for stale docs. Instead, use tools that extract prop information directly from your TypeScript types. Storybook does this automatically with itsautodocs feature. For custom documentation sites, react-docgen-typescriptcan parse your component files and generate JSON descriptions of every prop, including its type, default value, and description from JSDoc comments. This ensures your documentation is always in sync with your code.
Versioning and Publishing
Once your library is ready for consumers, you need a reliable versioning and publishing strategy. Semantic versioning (semver) is the standard: major versions for breaking changes, minor versions for new features, and patch versions for bug fixes. The challenge is tracking these changes across multiple packages in a monorepo. This is where Changesets comes in.
Versioning & Release Pipeline
═════════════════════════════
Developer pushes code
│
▼
┌─────────────────┐ ┌─────────────────┐
│ PR includes a │ NO │ CI reminds dev │
│ changeset? │────▶│ to add one │
└────────┬────────┘ └─────────────────┘
│ YES
▼
┌─────────────────┐
│ PR merged to │
│ main branch │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────────────┐
│ Changesets bot │────▶│ Creates "Version PR" │
│ runs in CI │ │ │
└──────────────────┘ │ - Bumps package.json │
│ - Updates CHANGELOG.md │
│ - Groups all pending │
│ changesets │
└────────┬────────────────┘
│ Merged by maintainer
▼
┌─────────────────────────┐
│ CI publishes to npm │
│ │
│ @acme/ui@1.2.0 │
│ @acme/tokens@1.1.0 │
└─────────────────────────┘Changesets is a tool designed specifically for versioning in monorepos. Each PR that modifies a package includes a "changeset" file that describes what changed and whether it is a major, minor, or patch change. When changesets are merged, the Changesets bot creates a "Version Packages" PR that bumps all affected package versions and updates changelogs. Merging that PR triggers the publish step. This workflow ensures that every change is intentionally versioned and documented.
Private Registry Distribution
If your component library is internal to your organization, you probably do not want to publish it to the public npm registry. GitHub Packages and Artifactory are popular choices for private registries. GitHub Packages integrates seamlessly with GitHub Actions, making your CI/CD pipeline straightforward. You configure your package.json with the registry URL, set up authentication via an npm token, and your publish step works identically to a public publish.
Another option is to skip the registry entirely and use internal monorepo references. If all your applications live in the same monorepo as your component library, you can reference the UI package directly with "@acme/ui": "workspace:*". This gives you instant updates without any publishing step. Turborepo handles the build order automatically. The trade-off is that every application must live in the monorepo, which may not be feasible for larger organizations with multiple repositories.
Lessons from Spectrum UI
Building Spectrum UI taught us lessons that no tutorial could have prepared us for. Here are the most important ones, each earned through real-world pain.
Lesson 1: Start With Fewer Components
Our first impulse was to build 50 components before releasing anything. That was a mistake. We should have started with 10 rock-solid components and expanded from there. A small library that is reliable, well-tested, and well-documented is infinitely more valuable than a large library with gaps in quality. Your consumers will tell you which components they need next. Listen to them instead of guessing.
Lesson 2: Do Not Break the API
Nothing erodes trust faster than breaking changes. Once you publish a component API, it is a contract. Renaming a prop from color to variant in a minor release will break every consumer. Use deprecation warnings, provide codemods when possible, and batch breaking changes into major releases. If you must change an API, give consumers a migration path. Ship the new API alongside the old one, mark the old one as deprecated, and remove it in the next major version. This gives teams time to migrate without their builds breaking overnight.
Lesson 3: Invest in Developer Experience
The best component library is the one developers choose to use, not the one they are forced to use. Invest in great TypeScript types so autocomplete just works. Invest in documentation with copy-paste examples. Invest in error messages that tell developers what went wrong and how to fix it. We added development-only warnings that detect common mistakes like passing both value anddefaultValue to the same component. These warnings cost almost nothing to implement but save developers hours of debugging.
Lesson 4: CSS is Harder Than You Think
CSS specificity battles, style isolation, and cascade conflicts are the most common source of bugs in a component library. Tailwind CSS helps enormously because it generates utility classes with predictable specificity. But you still need to be careful. Make sure your component styles do not leak into surrounding elements. Make sure consumer overrides via className actually work. And test your components in the context of a real application, not just in Storybook isolation. A component that looks perfect in isolation can break when composed with other components in a real layout.
Lesson 5: Documentation is Never Done
We thought we could write the docs once and move on. We were wrong. Documentation needs to evolve with every release. New variants need new examples. New patterns need new guides. Bug reports often reveal documentation gaps. We now treat documentation updates as a required part of every PR that changes a component API. If you change the code but not the docs, the PR is not done.
Common Mistakes to Avoid
- Over-abstracting too early. Do not build a generic component system before you understand your specific use cases. Start concrete, extract patterns later.
- Ignoring bundle size. Your component library is a dependency of every application. Tree-shaking is essential. Use
sideEffects: falsein your package.json and ensure your bundler supports it. - Not testing in consumer contexts. A component that works in isolation may break in a real application due to CSS conflicts, context providers, or React version mismatches. Always test in a real app.
- Skipping accessibility. Building on top of Radix gives you a strong foundation, but you still need to test with screen readers and keyboard navigation. Automated tests catch only about 30% of accessibility issues.
- Not versioning design tokens. Tokens change over time. If you do not version them alongside your components, you will have style mismatches between token versions and component versions.
- Building in isolation from your design team. Your component library should be a shared language between design and engineering. Involve designers in API reviews, naming decisions, and token definitions. The best component libraries are built collaboratively.
Conclusion: The Launch Checklist
Building a production component library is a significant undertaking, but the payoff is enormous. You get consistency across applications, faster development velocity, a single source of truth for your design system, and built-in accessibility and quality. Here is a checklist to make sure you are ready to ship.
Production Launch Checklist
- Monorepo is set up with Turborepo and pnpm workspaces
- Build pipeline produces ESM, CJS, and TypeScript declarations
- All components forward refs and spread props
- Every component has unit tests covering core behavior
- Accessibility tests pass for all components and variants
- Visual regression testing is configured in CI
- Design tokens are defined and documented
- Dark mode works correctly for every component
- Storybook is deployed with interactive examples and prop tables
- Changesets is configured for versioning and changelog generation
- CI/CD pipeline builds, tests, and publishes automatically
- Package exports are correctly configured for tree-shaking
- Consumer applications can install and use the library without issues
"use client"directive is added for Next.js App Router compatibility- README includes quick start guide with installation and basic usage
- Migration guide exists for teams moving from raw shadcn/ui components
The journey from shadcn/ui primitives to a production component library is not a weekend project. It requires careful planning, solid engineering, and ongoing investment. But once you have it, you will wonder how you ever shipped products without it. Every new feature starts faster because the building blocks are already there. Every application looks and feels consistent because they share the same components. Every developer is more productive because the patterns are documented and the APIs are intuitive. That is the promise of a well-built design system, and it is absolutely worth the effort to get there.
At Spectrum UI, we continue to evolve our library every day. We add new components based on user feedback, refine our tokens based on design reviews, and improve our documentation based on support questions. A component library is a living product, not a finished artifact. Treat it that way, invest in it consistently, and it will become the most valuable piece of infrastructure your frontend team owns.