I have built dozens of React applications, and I often found myself copying the same UI buttons and input fields from one project to another.
It was a huge waste of time. I finally decided to pack my reusable UI elements into a standalone library that I could install via NPM in seconds.
In this tutorial, I will show you exactly how I built a professional-grade React component library using Rollup and TypeScript.
I have included full code examples, including a USA-specific Zip Code Validator component, so you can see how this works in a real-world scenario.
Build Your Own Component Library
If you work on multiple React projects, a component library ensures that your branding and UI remain consistent across all of them.
It also makes maintenance much easier because you only need to update the code in one place to see changes across every app.
Set Up the Project Structure
The first thing I do is initialize a new project and install the necessary dependencies for bundling and TypeScript support.
Open your terminal and run the following commands to get started:
mkdir my-us-component-lib
cd my-us-component-lib
npm init -y
npm install react react-dom typescript @types/react @types/react-dom --save-devOnce the basic setup is done, I create a tsconfig.json file in the root directory to handle our TypeScript configurations.
{
"compilerOptions": {
"target": "ES5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"declaration": true,
"outDir": "dist",
"declarationDir": "types"
},
"include": ["src"]
}I prefer using Rollup for libraries because it creates smaller, more efficient bundles compared to Webpack.
Create a Reusable USA Zip Code Validator Component
Now, let’s create a functional component. In this example, I will build a Zip Code input field that validates if the entered value is a valid 5-digit USA Zip Code.
I usually create a src folder and a components subfolder to keep everything organized.
Create a file named src/components/ZipCodeInput.tsx and add the following code:
import React, { useState } from 'react';
interface ZipCodeProps {
label?: string;
onValidZip?: (zip: string) => void;
}
const ZipCodeInput: React.FC<ZipCodeProps> = ({ label = "Enter USA Zip Code", onValidZip }) => {
const [zip, setZip] = useState('');
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setZip(value);
// Regex for 5-digit US Zip Code
const zipRegex = /^\d{5}$/;
if (zipRegex.test(value)) {
setError('');
if (onValidZip) onValidZip(value);
} else {
setError('Please enter a valid 5-digit Zip Code.');
}
};
return (
<div style={{ fontFamily: 'Arial, sans-serif', marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>{label}</label>
<input
type="text"
value={zip}
onChange={handleChange}
maxLength={5}
style={{
padding: '8px',
borderRadius: '4px',
border: error ? '1px solid red' : '1px solid #ccc'
}}
/>
{error && <p style={{ color: 'red', fontSize: '12px' }}>{error}</p>}
</div>
);
};
export default ZipCodeInput;You can see the output in the screenshot below.

I also need an entry point for the library. Create a file named src/index.ts to export all your components:
export { default as ZipCodeInput } from './components/ZipCodeInput';Method 1: Bundle with Rollup
This is my preferred method for professional libraries. Rollup is excellent at “tree-shaking,” which means it removes unused code from the final bundle.
First, install the Rollup plugins:
npm install rollup rollup-plugin-typescript2 @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-peer-deps-external --save-devNext, create a rollup.config.mjs file in your root folder:
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true,
},
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true,
},
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ useTsconfigDeclarationDir: true }),
],
external: ['react', 'react-dom'],
};You can see the output in the screenshot below.

I use rollup-plugin-peer-deps-external to ensure that React isn’t bundled inside the library itself, which prevents version conflicts in the hosting app.
Method 2: Build with Vite (The Faster Way)
If you want a quicker setup, I have found that Vite is a fantastic alternative for building libraries. It uses Rollup under the hood but requires much less configuration.
To use Vite, you would install it and set the build.lib configuration in vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'MyUSComponentLib',
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});I’ve used this method for internal company tools where speed of development was more important than granular control over the build process.
Configure package.json for Publication
Before I publish to NPM, I have to tell the package where to find the bundled files.
Update your package.json with the following fields:
{
"name": "@your-username/my-us-component-lib",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rollup -c"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
}I always include the files array to ensure only the dist folder is uploaded to NPM, keeping the package size small.
Test Your Library Locally
I never publish a library without testing it locally first. I use the npm link command to simulate an installation.
In your library folder, run: npm link
Then, in a separate React test application (like a standard Create React App or Vite project), run: npm link @your-username/my-us-component-lib
Now you can import and use your component just like any other package:
import { ZipCodeInput } from '@your-username/my-us-component-lib';
function App() {
return (
<div>
<h1>Shipping Address</h1>
<ZipCodeInput label="Ship to Zip Code (USA)" onValidZip={(z) => console.log(z)} />
</div>
);
}Publish to NPM
Once I am happy with the results, it is time to share the library with the world.
First, I log in to my NPM account through the terminal:
npm loginThen, I run the build script and publish:
npm run build
npm publish --access publicAnd that’s it! Your React component library is now live and ready to be used in any project.
In this tutorial, I showed you how I create a React component library from scratch using TypeScript and Rollup.
I have found that having a personal library of components like the Zip Code validator makes building enterprise applications much faster and more reliable.
You may read:
- React Component Composition Patterns
- React Router Link Component
- React Component Hierarchy Diagrams
- Create a React JS Table Component

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.