How to Build a React Component Library That Sells in 2026
Why Component Libraries Sell
React component libraries are one of the highest-converting products on any code marketplace. Every developer needs them, they're reusable across projects, and a well-built library saves hundreds of hours.
The market is huge: there are millions of React developers who would rather buy a polished component library than build one from scratch. If you know how to build components properly, you can turn that skill into recurring passive income.
This guide walks you through building a sellable component library — from architecture to packaging to listing it on CodeCudos.
What Makes a Component Library Sellable
Before writing a single line of code, understand what buyers actually want:
| Buyers Want | Builders Often Skip |
|---|---|
| TypeScript types for every prop | Untyped or partial types |
| Accessible (ARIA, keyboard nav) | Accessibility as an afterthought |
| Themeable (CSS vars or props) | Hardcoded colors and sizes |
| Tree-shakeable (import only what you use) | Monolithic bundle |
| Storybook docs with live examples | No documentation |
| Dark mode support | Light mode only |
| Zero peer dependency conflicts | Pinned versions that break things |
| Working demo/preview site | No preview |
The gap between "component library I built for myself" and "component library someone pays for" is almost entirely documentation, theming, and types.
Project Structure
Start with a clean, professional structure:
my-ui-kit/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ ├── Input/
│ │ ├── Modal/
│ │ ├── Card/
│ │ └── index.ts ← barrel exports
│ ├── hooks/
│ │ ├── useTheme.ts
│ │ └── useMediaQuery.ts
│ ├── tokens/
│ │ └── tokens.css ← CSS custom properties
│ └── index.ts ← main entry point
├── .storybook/
├── dist/ ← built output (gitignored)
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.mdEach component in its own folder keeps things clean and makes tree-shaking easy.
Step 1: Initialize the Project
mkdir my-ui-kit && cd my-ui-kit
npm init -y
git initInstall core dependencies:
# Peer deps (don't bundle these)
npm install --save-peer react react-dom
# Dev deps
npm install -D typescript @types/react @types/react-dom
npm install -D vite @vitejs/plugin-react vite-plugin-dts
npm install -D @storybook/react @storybook/addon-essentials storybook
npm install -D vitest @testing-library/react @testing-library/jest-dom
npm install -D eslint prettierStep 2: TypeScript Configuration
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"declaration": true,
"declarationDir": "dist/types",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.test.tsx"]
}Step 3: Build a Proper Component
Here's what a production-quality, sellable Button component looks like:
// src/components/Button/Button.tsx
import { forwardRef, ButtonHTMLAttributes } from "react";
import styles from "./Button.module.css";
export type ButtonVariant = "primary" | "secondary" | "ghost" | "destructive";
export type ButtonSize = "sm" | "md" | "lg";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
fullWidth?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "primary",
size = "md",
loading = false,
leftIcon,
rightIcon,
fullWidth = false,
children,
disabled,
className,
...props
},
ref
) => {
return (
<button
ref={ref}
disabled={disabled || loading}
aria-busy={loading}
data-variant={variant}
data-size={size}
data-full-width={fullWidth}
className={[styles.button, className].filter(Boolean).join(" ")}
{...props}
>
{loading ? (
<span className={styles.spinner} aria-hidden="true" />
) : (
leftIcon && <span className={styles.icon}>{leftIcon}</span>
)}
<span>{children}</span>
{!loading && rightIcon && (
<span className={styles.icon}>{rightIcon}</span>
)}
</button>
);
}
);
Button.displayName = "Button";Key patterns buyers pay for:
forwardRef — works with form libraries and focus managementButtonHTMLAttributes — no prop gapsaria-busy on loading state — accessible by defaultdata-* attributes for CSS styling — avoids className conflictsdisplayName — shows correctly in React DevToolsStep 4: CSS Custom Properties for Theming
The single biggest differentiator for sellable components is easy theming. Use CSS custom properties:
/* src/tokens/tokens.css */
:root {
/* Brand */
--color-primary: #6366f1;
--color-primary-hover: #4f46e5;
--color-primary-foreground: #ffffff;
/* Neutrals */
--color-background: #ffffff;
--color-foreground: #0f172a;
--color-muted: #f1f5f9;
--color-muted-foreground: #64748b;
--color-border: #e2e8f0;
/* Radii */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Typography */
--font-sans: ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, monospace;
}
/* Dark mode */
[data-theme="dark"] {
--color-background: #0f172a;
--color-foreground: #f8fafc;
--color-muted: #1e293b;
--color-border: #334155;
}Buyers override any token with a single CSS rule. This is the exact pattern used by Radix UI, shadcn/ui, and every top-selling component kit.
Step 5: Storybook Documentation
No Storybook = 50% fewer sales. Buyers want to see components before purchasing:
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
title: "Components/Button",
component: Button,
parameters: {
layout: "centered",
docs: {
description: {
component:
"Accessible button component with variants, sizes, loading state, and icon support.",
},
},
},
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "ghost", "destructive"],
},
size: { control: "select", options: ["sm", "md", "lg"] },
loading: { control: "boolean" },
fullWidth: { control: "boolean" },
disabled: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { children: "Get Started", variant: "primary" },
};
export const AllVariants: Story = {
render: () => (
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
</div>
),
};
export const LoadingState: Story = {
args: { children: "Saving...", loading: true },
};Step 6: Build Configuration
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import dts from "vite-plugin-dts";
import { resolve } from "path";
export default defineConfig({
plugins: [react(), dts({ insertTypesEntry: true })],
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "MyUIKit",
formats: ["es", "cjs"],
fileName: (format) => `my-ui-kit.${format}.js`,
},
rollupOptions: {
external: ["react", "react-dom"],
output: {
globals: { react: "React", "react-dom": "ReactDOM" },
preserveModules: true,
preserveModulesRoot: "src",
},
},
sourcemap: true,
},
});The critical setting is preserveModules: true. Without it, buyers import your entire library even when they only use one component. With it, they get proper tree-shaking and smaller bundles.
Step 7: Package.json for Distribution
{
"name": "@yourname/my-ui-kit",
"version": "1.0.0",
"description": "Production-ready React component library with TypeScript",
"main": "./dist/my-ui-kit.cjs.js",
"module": "./dist/my-ui-kit.es.js",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/my-ui-kit.es.js",
"require": "./dist/my-ui-kit.cjs.js",
"types": "./dist/types/index.d.ts"
},
"./styles": "./dist/styles.css"
},
"files": ["dist"],
"sideEffects": ["**/*.css"],
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
},
"scripts": {
"build": "vite build",
"dev": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test": "vitest run",
"typecheck": "tsc --noEmit"
}
}What Components to Include
A complete sellable library should include at minimum:
| Category | Components |
|---|---|
| **Inputs** | Button, Input, Textarea, Select, Checkbox, Radio, Toggle, Slider |
| **Layout** | Card, Divider, Grid, Stack, Container |
| **Feedback** | Alert, Badge, Spinner, Progress, Skeleton, Toast |
| **Overlay** | Modal, Dialog, Drawer, Tooltip, Popover, Dropdown |
| **Navigation** | Tabs, Breadcrumb, Pagination, Sidebar |
| **Data** | Table, Avatar, Tag/Chip |
A library with 30–40 well-built, documented components sells at $49–$149. Niche libraries targeting dashboards or form builders command $99–$299.
Pricing Your Library
| What You Include | Suggested Price |
|---|---|
| 10–20 basic components, no Storybook | $19–$29 |
| 20–40 components + Storybook docs | $39–$79 |
| 40+ components + Figma file + themes | $79–$149 |
| Complete design system with tokens | $149–$299 |
| Full dashboard template using the library | $199–$499 |
Bundling a demo app that uses your components significantly increases conversion — buyers see exactly what they're getting before purchasing.
Listing on CodeCudos
When listing your library on CodeCudos:
dist/)The quality score algorithm rewards TypeScript coverage, test presence, documentation completeness, and a clean package.json. Higher quality scores = higher placement in search results = more sales.
The Compounding Advantage
The real value of building a component library to sell: you use it yourself. Every project you build goes faster. Every client project gets a head start. And every sale compounds — the library exists once, sells forever.
Start with 10 components done right. Ship. Iterate based on buyer feedback. A library that starts at $29 with 10 components can grow to $99 with 40+ components and a proven track record.
Browse existing component libraries on CodeCudos to understand what's already selling and where the gaps are.