Distributing CSS in npm package

stereobooster - Sep 9 '23 - - Dev Community

Introduction

Typical problem: you want to distribute React (Solid, Vue, etc.) component in npm package. Most likely it will need some kind of styles and maybe assets (images, svg, fonts). How you're gonna do it. Two options:

  • distribute CSS files
  • CSS-in-JS

Distributing CSS has following issues:

  • You need explicitly include those files (dependency)
  • You may need to process them (with bundler/compiler) in order to adjust paths
  • They may result in dead code (e.g. code that is never used)
  • Styles may clash (global namespace, isolation)
  • There can be issues with non-deterministic resolution
  • No customisation (no theme support)

Distributing CSS-in-JS:

  • resolves all CSS issues, but also
  • adds runtime penalty
  • increases bundle size

This is classical point of view, but there is a twist. Picture may change a bit with:

  • atomic CSS
  • CSS variables (custom properties)
  • zero-runtime CSS-in-JS (compilation)

CSS-in-JS alternatives

Before we continue let's mention alternatives:

If you want to distribute components (in npm package) you would have to compile those to CSS and distribute this file, which brings us back to CSS issues.

Note: there is a way to compile Tailwind inside the npm package

Example: @dlarroder/playground (blog post)

  1. It contains global styles:
*,
:after,
:before {
  border: 0 solid #e5e7eb;
  box-sizing: border-box;
}
Enter fullscreen mode Exit fullscreen mode
  1. You may be able to customize it via CSS variables
*,
:after,
:before {
  --tw-ring-color: rgba(59, 130, 246, 0.5);
}
Enter fullscreen mode Exit fullscreen mode
  • But there is no type-safety. I can do a typo --tw-ring-col: rgba(59, 130, 246, 0.5); and nothing will notify me about the error
  1. What if you use two component libraries that use different versions of Tailwind. There will be clash
  2. CSS file may contain all styles for all components, but if I use only one component all the rest of styles will be a dead code

CSS-in-JS issues

The biggest problem is runtime penalty. Your components would need to parse code for CSS (template literals or JS object), do a vendor specific prefixing, and inject styles and all this happens in the main thread.

Second problem is increased bundle size, which includes runtime itself and CSS expressed in JS.

Don't forget about server side rendering, which also would need special handling.

As the solution you may use one of zero-runtime approaches (in alphabetic order):

But then again if you "compile" CSS-in-JS before distributing via npm you will end up with "style" file. So you need to distribute it as is and compilation should be done by the consumer, which may be problematic. Because there are a lot of bundlers/compilers, for example:

  • webpack/babel
  • vite/esbuild
  • turbopack/swc
  • etc

So you either will vendor-lock your consumers to one solution or CSS-in-JS need to provide all options.

solution vite esbuild webpack next parcel rollup babel cli
vanilla-extract + + + + + +
linaria + + + + +
panda +
compiledcssinjs + + +

Note: this is not a fair comparison - devil is in details.

Real-world experience

What do big component libraries (UI kits) choose to use.

Use compile-time CSS-in-JS

Use Tailwind

Use runtime CSS-in-JS

Use "nothing"

There is a trend for un-styled components (aka renderless, headless). They use "nothing", but this becomes responsibility of consumer to provide styles (including essential ones):

Use CSS

Related

ReactNative with Tailwind

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .