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 / ConceptOne‑liner purpose
Vite 6 Library ModeFast TS/JS bundler for shipping ESM + CJS builds. cmdcolin.github.io
Storybook 9Docs, a11y, visual and interaction tests in one UI. Storybook
TypeScript 5Type‑safe props; emits .d.ts for consumers.
Playroom / Storybook DocsLive playground for designers without cloning repo.
ChangesetsAutomated semver & changelog from PR labels.
CLI CommandWhat it does
npm create vite@latest my-lib -- --template react-tsScaffolds library workspace.
npm i -D vite tsup@7 storybook@nextAdds build & docs tooling.
npx sb init --builder @storybook/builder-viteSets Storybook to Vite pipeline.
npx changeset initBootstraps versioning workflow.

Concept Foundations — Plain English


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

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

PitfallSymptomFix
Name collisionsImporting library crashes CRA due to duplicated ReactAdd peerDependencies: { react, 'react-dom' }
CSS leakStyles override host appScope everything under .rk- prefix or use CSS Modules
Tree‑shaking failsBundle includes unused componentsEnsure sideEffects: false in package.json
Storybook visual driftStories pass, prod UI brokenIntegrate 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


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

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.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x