The Journal

Design Systems3 min read

Building a Production Component Library: From shadcn Primitives to Your Own Design System

A complete guide to building, packaging, and shipping your own component library on top of shadcn/ui primitives. Learn monorepo setup, versioning, documentation, testing, and distribution strategies.

You built twenty components. They live in a components/ folder, copy-pasted into three apps that have already drifted apart. That’s not a library — it’s a maintenance bill. Turning it into a product is mostly unglamorous plumbing, and that plumbing is exactly what makes people trust it.

Publish, don’t copy

The moment two apps share a button, stop copying files between them. Move the components into one package — call it @acme/ui — with a build step and an exports map. A workspace tool like pnpm or Turborepo keeps that package and its consumers in one repo without publishing to npm on every change.

Now consumers install a version instead of reaching into your source. The question changes from “which copy is correct?” to a single number in a package.json, and the whole class of “works in app A, breaks in app B” bugs quietly disappears.

Version like an API

Every prop you expose is a contract. Renaming the ghost variant to subtle is a breaking change, even when nothing on screen looks different.

Adopt semver and let Changesets track it for you. A major bump with a written changelog is how downstream teams plan an upgrade instead of dreading one.

Docs are the product

Nobody reads your source to learn an API. Each component needs one page: a live example, a props table, and a note on keyboard and screen-reader behavior.

Generate that props table from your types so it can never drift from the real interface. If a component isn’t documented, treat it as unshipped — adoption happens in the docs, not in the code.

The boring 20%

The last fifth is what separates a real library from a nice repo. It’s tedious, and it’s the entire point.

  • Visual regression tests, so a shadow tweak can’t silently break forty screens.
  • An automated accessibility check running in CI on every pull request.
  • A published changelog and a deprecated path for props you retire.
  • One peerDependencies range for React, never a bundled copy.

Ship version 0.1

Don’t model the whole system on day one. Publish one package with three components, a changelog, and a docs page for each.

Then let real usage tell you what’s missing. A small library people install beats a perfect one that never leaves your laptop.

Frequently asked questions

How do you turn a folder of components into a real component library?
Move the components into one versioned package, for example @acme/ui, with a build step and an exports map, managed in a monorepo with pnpm or Turborepo. Consumers then install a version instead of copying files, which eliminates the whole class of works-in-app-A-breaks-in-app-B bugs caused by drifting copies.
How should you version a component library?
Treat every exposed prop as a contract and follow semver, since even renaming a ghost variant to subtle is a breaking change when nothing on screen looks different. Let Changesets track versions and generate a changelog, so downstream teams can plan a major upgrade instead of dreading one.
What separates a production component library from a repo of components?
The unglamorous plumbing: visual regression tests so a shadow tweak can't silently break screens, an automated accessibility check running in CI on every pull request, a published changelog with a deprecated path for retired props, and one peerDependencies range for React rather than a bundled copy. Each component also needs a docs page.