This is a repost from 2021. Every other week for the time being, I will be featuring some of my older posts that are still highly relevant. A few updates have been made but it’s largely the original.
Hope you enjoy, and a new post will be out next week!
Have you ever thought about making shared npm modules that share styles in the most efficient way?
The purpose of this post can be summed up in one sentence: Supported using our library in apps without tailwind, and those with tailwind, without having duplicate styles, or a painful experience.
As a library author who needs to style some of your web code, it’s tough to do without enforcing styling preferences on the apps using this shared library.
Here’s what happened and how I solved this in the most ideal way I could think of at the time.
A few months ago, I created a calendar component using React, date-fns, and tailwind. You can find it at the GitHub link below.
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:
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: [
'node_modules/@zach.codes/react-calendar/dist/**/*.js',
],
}
Awesome, this seems simple enough! When I launched the library I thought this would be all that I needed... turns out I was wrong.
This issue was 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 = [
'react-calendar.cjs.development.js',
'react-calendar.cjs.production.min.js',
'react-calendar.esm.js',
];
for (let file of files) {
let filePath = `${localDir}/dist/${file}`;
let contents = fs.readFileSync(filePath, { encoding: 'utf8' });
fs.writeFileSync(filePath, contents.replace(/rc-/g, ''));
}
};
check();
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: [
...
'node_modules/@zach.codes/react-calendar/dist/**/*.js',
],
},
}
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!
Summary
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.
Gotchas
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.
Very good point. Thanks for sharing!