Building Component libraries with TSDX and Tailwind
15 min read

Building Component libraries with TSDX and Tailwind

You can see the video podcast of this post on youtube with more info.

Need to build your own component library? This problem will likely come up at some point in your career and if you’re anything like me you’ve probably wondered, where do I even start? How do other people do this?

Today I’ll walk through a very simple and easy setup to get you jump started building your own reusable component library. No need for a long backstory of my whole career and why we are writing this, this ain’t no recipe website so let’s jump in.

You can find all the finished code for this project on my github here.

Tooling

We will build this project with as few dependencies as possible. Every project, developer, designer, etc has different needs so you’re welcome to add or change tools once we are done.

TSDX

TSDX is a zero-config CLI that helps you develop, test, and publish modern TypeScript packages with ease. It doesn’t even have to be a component library. TSDX takes care of all the tedious tasks of setting up a new project.

TSDX Features

Let's look at some of the features.

Zero-config: This means you don't need to do anything to get building. The basic configuration needed is all taken care of for you.

Rollup: TSDX uses Rollup (similar to Webpack) under the hood and is automatically setup to export both CommonJS and ESM modules without any configuration.

Live Playground: Your new project is set up with a live (React) playground so you can import your project and test it out all in the same place.

Jest: Jest is setup so you can start writing tests. Don't forget to test your project.

Project Templates: There are project templates like react-with-storybook (this is the one we'll be using) which will setup your project with Storybook and React. You can create your library and immediately start developing in Storybook.

Tree shaking: Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. Projects using ES modules and modern bundlers, will drop any code they aren't using. For instance, if they only use one component in our library, the rest of the code wont be included at the app level.

TSDX is also customizable, so if your project needs some special configuration you can always override default functionality, see more here. If you want to read more about TSDX you can check out the docs here.

Tailwind CSS

Tailwind has been gaining a lot of traction in the developer community over the last few years. It’s got great community support, it’s tiny in production, avoids css duplication, and it can be customized and extended to meet your needs (among many other great features).

Newest Tailwind CSS features

There are lots of strong opinions for and against using a library like Tailwind to style your website. But that’s not what we are here for. If you want to learn more you can check out the docs here.

Headless UI

Headless UI is a library built by the creators of Tailwind CSS. It is completely un-styled. Yes, you heard that correctly. There is not a single line of CSS. Just plain, accessible, functional components.

Headless UI component list

We are going to use this so we don’t have to worry about building those complicated components like drop-downs, modals, transitions, and select menus. It currently has support for both React and Vue. It may not seem like a lot of components right now, but there are lots more ideas being discussed on what to add next. You can check out the docs here.

Lets Build

Initializing the project with TSDX

Open up your terminal and navigate to wherever you want this project started. I'll call mine awesome-component-library (catchy I know 😏), but feel free to change this to whatever name you want

npx tsdx create awesome-component-library

After this you will be prompted to select a template, choose react-with-storybook, we want to develop our components in isolation using Storybook.

Once that is finished you are pretty much done with the core library setup. Simple right?

cd awesome-component-library

Open the project in your favorite editor. You’ll notice that you’ve been given a pretty standard folder structure from TSDX. Let's take a look at it:

  • .github: This folder is generated by Github actions which is used to automate your workflows. I definitely recommend reading about them.
  • .storybook: Your Storybook configuration lives here. We'll be using this a bit later on but if you want to learn more you can check out the setup docs here.
  • dist: This is where our bundled code will live. It is where TSDX will output the ES modules, CommonJS modules, our source maps, and all our Typescript types.
  • example: This is the Live Playground piece I described above. It will not be part of the bundle and will not be published.
  • node_modules: All your npm packages go here.
  • src: This is where you will build all your components. This is the main folder that will be compiled to our dist folder.
  • stories: This is where your stories will live for Storybook. Storybook is an open source tool for building UI components and pages in isolation. It streamlines UI development, testing, and documentation.
  • test: This is where we will put all our component tests. I won't be going into writing tests for components in this article, however I highly recommend reading more about how to write tests for components using React Testing Library.
  • package.json - our project dependencies, scripts, configuration, etc.
  • gitignore -  files and folders that will be ignored by git.
  • LICENSE - MIT license is automatically generated.
  • README.md - This is a pretty straight forward readme by TSDX. It has all the commands that are generated with their package. I recommend updating this to suite your package needs.
  • tsconfig.json: The TypeScript configuration file that was generated by TSDX. Unless you need some specific configuration I would leave this alone. If you want to learn more about it you can read more here.

As a quick test I like to try out the commands and see them working. Let's start with a quick build:

yarn build

Take a look at the dist folder. You can see the ESM, CJS, source maps, and Typescript types all there. That seems to be working as expected. Next let's start up Storybook:

yarn storybook

Storybook should start and open up in your browser. There is one story configured and it’s looking pretty bland. Let's fix that.

Installing Tailwind CSS

First we'll begin by installing Tailwind CSS and it's required packages, postcss and autoprefixer.

  • postcss:  PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more. If you want to learn more you can check out a great article here.
  • autoprefixer: This is a PostCSS plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use.
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

Next we’ll create our postcss config. Create a new file at the root level of your project and call it postcss.config.js. Add tailwindcss and autoprefixer to your PostCSS configuration.

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

Next we’ll generate a Tailwind config file. We'll do this using the Tailwind CLI utility included when you install the Tailwind CSS npm package:

npx tailwindcss init

This will create a minimal tailwind.config.js file at the root of your project. You can use this to customize your Tailwind theme and add overrides. If you want to add your own theme colors and classes you can read more about that on Tailwind’s theme documentation.

// tailwind.config.js
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

We need to make a few changes to this config.

First, let's add the Just In Time mode. This is a semi-new feature the Tailwind team is currently working on - this makes development very quick. You will get a warning that it is JIT mode is currently a "preview" feature, however, we're just using it for development and it won't affect our production code. If you want to learn more about it see more here.

Let's also add purge paths to the config. This is so Tailwind can look through the specified files and only add the css classes that are being used.

// tailwind.config.js
module.exports = {
  mode: 'jit',
  purge: ['./src/**/*.{js,jsx,ts,tsx}', './stories/*'],
  ...rest of config
}

Next, create a tailwind.css file at the root of your project. We’ll be adding the required Tailwind CSS here. You can technically put this file wherever you’d like but this is where I prefer to put it.

// tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;

Okay, almost there. For the most part you’re done setting up Tailwind CSS. We just need to get it working in Storybook so we can see the styles there.

Open up ./.storybook/preview.js and include the tailwind.css at the top of the file:

// ./.storybook/preview.js
import '../tailwind.css';
//...

At this point your styles technically work in Storybook. But you’ll notice this little deprecation warning:

DeprecationWarning: Relying on the implicit PostCSS loader is deprecated and will be removed in Storybook 7.0. If you need PostCSS, include '@storybook/addon-postcss' in your '.storybook/main.js' file.`

I don’t like this, and it’s not a good idea to work with deprecated features.

Previously, @storybook/core would automatically add the postcss-loader to your preview. This didn't work well with tools that had already made the jump to v8 of PostCSS, like Tailwind CSS and Autoprefixer.

Instead of continuing to include PostCSS inside the core library, it has been moved to @storybook/addon-postcss. Using this addon will allow us to include v8 of PostCSS so that Storybook can properly load Tailwind.

Let's start by installing the Storybook PostCSS plugin:

yarn add -D @storybook/addon-postcss

Next in ./.storybook/main.js add the PostCSS addon. This configuration comes straight from the docs of the addon, which can be seen here.

// ./storybook/main.js
module.exports = {
  ...
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    {
      name: '@storybook/addon-postcss',
      options: {
        postcssLoaderOptions: {
          implementation: require('postcss'),
        },
      },
    },
  ],
  ...
};
Storybook configuration

Boom. 💥 Done.

Now we need to see if what we did actually works. We can do this by testing out some Tailwind CSS classes in one of our stories:

yarn storybook

Next, open up src/index.tsx and add a Tailwind class to the div and see if we get a result.

// src/index.tsx
export const Thing: FC<Props> = ({ children }) => {
    return (
      // We are adding these classes here
      <div className="mt-10 bg-yellow-100 p-5 rounded-md"> 
        {children || `the snozzberries taste like snozzberries`}
      </div>
    );
};

Lookin good 😎

Tailwind CSS in Storybook working!

As a side note I highly recommend the Tailwind CSS IntelliSense plugin for VSCode. It is really helpful when learning Tailwind for the first time.

Tailwind CSS IntelliSense Plugin

Just start typing inside a className attribute and it’ll start auto-completing classes and give you useful information on what they translate to.

Tailwind CSS class name auto-complete.

We are done with the Tailwind CSS setup and can now start using the full capabilities of Tailwind in Storybook to develop our components.

Headless UI

This part should be pretty simple. Let’s start by installing Headless UI:

yarn add @headlessui/react

Next, we can start with something simple and create a new Toggle Switch component. Create a new file and add the following code. This is an example mostly identical to the one on Headless UI’s examples.

// src/ToggleSwitch.tsx

import React from 'react';
import { Switch } from '@headlessui/react';

export interface ToggleSwitchProps {
  enabled: boolean;
  onChange: (value: Boolean) => void;
}

export const ToggleSwitch = ({ enabled, onChange }: Props) => (
  <Switch
    checked={enabled}
    onChange={onChange}
    className={`${enabled ? 'bg-green-500' : 'bg-gray-400'}
          relative inline-flex flex-shrink-0 h-7 w-14 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus-visible:ring-2  focus-visible:ring-white focus-visible:ring-opacity-75`}
  >
    <span className="sr-only">Use setting</span>
    <span
      aria-hidden="true"
      className={`${enabled ? 'translate-x-7' : 'translate-x-0'}
            pointer-events-none inline-block h-6 w-6 rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`}
    />
  </Switch>
);

Now we’ll export our component. Because TSDX packages everything from the src folder, we will export all of our components from the main index.tsx file. Go into src/index.tsx and get rid of the existing Thing component. Then export everything from ToggleSwitch.tsx.

// src/index.tsx
export * from './ToggleSwitch';
// The rest of your component exports will go here

Next we’ll create the story for the component.

// stories/ToggleSwitch.stories.tsx

import React from 'react';
import { Meta, Story } from '@storybook/react';
import { ToggleSwitch, Props } from '../src';

const meta: Meta = {
  title: 'Input/Toggle Switch',
  component: ToggleSwitch,
  argTypes: {
    onClick: { action: 'clicked' },
    enabled: {
      control: {
        type: 'boolean',
      },
    },
  },
  parameters: {
    controls: { expanded: true },
  },
};

export default meta;

const Template: Story<Props> = (args) => <ToggleSwitch {...args} />;

export const Default = Template.bind({});
Default.args = {
  enabled: false,
};

Now start up Storybook again (if it's not still running):

yarn storybook

Go to the Toggle Switch story and mess with the story controls.

Beautiful. We have Tailwind CSS working with Headless UI in Storybook. Nice work. 👍

Build Configuration

At this point we have Tailwind CSS, Storybook and HeadlessUI working together. However, you’ll notice if we build our package and look at the dist folder there isn’t any CSS. That’s because we need to include the Tailwind build as part of our build process.

Open package.json and include a new build script so that Tailwind can build our CSS file. This comes straight from the Tailwind documentation on building for production:

"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/tailwind.css --minify"

Now we will add this new script to the main build task in package.json so that any time we run a build, a new CSS file is generated. It's important to put this after the tsdx build command because TSDX rewrites the entire dist folder on every build.

"build": "tsdx build && yarn build-tailwind"

Run a build:

yarn build

Now check your dist folder and you’ll see tailwind.css being included and completely minified. It works!

Publish

First what we’ll need is a git repo. Go onto your github account and create a new repository.

Give it a fancy name:

Copy the origin url:

Back in our terminal, initialize the repo by running the following in your project directory:

git init
git remote add origin {your origin url}
git add .
git commit -m “init commit”
git push origin master
Initialize git and commit to your repository.

Now go into your package.json and change the prepare script to:

"prepare": "yarn build"

When publishing, the prepare command is what gets run so we want to make sure the CSS is built when we are publishing.

Next we get to publish this thing. If you already have an npm account setup you can skip the account creation steps. First, you need to have an npm account. Create one here if you don’t have one yet. Second, you need to login to your npm account through the command line:

npm login

You’ll be prompted to enter your username, password, and email address. After successfully logging in we can finally publish our package:

yarn publish --access public

And we’re done! You can go to https://www.npmjs.com/ to search for your package, or you can just look under your account.

Trying it !

Now that we've finished publishing our library, let's actually try it out and see it in action. For this we’ll use the playground that came with TSDX. Remember the example folder from earlier? This is where we can test our new package out. In your terminal type the following commands:

// Change to the playground directory
cd example

// Install packages in the example repo
yarn install

// There’s a known bug that this package update will fix.
yarn add -D parcel@2.0.0-beta.2

// Install your new pacakge
yarn add awesome-component-library
Install your package in the playground

Next, open up example/index.tsx and import the ToggleSwitch along with the tailwind.css file here. We'll also add the ToggleSwitch component to the main App component:

// example/index.tsx
// ... imports
import { ToggleSwitch } from 'awesome-component-library';
import 'awesome-component-library/dist/tailwind.css';

const App = () => {
  const [isToggled, setIsToggled] = React.useState(false);
  return (
    <div>
      <ToggleSwitch
        enabled={isToggled}
        onChange={(v: boolean) => setIsToggled(v)}
      />
    </div>
  );
};
Import our ToggleSwitch and Tailwind CSS file. Add the component the the main App component. 

Save your file and start up the app:

yarn start

And our component works!

Working component library in the playground

Remember how I mentioned earlier that TSDX also exports the types for project? We can see an example of that in our playground. Our project is exported in plain old Javascript, so it will work in any regular React projects that don't use Typescript. However, when using our library in a Typescript project (like the example playground), you can hover over your ToggleSwitch component in in example/index.tsx you can see that Typescript recognizes the interface and exposes the props it takes:

Typescript interface recognized in the consuming application.

Not only that but since we exported them all we can also import and use those interfaces/types in our consuming application.

import { ToggleSwitch, ToggleSwitchProps } from 'awesome-component-library';

interface AppProps extends ToggleSwitchProps {
  // ...other props
}
Example of importing the interface.

One more thing to note about the implementation of this component library. We do have to import our tailwind.css in whatever project we use. However, if we were to use our component library in a project that also uses Tailwind CSS, we can omit the css file import, and include the component library in the purge path of the tailwind.config.css

// new Project tailwind.config.js
purge: [
  './node_modules/awesome-component-library/dist/*.js',
],
tawind.config.js in project that uses Tailwind CSS and our component library

Component Building Process

Now that we are done you may be curious about what to do next. Let's walk through a quick example of how to start a new component.

First, we'll create our component in the src/ folder. For this example lets create a Button component:

// src/Button.tsx
import React from 'react';

export const Button = () => (
  <button className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded">
    Button
  </button>
);
Button component

Next, we'll open up our src/index.tsx and export our component:

// src/index.tsx
export * from './ToggleSwitch';
export * from './Button';
Export your new component

Finally, we'll create our Story:

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

const meta: Meta = {
  title: 'Input/Button',
  component: Button,
  parameters: {
    controls: { expanded: true },
  },
};

export default meta;

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

export const Default = Template.bind({});
Default.args = {};

Let's start up Storybook and see our new component:

yarn storybook

New Button component in Storybook

Now we'll add some Button props and Storybook controls so we can interact with the button. First, let's add the props to the button:

import React from 'react';

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  title: string;
}

export const Button = ({ disabled, title, ...rest }: ButtonProps) => (
  <button
    className={`bg-transparent text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded disabled:text-gray-400 disabled:border-gray-400 disabled:cursor-default ${!disabled && 'hover:bg-blue-500'}`}
    disabled={disabled}
    {...rest}
  >
    {title}
  </button>
);

You'll notice above I also added some disabled styles to the button.

Now let's update the story with controls:

import React from 'react';
import { Meta, Story } from '@storybook/react';
import { Button, ButtonProps } from '../src';

const meta: Meta = {
  title: 'Input/Button',
  component: Button,
  argTypes: {
    onClick: { action: 'clicked' },
    title: {
      control: {
        type: 'text',
      },
    },
    disabled: {
      control: {
        type: 'boolean',
      },
    },
  },
  parameters: {
    controls: { expanded: true },
  },
};

export default meta;

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

export const Default = Template.bind({});
Default.args = {
  title: 'Storybook Button',
  disabled: false
};
Updated story with controls.

Let's try the controls in Storybook:

Nice! 👍🏻

Once you get the story setup you can continue to build on your components and see updates instantly in Storybook as you save. It's a really great way to develop your library.

This is the general flow when creating new components. Feel free to make it your own.

🎉 Congrats 🎉

We finally made it to the end!  If you made it this far I appreciate you staying with me. I hope you learned something or found this useful in some way. I look forward to seeing you in the next post.

Enjoying these posts? Subscribe for more