I’ve been building a headless React component library with my team, and we had to pick a way to style our components. We evaluated several options, including Styled Components, Tailwind, and Vanilla Extract. We ended up going with static CSS, here’s why.
CSS-in-JS is slow
Our first requirement was that we didn’t want to use CSS-in-JS, because it’s slow. CSS-in-JS requires runtime Javascript, while static CSS does not. This article has in depth stats, I also did my own quick test below.
There are other drawbacks to libraries like Emotion and Styled Components.
- Complex nested selectors get unwieldy
- Your library ships with default styling, even if the consumer doesn’t want it
- More dependencies = bigger bundle size
- Dynamic theme switching and variations can be difficult
This led us to choose Vanilla Extract.
Vanilla Extract – no runtime static CSS extraction
Vanilla Extract gives you static CSS files with themeing, variables, typescript support, and more. We started building out our core component library using Vanilla, and we found pros and cons.
What I liked:
- Typescript support
- Great handling of themes and variables
- Great handling of variations
- Easily handle responsive styles
- Simple output of class names and static CSS files
What I didn’t like:
- No colocation (you have to use separate css.ts files, can’t have styling in your component file)
- Had a bug on my M1 mac where compile times were super slow (I’m sure they’ll fix this)
- Doesn’t work well with a library in a monorepo, where you have css.ts files in separate packages imported to components (there’s probably a way around this, but it wasn’t worth the effort for us)
- Docs are really thin, we had to figure out how to do a lot of stuff on our own
- Really verbose to do something simple like add a single style (see below)
- It felt like we had to do a ton of work up front to use static CSS, eventually it was easier to just use static CSS
Here’s an example of what you need to do to add a single style, like display: flex. Write a sprinkles.css.ts file, and include display properties. Import that into your component, then write this craziness:
<div className={sprinkles({display: 'flex'})}>I'm flexing</div>
That’s a lot of code just to add a single CSS style, when something like Tailwind is: className=”flex”.
Even with the things I didn’t like, if you want static CSS extraction with Typescript support, Vanilla Extract is still your best bet.
Why we went with static CSS
There were too many hangups with any CSS framework, so we decided to roll our own solution. Starting with static CSS allows us to progressively enhance using build tools like PostCSS and CSS variables. We can start with a performant, native web technology and build from there.
You’d be surprised how much you can accomplish with this approach. For example, using a VS Code plugin called CSS Variable Autocomplete, you get the equivalent of intellisense.
We use data attributes for presentational styling, and PostCSS allows you to write nested CSS without a tool like Saas.
In the example above, the consumer writes <Button size=”small”> and we change that to a data-attribute in our component.
Here are some of the things we liked about this approach:
- Themeing and dark mode works great by just changing CSS variables and swapping top level class names.
- It also kept our markup and internal styling logic simple, and our library more lightweight, because we don’t have CSSProperties everywhere.
- Components can be used without the styles, just by not importing them. This keeps the components lightweight and reusable anywhere (for example React Native, which doesn’t use CSS).
- Base components use a className from a static stylesheet. Customization is easy by merging user classNames or even allowing a style prop. This allows for maximum flexibility.
Some of the other things we are doing is using BEM for class names, enforcing accessibility with aria attribute selectors where possible, and using CSS variables everywhere to make themeing easy.
Our library won’t ship with any dependencies, so if the consumer wants to use Tailwind, Styled Components, or Vanilla Extract, they are free to do that.
I am loving this approach because it starts simple and performant, and we only add what we really need.