Bundling Tailwind with Open Source Components
6 min read

Bundling Tailwind with Open Source Components

Bundling Tailwind with Open Source Components

A few months ago, I created a calendar component using React, date-fns, and tailwind

Simple and flexible events calendar written in React - zackify/react-calendar

When I decided on tailwind for the library, I wanted to support two scenarios:

I knew some people would not be using Tailwind, and I needed to export a CSS file for those consumers.

I also want to let those already using tailwind, to add this component with no extra work.

This is possible, thanks to Tailwind's new JIT mode:

Just-in-Time Mode - Tailwind CSS
A faster, more powerful, on-demand engine for Tailwind CSS v2.1+.

This meant my library could use any class name, and the consuming application would pick them up, without needing to add extra configuration if my library wanted to use more tailwind variants in the future.

My first attempt

Initially, I setup a tailwind.config.js inside of the repo

module.exports = {
  purge: {
    content: ['./src/**/*.tsx'],
    options: {
      keyframes: true,
      fontFace: true,

When building the library, I added this command to my package.json

"build": "tsdx build && npm run build-tailwind",
"build-tailwind": "NODE_ENV=production tailwindcss build src/tailwind.css -o dist/calendar-tailwind.css"

I'm using tsdx for the library, since it handles bundling and support for TS with zero configuration on my part. Anytime I build the library, I'm also spitting out the CSS used in the library.

Consuming the CSS

For those not using Tailwind in their own apps, they need to add

import '@zach.codes/react-calendar/dist/calendar-tailwind.css';

to their application, and make sure they are using a bundler that supports loading CSS. Pretty simple, but how will those already using tailwind consume it?

They must add a new entry to the purge section in their app's tailwind.config.js

module.exports = {
  purge: [

Awesome, this seems simple enough! When I launched the library I thought this would be all that I needed... turns out I was wrong.

CSS should be calendar specific · Issue #9 · zackify/react-calendar
I'm not sure why your css rules are generic? They should all relate to a calendar ID so they only effect the calendar component As it stands, anyone using your project will have the css rules a...

I had an issue filed about the generic CSS rules being applied. These come from tailwind's preflight setting. At the time I didn't know about this.

Also, I realized that tailwind rules are generic enough that a consuming app not using tailwind could end up with a conflict. So let's go over the approach I've been dwelling on for the past couple weeks, and see if it works!

The Ideal Way

I started off by adding a prefix to my tailwind config:

module.exports = {
  prefix: 'rc-',

I went with rc for react-calendar and then went through, and added this in front of every class in the library. It took about 10 minutes, since it's not a very large library.

Next, I added an env flag for disabling the preflight check:

corePlugins: {
    preflight: process.env.TW_PREFLIGHT == 'false' ? false : true,

Then proceeded to update my tailwind build command in the package.json to do two builds:

NODE_ENV=production tailwindcss build src/tailwind.css -o dist/calendar-tailwind.css

NODE_ENV=production TW_PREFLIGHT=false tailwindcss build src/tailwind.css -o dist/calendar-tailwind-no-reset.css

This will let consumers import the styles without the default reset styles for the page, if they want.

One Problem left...

Alright, so these new tweaks solve the issue for consumers without tailwind. Those who do use tailwind have a new problem. Our code is prefixed, and theirs will not be. So their tailwind config wont pick up our styles. After lots of thinking, I came up with one approach, that I hope is the easiest 🤓

Added a new script to the package.json:

"postinstall": "node tw-check.js"

and then created a new file called tw-check.js

let localDir = __dirname;
let installedDir = process.env.INIT_CWD;

const fs = require('fs');

let check = () => {
  if (localDir === installedDir) return;

  //if they are not using tailwind return early
  if (!fs.existsSync(`${installedDir}/tailwind.config.js`))
    return console.log('no tw');

  const files = [

  for (let file of files) {
    let filePath = `${localDir}/dist/${file}`;
    let contents = fs.readFileSync(filePath, { encoding: 'utf8' });
    fs.writeFileSync(filePath, contents.replace(/rc-/g, ''));

You may be thinking.... Waaaa? This script is really simple when we break it down.

NPM and Yarn expose a postinstall script that will get ran after our package gets installed.

When that happens, our script will check "is this running in the local package." To put it another way, is this npm install taking place inside of react-calendar itself? This would only happen when locally developing the component. We want to skip doing anything if that is the case.

We only continue if we are being installed inside of another project. When that happens, we check if the consuming application is using Tailwind, by checking for a tailwind.config.js. If that file exists, we loop over our built files in the dist folder, and remove the rc- prefix!

How to use

Let's summarize how consumers will use this library:

"I'm not using tailwind"

npm install @zach.codes/react-calendar date-fns

Then, inside of their app, they will add:

import '@zach.codes/react-calendar/dist/calendar-tailwind.css';

If they do not like our reset css, they can optionally use

import '@zach.codes/react-calendar/dist/calendar-tailwind-no-reset.css';

and provide their own.

"I am using tailwind in my app"

npm install @zach.codes/react-calendar date-fns

Our postinstall script removes the prefixes after install, and now they have to update their tailwind.config.js

module.exports = {
  purge: {
    content: [

We've supported those without tailwind, and those with tailwind, without having duplicate styles, or a painful experience. I think this setup is mostly the best of both worlds!


I want to take a moment and summarize the setup process I would recommend when starting a new shared component from scratch and using Tailwind.

First, your library's config should look something like this:

module.exports = {
  prefix: 'rc-',
  corePlugins: {
    preflight: false

To support those without tailwind, we need a prefix, and we need to offer the built css without touching global styles.

Next, including some sort of postinstall script that removes the prefix if the consuming application uses tailwind, is a must. Otherwise we would be asking consumers to include duplicate tailwind styles inside of their app.

I don't think this approach is perfect, but it is pretty neat. I would love to hear from others about their experience using Tailwind this way, and if there are any other approaches out there. Please react out to us on Twitter with any comments.



There's a few parts of this to be careful with. First, if tailwind introduces breaking changes, this could cause issues for apps that consume our library, but use a different tailwind version. I don't see this being much of an issue though. Since tailwind was first released, the classes haven't changed. With the just-in-time mode coming out, it prevents issues where the user's configuration doesn't match ours.

Lastly, I would recommend removing the preflight (css reset) from the start. My library takes advantage of some of the defaults for styling, so I decided to export both. This can be confusing to users, so it might be best to leave it out in a component library.

Enjoying these posts? Subscribe for more