Distributing CSS in npm package
Table of Contents
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:
- atomic CSS UI kit - Tailwind it the most popular option here
- CSSModules
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)
- It contains global styles:
*,
:after,
:before {
border: 0 solid #e5e7eb;
box-sizing: border-box;
}
- 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
- What if you use two component libraries that use different versions of Tailwind. There will be clash
- 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):
- compiledcssinjs
- linaria
- macaron uses
vanilla-extract
- panda
- style9
- styled-vanilla-extract uses
vanilla-extract
- tamagui inspired by
vanilla-extract
- unocss
- vanilla-extract
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 #
- mui: [RFC][system] Zero-runtime CSS-in-JS implementation. But currently uses emotion
- Chakra UI: The future of Chakra UI. Currently uses panda. Before was using emotion.
Use Tailwind #
Use runtime CSS-in-JS #
- baseweb uses styletron
- mantime uses emotion based css-in-js library
- fluentui: Motivations for moving away from SCSS. Currently uses @fluentui/merge-styles
- evergreen uses ui-box
- grommet uses styled-components
- All
react-native-web based systems:
- paper
- RNUI
- magnus
- React Native UI Kitten
- gluestack-ui, successor for NativeBase
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):
- radix-ui primitives uses
style
prop and CSS variables - ariakit uses
style
prop and CSS variables - headlessui can be used with Tailwind
- reach-ui uses CSS for essential styles
- @mui/base uses
useUtilityClasses
Use CSS #
- Ant design (less)
- Semanitc UI (less)
- carbon (SCSS)
- semi-design (SCSS)
- gestalt
- primereact
- pivotal-ui (SCSS)
- coreui-react
- agnosticui
Related #
- CSS-in-JS and Server Components
- Library Upgrade Guide: <style> (most CSS-in-JS libs)
- The Most Popular CSS-in-JS Libraries in 2023
- A Thorough Analysis of CSS-in-JS
- CSS in JS techniques comparison
- react-native-web/benchmarks
- Breaking Up with SVG-in-JS in 2023
ReactNative with Tailwind #
Read more: Component libraries trends, Styling components