Open-Sourcing Your Component Library: Docs, Versioning, DX
Open-sourcing a React component library means more than pushing to GitHub. Here's how to handle docs, semver, and DX so contributors don't rage-quit.
Why Open-Sourcing a Component Library Is Harder Than It Looks
Honestly, most component libraries die not because the components are bad — they die because nobody can figure out how to use them. You push the repo, write a one-paragraph README, and then wonder why the issues tab is full of "how do I install this" questions three weeks later.
The gap between an internal design system and a public open-source library is enormous. Internally, you can Slack someone. Externally, your documentation IS the product. People will judge the quality of your components entirely by how easy it is to get the first one rendering on their screen.
This guide walks through the pieces that actually matter: docs structure, semver discipline, changelog hygiene, and the small DX choices that make contributors want to come back. We'll reference specific tooling versions and real config values throughout — not vague advice.
Structuring Your Documentation for Real Developers
Don't write docs the way you'd write a landing page. Developers want to copy code, not read paragraphs. Structure each component page the same way: installation snippet, a minimal working example, a props table, and then a section for advanced usage or gotchas.
The single thing that kills docs fastest is staleness. If you document default prop values, they drift. Instead, generate the props table automatically from TypeScript types using something like react-docgen-typescript or typedoc. Wire it into your build step so it can't diverge. Combine this with a Storybook setup where each story is also a live code example — your docs and your tests stay in sync without manual effort.
Versioned docs matter more than you think. When someone is on v2.4.0 and you're publishing docs for v3.0.0, they're going to be confused and annoyed. Docusaurus handles this well with its versions.json approach. Set it up from day one, not after you've shipped three breaking changes.
Semver Without the Chaos: A Practical Versioning Strategy
Semantic versioning sounds simple until you're actually doing it. What counts as a breaking change in a component library? Renaming a prop? Changing a default value? Removing a variant? The answer is: anything that would require a consumer to change their code is a breaking change, and it belongs in a major version bump.
Set up Conventional Commits from the start. The commit format — feat:, fix:, chore:, BREAKING CHANGE: — feeds directly into automated changelog generation and version bumping via tools like semantic-release or changesets. The changesets approach is particularly good for monorepos where you might ship @empire-ui/core@2.1.0 and @empire-ui/icons@1.3.0 independently.
Here's a minimal .changeset workflow snippet that works well:
Setting Up Changesets for Multi-Package Versioning
If your library lives in a monorepo — which it probably should, so you can split core, icons, and utils into separate npm packages — changesets is the cleanest versioning tool available right now.
# Install once at the root
npm install --save-dev @changesets/cli
npx changeset init
# When you make a change that warrants a release:
npx changeset
# Interactive prompt: pick packages, bump type (patch/minor/major), write summary
# On CI, to publish:
npx changeset version # bumps versions + updates CHANGELOG.md
npx changeset publish # runs npm publish for changed packagesThe key config lives in .changeset/config.json. Set "access": "public" if you're publishing scoped packages, and set "baseBranch": "main". Don't overthink it — the defaults are sane for most library projects. What matters is discipline: every PR that changes user-facing behavior ships with a changeset file, enforced by a CI check.
Developer Experience: Peer Deps, Exports, and Tree-Shaking
The most common DX failure in open-source component libraries is peer dependency hell. If you bundle React inside your library instead of listing it as a peer dep, consumers end up with two React instances and a wall of cryptic hook errors. Always declare React and ReactDOM as peerDependencies, not dependencies. Same for Tailwind — you don't ship Tailwind, you ship class names that assume the consumer has Tailwind configured.
Export maps in package.json are non-negotiable now. They let you expose multiple entry points cleanly, enable proper tree-shaking, and prevent consumers from reaching into your internals. A well-structured export map looks like this:
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./button": {
"import": "./dist/button/index.mjs",
"require": "./dist/button/index.cjs",
"types": "./dist/button/index.d.ts"
}
}
}This matters for bundle size. A consumer who only needs your Button shouldn't pull in your Modal, your DataTable, and your 40 animation variants. When we built Empire UI's spacing utilities, we followed a CSS spacing system approach that keeps utilities independently importable — each token file stays under 2kb gzipped. That only works if your export map is set up correctly.
Writing a CHANGELOG That People Actually Read
Nobody reads a CHANGELOG that just says "bug fixes and improvements." Write for the developer who is about to upgrade and needs to know in 30 seconds whether anything will break for them.
Group entries under ### Breaking Changes, ### New Features, and ### Bug Fixes. Under Breaking Changes, always include a migration snippet — one before, one after. If you removed the variant="ghost" prop and replaced it with appearance="ghost", show that exact change. Don't make people grep through git blame to figure out what moved.
Automated changelog generation via changesets gives you a decent base, but you'll still want to edit the output before publishing. The auto-generated summaries tend to be terse. Spend five minutes expanding them. Your users will feel that effort. And honestly, a well-maintained CHANGELOG is one of the strongest signals that a library is worth depending on — it tells people you care about backward compatibility and communication.
Theming and Token Contracts: Don't Break Your Consumers' Themes
What happens when you rename a CSS custom property from --color-primary to --color-brand-primary in v2.0.0? Every consumer who overrode that token in their theme just silently broke. Their override stopped doing anything. No error, no warning — just a component that stopped honoring their brand color.
CSS custom properties are a public API. Treat them with the same semver rigor as prop names. Document every exposed token. A good color system defines which tokens are stable and which are considered internal. Prefix internal tokens with --_ (double dash underscore) as a convention — consumers know not to reach for those. Public tokens like --button-bg, --button-radius, and --button-gap: 8px stay stable across minor versions.
For libraries targeting Tailwind users specifically, consider shipping a preset. A tailwind.config.js preset that consumers spread into their own config gives you a documented extension point. When you're on Tailwind v4.0.2 and using the new CSS-first config approach, a preset becomes a single @import line — much cleaner than asking consumers to copy-paste your theme object. Pair this with a theme toggle implementation guide in your docs so consumers understand how dark mode interacts with your token layer.
Contributor Experience: Making It Easy to Send That First PR
How long does it take a new contributor to go from cloning your repo to seeing their change render in the browser? If the answer is more than ten minutes, you're losing contributors. Time that flow yourself. Write it down as a numbered list in your CONTRIBUTING.md.
A few things that genuinely move the needle: a .devcontainer config so contributors don't fight Node version mismatches, a pnpm dev command that starts both Storybook and type-checking in watch mode simultaneously, and a PR template that asks for a changeset file and a screenshot for visual changes. Small friction reduction compounds fast when you have dozens of contributors.
Also: link to your accessibility guide from the CONTRIBUTING.md. If you care about WCAG compliance, make it part of the contribution checklist rather than a post-merge review note. Contributors who know your standards upfront write better PRs. And a contributor who ships an accessible component their first time around is way more likely to come back and ship another one.
FAQ
Changesets is generally the better fit for component libraries, especially monorepos. It gives individual developers the ability to describe their own changes in plain language, and it handles per-package versioning cleanly. semantic-release is more opinionated and works better for single-package projects with strict Conventional Commits discipline on every commit.
You can't, really. Changing a prop type from string to string | number, narrowing a union, or removing an optional prop are all breaking changes for TypeScript consumers. Mark them as major version bumps. If you find yourself doing this frequently, it's a sign your prop API needs more design time upfront before going public.
Yes, unfortunately. Next.js, Remix, and most modern bundlers handle ESM fine, but there are still plenty of projects on older setups — Jest without experimental ESM, CRA-derived configs, etc. — that need CJS. Tools like tsup or unbuild make dual-format output nearly zero config. Ship both and use export maps to let the consumer's bundler pick the right one.
Don't bundle Tailwind or run it during your build. Instead, ship your components with class names applied and tell consumers to add your library's source to their Tailwind content paths so the classes get included in their build output. With Tailwind v4.0.2's CSS-first config, you can ship a preset as a plain CSS file that consumers import — this is cleaner than the old tailwind.config.js preset approach.
A good starting point is one package for core components, one for icons, and one for utility functions or hooks. Don't split at the individual component level — that creates too much versioning overhead and confuses consumers. If your icon package is 400kb and someone only needs components, they shouldn't have to wonder why their bundle ballooned.
README: installation, a one-component getting started example, a link to the full docs, badges for npm version and license. That's it. Everything else goes in the docs site. The README is a front door, not a reference manual. Developers scanning npm or GitHub need to know within 20 seconds whether this library is worth clicking into.