Madushika Perera Software Engineer at FusionGrove

Build a component library with React and TypeScript

12 min read 3498


React is still the most famous frontend library in the web developer community. When Meta (previously Facebook) open-sourced React in late 2013, it made a huge impact on single-page applications (SPAs) by introducing concepts like the virtual DOM, breaking the UI into components, and immutability.

React has evolved a lot in the past few years, and with the introduction of TypeScript support, Hooks, <Suspense>, and React Server Components, we can assume that React will still hold the crown for best frontend tool. React has opened the doors to some awesome projects like Next.js, Gatsby, and Remix, which focus more on improving the developer experience.

Along with this robust React ecosystem is a wide variety of component libraries, like Material UI, Chakra UI, and React-Bootstrap. When you come across these awesome tools and libraries, have you ever wondered how they are made? Or what it would take to create your own UI component library with React?

In the following article, we’ll learn how to build our component library with TypeScript and publish it to npm, so that you too can contribute to React’s ever-growing community of projects.

Required tools

Creating a UI library is quite different from creating a web application; instead of using popular tools like Next.js or Create React App, we have to start from scratch.

To build this UI library, we’ll be using the following tools:


There’s no doubt that TypeScript is the go-to method when developing with JavaScript. TypeScript will save a ton of time for us, because it introduces types and compile-time checking.

In addition, TypeScript will be helpful when we build components for our library, because we will be dealing with props in React components, and defining types will avoid issues when passing props to the components.


We will need some custom configurations for our library. Rollup is an excellent middle ground between popular JavaScript bundlers because it is more customizable than Parcel, but requires less configuration effort than webpack.


We are building components, so we need a way of visualizing them in an isolated environment. Storybook does that for us because it spins up a separate playground for UI components

We made a custom demo for .
No really. Click here to check it out.

Jest and React Testing Library

Jest is a complete testing framework with a test runner, plus an assertion and mocking library. It also lets the user create snapshot tests for components, which is ideal when building components in React.

React Testing Library helps us write tests as if a real user is working on the elements.

Project structure

Let’s think about the project structure. We will be adding components, tests, stories, and types to our project to add them under component directories.

Let’s start with project initialization. For this, create a directory and initialize it as a JavaScript project using the following command:

npm init

This command will generate a package.json file in our application.

Now, install TypeScript and React to our project through the following command:

npm i -D react typescript @types/react

Notice that we are passing the flag -D because we need to install it as devDependencies rather than a project dependency; we’ll need those dependencies when we are building the bundle.

In the future, this library will be used inside a React project. Therefore, we don’t need to bundle it, and we can move the React dependency to the peerDependency section (you will probably need to add this in your package.json file).

Creating components

Now that we have added React and TypeScript, we can start creating our components, beginning with a button.

Create the components under the src/components directory. Because this is a TypeScript project, we first create the type definitions for the components, which I’m naming Button.types.ts:

import {  MouseEventHandler } from "react"
export interface ButtonProps {
    text?: string,
    disabled?: boolean,
    size?: "small" | "medium" | "large",
    onClick?: MouseEventHandler<HTMLButtonElement>

This file consists of all the props of the button, including the onClick event. We will use these props in the button component to add or enable different properties. Now, let’s move on to the creation of the button component itself.

Integrating styled-components for a button component

In this project, we will not be using regular CSS; instead, we will be using CSS-in-JS. CSS-in-JS provides many benefits over regular CSS, for example:

  • Reusability: Because CSS-in-JS is written in JavaScript, the styles you define will be reusable JavaScript objects, and you can even extend their properties
  • Encapsulation: CSS-in-JS scopes are generated by unique selectors that prevent styles from leaking into other components
  • Dynamic: CSS-in-JS will allow you to dynamically change the properties of the styling depending on the value that the variables hold

There are many ways to write CSS-in-JS in your component, but for this tutorial we will be using one of the most famous libraries called styled-components. You can run the following commands to install the Storybook dependencies along with the type definitions for TypeScript support:

npm install -D styled-components @types/styled-components

Now, let’s create our button component:

import React,{FC} from 'react'
import styled from 'styled-components';

import {ButtonProps} from "./Button.types"

const StyledButton = styled.button<ButtonProps>`
    border: 0;
    line-height: 1;
    font-size: 15px;
    cursor: pointer;
    font-weight: 700;
    font-weight: bold;
    border-radius: 3px;
    display: inline-block;
    padding: ${props => props.size === "small"? "7px 25px 8px" : (props.size === "medium"? "9px 30px 11px" : "14px 30px 16px" )};
    color: ${props => props.primary? "#1b116e":"#ffffff"};
    background-color: ${props => props.primary ? "#6bedb5":"#1b116e"};
    opacity: ${props => props.disabled ? 0.5 : 1};
    &:hover {
      background-color: ${props => props.primary ? "#55bd90":"#6bedb5"};
    &:active {
        border: solid 2px #1b116e;
        padding: ${props => props.size === "small"? "5px 23px 6px" : (props.size === "medium"? "7px 28px 9px" : "12px 28px 14px" )};

const Button: FC<ButtonProps> = ({size, primary, disabled, text, onClick, ...props}) => {
    return (
        <StyledButton type="button" onClick={onClick} primary={primary} disabled={disabled} size={size} {...props}>

export default Button;

If you go through the code above, you will notice something special: we have defined a variable called StyledButton to which we assign styling properties through special tags called tagged template literals, a new JavaScript ES6 feature that enables you to define custom string interpolations.

These tagged templates are used to write the StyledButton variable as a React component that you can use like any other. Notice that we have access to the component props inside these special tagged templates.

Creating an input component

Now let’s create an input component, starting first with its types by creating a file called Input.types.ts:

import { ChangeEventHandler } from "react"

export interface InputProps {
    id?: string,
    label?: string,
    message?: string,
    disabled?: boolean,
    onChange?: ChangeEventHandler<HTMLInputElement>

Like in the previous component, we have defined the prop attributes in the Input.types.ts file. You can see that we have added the onChange event and assigned it ChangeEventHandler<HTMLInputElement> from React. We do this in order to tell that these props are responsible for an input change event.

Finally, let’s create the input component:

import React, { FC,Fragment } from 'react'
import styled from 'styled-components';
import { InputProps } from "./Input.types"

const StyledInput = styled.input<InputProps>`
    height: 40px;
    width: 300px;
    border-radius: 3px;
    border: solid 2px ${props => props.disabled ? "#e4e3ea" :(props.error ? "#a9150b":(props.success ? "#067d68" : "#353637"))};
    background-color: #fff;
      border: solid 2px #1b116e;

const StyledLabel = styled.div<InputProps>`
   font-size: 14px;
   color: ${props => props.disabled ? "#e4e3ea" : "#080808"};
   padding-bottom: 6px;

const StyledMessage = styled.div<InputProps>`
   font-size: 14px;
   color: #a9150b8;
   padding-top: 4px;

const StyledText = styled.p<InputProps>`
   margin: 0px;
   color: ${props => props.disabled ? "#e4e3ea" : (props.error ? "#a9150b": "#080808")};

const Input: FC<InputProps> = ({id, disabled, label, message, error, success, onChange, placeholder, ...props}) => {
    return (
        <StyledLabel><StyledText disabled={disabled} error={error}>{label}</StyledText></StyledLabel>
        <StyledInput id={id} type="text" onChange={onChange} disabled={disabled} error={error} success={success} placeholder={placeholder} {...props}></StyledInput>
        <StyledMessage><StyledText error={error}>{message}</StyledText></StyledMessage>

export default Input;

In the above code, you can see that we have defined several styled-components and wrapped them together through a React fragment. We use a fragment because it enables us to group multiple sibling components without introducing any extra elements in the DOM. This will come in handy because there won’t be any unnecessary markup in the rendered HTML of our components.

Configuring TypeScript and Rollup

Now it’s time to configure TypeScript with Rollup. We are using TypeScript to build the components; in order to build the library as a module, we will need to configure Rollup along with it.

In a previous step, we installed TypeScript to our project, so now we just need to add the TypeScript configurations (from the tsconfig.json file). For this, we can use TypeScript’s CLI to generate the file:

npx tsc --init

Let’s make a few tweaks to this tsconfig.json file so that it will fit our project scenario:

  "compilerOptions": {
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "jsx": "react",
    "module": "ESNext",  
    "declaration": true,
    "declarationDir": "types",
    "sourceMap": true,
    "outDir": "dist",
    "moduleResolution": "node",
    "emitDeclarationOnly": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
  "exclude": [

In the above code, you can see that we have added the configurations like "skipLibCheck": true, which skips type checking of declaration files. This can save time during compilation.

Another special configuration is "module": "ESNext", which indicates that we will be compiling the code into the latest version of JavaScript (ES6 and above, so you can use import statements).

The attribute "sourceMap": true tells the compiler that we want source map generation. A source map is a file that maps the transformed source to the original source, which enables the browser to present the reconstructed original in the debugger.

You will notice that we have defined an "exclude" section in order to tell TypeScript to avoid transpiling specified directories and files, so it won’t transpile the tests and stories of our library.

Now that we have configured TypeScript, let’s begin configuring Rollup.

First, install Rollup as a devDependencies project through the following command:

npm i -D rollup

However, installing Rollup is not enough for our project because we will need additional features, like:

  • Bundling to CommonJS format
  • Resolving third-party dependencies in node_modules
  • Transpiling our TypeScript code to JavaScript
  • Preventing bundling of peerDependencies
  • Minifying the final bundle
  • Generating type files (.d.ts), which provide TypeScript type information about the components in our project

The above features will come in handy when we finally build and use the library as a package. CommonJS is a specification standard used in Node, and modules are loaded synchronously and processed in the order the JavaScript runtime finds them.

Luckily, there are several plugins for Rollup that we can use for the above requirements, which can be installed through the following command:

npm i -D @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-peer-deps-external rollup-plugin-terser rollup-plugin-dts

Now that Rollup and its awesome plugins are installed, let’s move on to its configuration. Create a rollup.config.js file in the root of our project and add the following:

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import { terser } from "rollup-plugin-terser";
import peerDepsExternal from 'rollup-plugin-peer-deps-external';

const packageJson = require("./package.json");

export default [
        input: "src/index.ts",
        output: [
                file: packageJson.main,
                format: "cjs",
                sourcemap: true,
                file: packageJson.module,
                format: "esm",
                sourcemap: true,
        plugins: [
            typescript({ tsconfig: "./tsconfig.json" }),
        external: ["react", "react-dom", "styled-components"]
        input: "dist/esm/types/index.d.ts",
        output: [{ file: "dist/index.d.ts", format: "esm" }],
        plugins: [dts()],

In the code above, you can see that we are building our library with both CommonJS and ES modules. This will allow our component to have more compatibility in projects with different JavaScript versions. ES modules allows us to use named exports, better static analysis, tree shaking, and browser support.

Next, we have to define the paths in package.json for both ES modules and CommonJS:

"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",

Now that we have added the necessary configurations, let’s add the build command in the script section of the package.json file:

"build": "rollup -c"

Now you can build the project by using the following command from your terminal:

npm run build

The above command will generate a directory called dist, which is our build directory defined in the Rollup configurations.

Integrating Storybook

The next step is to integrate Storybook into our library. Storybook is an open-source tool for building UI components and pages in isolation. Because we are building a component library it will help us to render our components and see how they behave under particular states or viewpoints.

Configuring Storybook is quite easy thanks to its CLI, which is smart enough to recognize your project type and generate the necessary configurations:

npx sb init

When you execute the above code on your terminal, it will generate the directories .storybook and stories in your project. .storybook will hold all the configurations, and stories holds the stories for your component. A story is a unit that captures the rendered state of a UI component.

The above command will also add some scripts for our package.json file as well:

  "scripts": {
   "storybook": "start-storybook -p 6006",
   "build-storybook": "build-storybook"

The above scripts added by the Storybook CLI will allow us to build the Storybook through the npm or yarn command like so:

"npm run storybook"
"yarn storybook"

Next, we will change the default directory structure and move the stories file to our component level. This will make our library more organized, as all the files (types, tests, and stories) related to a particular component are moved to one place. We will have to indicate them in the main.js file under the .storybook directory.

First, let’s start by changing the default configurations of the Storybook:

 module.exports = {
  "stories": [
    "../src/**/**/*[email protected](js|jsx|ts|tsx)"
  "addons": [

Now that we have done the necessary configurations, we can write our first story for the button component:

import React from 'react';
import { Story, Meta } from '@storybook/react';

import Button  from './Button';
import {ButtonProps} from "./Button.types"

export default {
  title: 'Marbella/Button',
  component: Button,
  argTypes: {
} as Meta<typeof Button>;

const Template: Story<ButtonProps> = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  disabled: false,
  text: 'Primary',

export const Secondary = Template.bind({});
Secondary.args = {
  primary: false,
  disabled: false,
  text: "Secondary",

export const Disabled = Template.bind({});
Disabled.args = {
  primary: false,
  disabled: true,
  text: 'Disabled',

export const Small = Template.bind({});
Small.args = {
  primary: true,
  disabled: false,
  text: 'Small',

export const Medium = Template.bind({});
Medium.args = {
  primary: true,
  disabled: false,
  text: 'Medium',

export const Large = Template.bind({});
Large.args = {
  primary: true,
  disabled: false,
  text: 'Large',

In the above code, we imported the button component and its types. Next, we created a story template from it to render different states of the button (like primary, secondary, and disabled) through template arguments. This template allows us to see how the button component behaves according to the props we pass to the component.

Now let’s see how this component is rendered in Storybook by running the following command:

npm run storybooks

The above command will open a new tab on your browser and render the component inside the Storybook view.

button component displayed in Storybook

In the above image, you can see that in the left sidebar we have our components defined in the story, including the templates. In the center, you can see our button component with the options we included in the last code block.

Next, we can create the story for the input component:

import React from 'react';
import { Story, Meta } from '@storybook/react';
import Input  from './Input';
import {InputProps} from "./Input.types"

export default {
  title: 'Marbella/Input',
  component: Input,
  argTypes: {
} as Meta<typeof Input>;

const Template: Story<InputProps> = (args) => <Input {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  error: false,
  disabled: false,
  label: 'Primary',

export const Success = Template.bind({});
Success.args = {
  error: false,
  disabled: false,
  label: "Success",

export const Error = Template.bind({});
Error.args = {
  error: true,
  disabled: false,
  message: "Error",

export const Disabled = Template.bind({});
Disabled.args = {
  disabled: true,
  label: 'Disabled',

In the above code, we have defined the story templates to indicate the state of the input component through props. Let’s see how this component is rendered in Storybook.

The input component in Storybook

In this image, you can see the input component’s story templates and properties. Now you can play around with these options and see how the components behave in different scenarios. You can enhance these properties by installing some additional add-ons for Storybook.

You may face some issues when configuring Storybook due to missing dependencies like react-dom. If you face any issues, go through the logs and install only the necessary library, or try downgrading the version of Storybook.

Testing with Jest and React Testing Library

Now it’s time to add some testing for our component. First, install the dependencies to configure Jest and React Testing Library. You can execute the following command to install the dependencies:

npm install @testing-library/react @testing-library/jest-dom @testing-library/user-event  jest @types/jest --save-dev

Here we have installed Jest and testing-library along with several add-ons for them like jest-dom and user-event, which enable Jest to render the components using the DOM for making assertions, and mock user events (like click, focus, or lose focus).

After installing the dependencies, create a jest.config.js file to hold the configurations for the tests. You can also use the Jest CLI to generate this file by running the following command:

npx jest --init

Jest CLI output

The above image is the console output that you will get when you use the Jest CLI.

Now, let’s start writing the tests, starting with the button component:

import React from "react";
import '@testing-library/jest-dom'
import {render, screen } from '@testing-library/react'

import Button from "./Button";

describe("Running Test for Marbella Button", () => {

  test("Check Button Disabled", () => {
    render(<Button text="Button marbella" disabled/>)
    expect(screen.getByRole('button',{name:"Button marbella"})).toBeDisabled();


In the above code, we are rendering the button component and checking if we are rendering the properties we defined (in this case, disabling the button). This particular test will pass if the rendered button is disabled.

Next, we can test the input component:

import React from "react";
import '@testing-library/jest-dom'
import userEvent from "@testing-library/user-event";
import {render, screen } from '@testing-library/react'

import Input from "./Input";

describe("Running Test for Marbella Input", () => {

  test("Check placeholder in Input", () => {
    render(<Input placeholder="Hello marbella" />)
    expect(screen.getByPlaceholderText('Hello marbella')).toHaveAttribute('placeholder', 'Hello marbella');

  test("renders the Input component", () => {
    render(<Input placeholder="marbella" />)
    const input = screen.getByPlaceholderText('marbella') as HTMLInputElement
    userEvent.type(input, 'Hello world!')
    expect(input.value).toBe('Hello world!')


In the above code, we have two test scenarios: one will render the input component with a prop placeholder, and the other will mock a user event of typing inside the component. Let’s execute the test through the following command:

npm run test

Output of Jest testing

The above image shows the output of running the test with some metrics.

Packaging and publishing to npm

We’re all done! Now we can publish this library as an npm package. To create and publish a package on npm, you will need to create an account on npmjs. When you create an account, search for your package name in npmjs just to see if it already exists, because there may be packages with similar names.

Next, begin the process of publishing your package by running the following command in the terminal:

npm login

This will prompt you to enter the username and password of your npm account. Do so, then run the build command again just to build the package for the last time. After building we can publish it as a package through the following command:

npm publish --access public

You should be able to see the published package on npm in your profile. Now your package will be available for everyone to download!


In this article, we built a React library with TypeScript and used CSS-in-JS with some tools like Rollup, Storybook, and Jest.

There are many ways to create component libraries, and there are a lot of starter templates available for you to create one. But building your own library from scratch gives you the opportunity to add or use the tools that you love the most, and it gives you in-depth knowledge on how build tools work.

Thank you for reading this article! I would like to hear your thoughts in the comment section.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Madushika Perera Software Engineer at FusionGrove

18 Replies to “Build a component library with React and TypeScript”

  1. Hi,

    Thanks for the wonderful article.

    I getting error while running ‘rollup -c’.

    dist/esm/types/index.d.ts → dist/index.d.ts…
    [!] Error: Could not resolve entry module (dist/esm/types/index.d.ts).

    In the dist folder types are are saved in esm/types/components/Button/index.d.ts

    How to resolve this?

      1. It took me some time, but a was able to solved it!
        Replace “dist/esm/types/index.d.ts” by “types/index.d.ts”

        input: “types/index.d.ts”,
        output: [{ file: “dist/index.d.ts”, format: “esm” }],
        plugins: [dts()],

        1. I still had an error and it took adding “rootDir” to my tsconfig. If no rootDir this error will persist.

  2. The rollup config file is asking for src/index.ts file but not included with article. please share the contents of it. Also can you share code on a github repo?

    1. Hello, I had the same issue. The tutorial doesn’t mention a folder structure, but the thing it that you must have an index.ts file on each component, but also must have one on the components folder itself, with something like ‘export { default as Button } from ‘./Button” (repeat to all your components), and finally another index.ts file on your src folder with something like ‘export * from ‘./components”. Basically, you must have an index.ts on Button folder, then Components folder, then SRC folder, and you should be fine.

  3. Wooow, I dont believe this, a great article, I just want to suggest that you can creat a full react cource entitled creating a component library by react ,,, it would be great.think about it. Anyway I learned so many things from this article.
    thank you.

  4. Hi,
    I stopped on “npm run build” with the following error:

    src/index.ts → dist/cjs/index.js, dist/esm/index.js…
    [!] Error: Could not resolve entry module (src/index.ts).
    at async Promise.all (index 0)

    in this tutorial there are no instructions for creating the “src/index.ts” file, but there is a reference to it in rollup.config.js in line 12

    Fernando Pacheco

  5. I created the file with this content:

    import Button from ‘./components/Button’
    import Input from ‘./components/Input’

    export { Button, Input }

    build without error

  6. Hello all, wonderful article. Please can anyone shar how to use it in a project. I tried importing it like this into a project:

    import { Button } from ‘@philz/react-ui-kit’;

    but got the following error:

    Could not find a declaration file for module ‘@philz/react-ui-kit’

  7. Running into Button is not exported from library error. I have an index file that

    exports * from ‘./components/Button/Button.tsx’

    published and installed the library into a project and getting this error.

    also I’m getting a cannot find type declarations error…

  8. I’m getting a bunch of errors relating to styled-components:

    Module not found: Error: Can’t resolve ‘styled-components’ in ‘X:\Github\component-library-test2\node_modules\@lsg2099\react-component-library-test2\dist\esm’

    WARNING in ./node_modules/@lsg2099/react-component-library-test2/dist/esm/index.js
    Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
    Failed to parse source map from ‘X:\Github\component-library-test2\node_modules\@lsg2099\react-component-library-test2\src\components\Button.tsx’ file: Error: ENOENT: no such file or directory, open ‘X:\Github\component-library-test2\node_modules\@lsg2099\react-component-library-test2\src\components\Button.tsx’
    @ ./src/App.tsx 7:0-64 45:37-43
    @ ./src/index.tsx 7:0-24 11:33-36

    WARNING in ./node_modules/@lsg2099/react-component-library-test2/dist/esm/index.js
    Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
    Failed to parse source map from ‘X:\Github\component-library-test2\node_modules\@lsg2099\react-component-library-test2\src\components\Input.tsx’ file: Error: ENOENT: no such file or directory, open ‘X:\Github\component-library-test2\node_modules\@lsg2099\react-component-library-test2\src\components\Input.tsx’
    @ ./src/App.tsx 7:0-64 45:37-43
    @ ./src/index.tsx 7:0-24 11:33-36

    Thats for starters. Most of the others are probably a result of the above failings. Did anyone else have this issue?

    1. You need add package ‘rollup-plugin-sass-modules’, after that add import it to rollup.config.js
      import sassModules from ‘rollup-plugin-sass-modules’;

      and use it in rules:

      sassModules({include: [‘**/*.scss’, ‘**/*.sass’]})

  9. Anybody having “Could not find a declaration file for module” error after publishing your library, You need to add type properties inside package.json.

    “types”: “./dist/index.d.ts”,

  10. hi.
    I getting error while running ‘rollup -c’.
    [!] Error: Could not resolve entry module (rollup.config.js).

Leave a Reply