Boilerplate Déjà Vu in a Bogotá Café
Last spring I was pair‑programming with Ana in Barcelona. We were on our third React landing page in as many weeks, copy‑pasting the same Button, Input, and Modal into yet another repo while battling low Wi‑Fi in a Bogotá café. A typo in one copy broke keyboard focus, sneaking past code review and shipping to production. That bug convinced us to extract a component library—one canonical source of truth, versioned, documented, and installable with a single npm i
. Six months later, the Button has 18 variants and zero regressions. Let’s walk through how to build that kind of library from scratch.
Why Component Libraries Matter in 2025
Design systems are no longer nice‑to‑have; product teams iterate weekly, designers prototype in Figma tokens, and accessibility gates every release. Storybook 9’s built‑in a11y and visual test panels make UI regression obvious Storybook, and Vite’s library mode bundles components in milliseconds DEV Community. A well‑structured library turns scattered UI snippets into versioned packages, freeing juniors to compose screens instead of cloning code.
Toolbelt at a Glance
Tool / Concept | One‑liner purpose |
---|---|
Vite 6 Library Mode | Fast TS/JS bundler for shipping ESM + CJS builds. cmdcolin.github.io |
Storybook 9 | Docs, a11y, visual and interaction tests in one UI. Storybook |
TypeScript 5 | Type‑safe props; emits .d.ts for consumers. |
Playroom / Storybook Docs | Live playground for designers without cloning repo. |
Changesets | Automated semver & changelog from PR labels. |
CLI Command | What it does |
---|---|
npm create vite@latest my-lib -- --template react-ts | Scaffolds library workspace. |
npm i -D vite tsup@7 storybook@next | Adds build & docs tooling. |
npx sb init --builder @storybook/builder-vite | Sets Storybook to Vite pipeline. |
npx changeset init | Bootstraps versioning workflow. |
Concept Foundations — Plain English
- Package Boundary: Your library ships as an npm package; consumers import compiled code, not source.
- Atomic Components: Start with leaf nodes—Button, TextField—then compose higher‑order pieces.
- Theming Layer: Separate styling tokens (colors, spacing) from logic; consumers override via CSS vars or a ThemeProvider.
- Semver Discipline: Increment major on breaking prop changes; minors for additive props; patch for bug fixes.
Step‑by‑Step Walkthrough
1. Scaffold & Build Configuration
bashCopyEditnpm create vite@latest react-kit -- --template react-ts
cd react-kit
npm i -D vite tsup typescript
jsCopyEdit// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
sourcemap: true,
clean: true,
});
Line‑by‑line
entry
points to a barrel file exporting components.- Dual formats ensure Node & bundlers resolve correctly.
.d.ts
lets IDEs surface prop types.
Add an npm script:
jsonCopyEdit"build": "tsup"
Run npm run build
—a dist/
folder appears with tiny bundles.
2. Create Your First Component
tsxCopyEdit// src/Button/Button.tsx
import React from 'react';
import clsx from 'clsx';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'ghost';
}
export const Button = ({ variant = 'primary', className, ...rest }: ButtonProps) => (
<button
className={clsx(
'rounded px-4 py-2 font-medium',
variant === 'primary' && 'bg-blue-600 text-white',
variant === 'ghost' && 'bg-transparent hover:bg-slate-100',
className,
)}
{...rest}
/>
);
Update the barrel:
tsCopyEditexport { Button } from './Button/Button';
export type { ButtonProps } from './Button/Button';
3. Storybook Docs & Tests
bashCopyEditnpx sb init --builder @storybook/builder-vite
Storybook 9 loads instantly; create a story:
tsxCopyEdit// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
export default { title: 'Atoms/Button', component: Button } as Meta<typeof Button>;
export const Primary: StoryObj = { args: { children: 'Save', variant: 'primary' } };
export const Ghost: StoryObj = { args: { children: 'Cancel', variant: 'ghost' } };
Storybook’s a11y panel now runs realtime checks Storybook—fix contrast issues before merging.
4. Publish & Version with Changesets
bashCopyEditnpx changeset
# prompts for summary; choose minor/patch/major
git add .changeset && git commit -m "docs(button): add ghost variant"
git push & open PR
A GitHub Action reads changesets on merge, bumps package.json, and pushes a tag. Consumers install @scope/react-kit@1.1.0
the next minute.
5. Theming & Token Strategy
Create a light theme file:
cssCopyEdit/* src/tokens/light.css */
:root {
--color-primary: #2563eb;
--color-primary-contrast: #ffffff;
--radius: 0.5rem;
}
Refactor Button:
tsxCopyEditconst styleMap = {
primary: 'bg-[var(--color-primary)] text-[var(--color-primary-contrast)]',
ghost: 'bg-transparent hover:bg-slate-100',
};
Consumers override tokens with a higher‑priority stylesheet—no recompilation required.
Common Pitfalls & Fixes
Pitfall | Symptom | Fix |
---|---|---|
Name collisions | Importing library crashes CRA due to duplicated React | Add peerDependencies: { react, 'react-dom' } |
CSS leak | Styles override host app | Scope everything under .rk- prefix or use CSS Modules |
Tree‑shaking fails | Bundle includes unused components | Ensure sideEffects: false in package.json |
Storybook visual drift | Stories pass, prod UI broken | Integrate Storybook’s visual test addon for diff snapshots Storybook |
Remote‑Work Insight Box
Our library lives in a monorepo with apps. When a developer in Panama proposes a prop change at 10 p.m. my time, GitHub Actions builds the storybook, runs visual diffs, and posts a Chromatic link. I can review on my phone while boarding a bus in Medellín—zero local checkout needed.
Performance & Accessibility Checkpoints
- Bundle Weight: Target < 35 kB gzip per app install. Verify with
npm run size
. - Only ESM? Publish dual builds until all consumers shift to native ESM.
- RTL & LTR: Include logical CSS props (
margin-inline-start
) or an RTL flip plugin. - Keyboard Support: Storybook’s a11y tests flag missing
aria-pressed
etc.—treat red badges as merge blockers. - Lighthouse: Mount the library inside a demo page; ensure 100 / 100 a11y score before tagging a release.
Optional Diagram Description
SVG: Monorepo root →
packages/react-kit
(library) →apps/web
,apps/mobile-web
. Arrows show React Kit flowing via npm workspace links into each app; CI pipeline validates library, then integration tests downstream.
Wrap‑Up — Key Takeaways
- React component libraries centralize UI, enforce design systems, and speed up new screens.
- Use Vite library mode + tsup for fast dual‑format builds.
- Document with Storybook 9—its a11y and visual tests prevent expensive regressions.
- Manage versions with Changesets; automate release tags and changelogs.
- Design token‑driven theming to keep branding flexible without recompiling.
Ready to sunset copy‑paste components? Clone the scaffold, publish @your-scope/ui
, and let me know how it goes—I’ll be coding from the next tropical co‑working spot.