back

Publishing a React Component Library to NPM — The Complete Workflow

August 20, 2025·8 min read·Antima Singh

Building Vividora UI taught me that the hard part of a component library is not writing components — it is the tooling around them. Bundling, TypeScript declarations, peer dependency management, documentation, and the release pipeline all require deliberate decisions.

Start with Radix UI primitives

Accessibility is non-negotiable. Instead of building modals, dropdowns, and tooltips from scratch, I built on top of Radix UI's unstyled, accessible primitives. Radix handles focus management, keyboard navigation, and ARIA attributes. I handled styling with Tailwind CSS.

The bundler choice matters

I evaluated Rollup, tsup, and Vite library mode. I settled on `tsup` — it handles CommonJS and ESM outputs simultaneously, generates TypeScript declarations, and has near-zero configuration for a component library.

{
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts --clean"
  }
}

Keep the public API explicit

I export every component from a single `src/index.ts` barrel file. Nothing else is exported. This gives consumers a clean, stable API and lets me refactor internals freely.

Peer dependencies, not dependencies

React and Tailwind should be listed as `peerDependencies`. They should not be bundled — consumers bring their own versions. Bundling them leads to duplicate React instances in the consumer's app, which causes hooks to break in confusing ways.

The documentation is the product

I wrote usage examples and prop tables for every component. This felt tedious while doing it. It was the single highest-leverage thing I did for adoption. If people can not figure out how to use a component in five minutes, they will not use it.

Automated releases with semantic versioning

I use conventional commits and a simple GitHub Action that bumps the version, generates a changelog, and publishes to NPM on every merge to main. This removes the mental overhead of remembering to publish.