Skip to main content

Distributing CSS in npm package

··

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;
}
  1. You may be able to customize it via CSS variables
*,
:after,
:before {
  --tw-ring-color: rgba(59, 130, 246, 0.5);
}
  • 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.

solutionviteesbuildwebpacknextparcelrollupbabelcli
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 #

ReactNative with Tailwind #

Read more: Component libraries trends, Styling components