DEV Community

loading...
Cover image for The tiniest CSS-in-JS solution for your open-source React components

The tiniest CSS-in-JS solution for your open-source React components

omgovich profile image Vlad Shilov ・4 min read

Hey there! My name is Vlad and I'm fascinated by the development of JavaScript micro-libraries. My primary projects at the moment are:

  • react-colorful — the tiniest and fastest color picker component for React and Preact apps (14 times lighter than react-color)
  • wouter — a minimalist-friendly 1.3KB routing solution for React and Preact
  • omgopass — a tiny 0.3 KB and ultra-fast memorable password generator (600 times faster than password-generator)

react-colorful is the only project that ships both JS and CSS. I want to share with you the challenges we encountered while developing it. You might find our experience useful in making your open-source React components lighter.

📦 Problem

Since its first release, react-colorful has included a CSS file that developers have to import:

import { HexColorPicker } from "react-colorful";
import "react-colorful/dist/index.css"; // 👈 this one
Enter fullscreen mode Exit fullscreen mode

In terms of bundle size, everything was super cool because we wrote all styles as CSS files that were then compressed and minified by a bundling tool.

It seemed like a good solution, but we've started receiving complaints from many developers that were unable to use the styles. It is quite common in the React ecosystem to use libraries like Emotion, Styled-Components, or other CSS-in-JS libraries exclusively and to entirely forgo the inclusion of a style loader. As such, usage of react-colorful would necessitate the alteration of build configurations for these users to be able to access the default styling.

Moreover, many component libraries and UI kits are CSS-in-JS exclusive, so the CSS-only approach makes it difficult for the picker to be compatible.

👩‍🎤 Why not just use an existing CSS-in-JS solution?

In my last post, I mentioned that react-colorful aims to be a zero-dependency package and we didn't want to change that by adding a CSS-in-JS framework, such as Emotion.

The entirety of react-colorful costs about 2 KB, but installing emotion would make the size of the package almost 7 times bigger. Of course, there are tiny CSS-in-JS libraries like goober that cost about 1KB, but we didn't feel like we need the entire library since our styles are not dynamic.

Installing an extra dependency on other developers' projects does not seem justified to us.

🗜 The tiniest solution

In order to achieve our goals, we decided to find a simple way of injecting styles into the page just like how CSS-in-JS libraries do that.

I use Microbundle to build my open-source projects and love it a lot. It's a zero-configuration bundler for tiny modules, powered by Rollup.

Most bundlers, Microbundle included, save processed styles to the disk as a CSS file. Because we wanted to have the styles in JS, we asked Jason Miller, the author of Microbundle (amongst many other notable projects), to provide us with a way to import processed CSS as a string into JavaScript and he generously added a new option to Microbundle:

// with the default external CSS:
import "./foo.css";  // generates a minified .css file in the output directory

// with `microbundle --css inline`:
import css from "./foo.css";
console.log(css);  // the generated minified stylesheet string
Enter fullscreen mode Exit fullscreen mode

It is not necessary to use Microbundle; you can use any build tool that you'd like, just make sure to read up on the docs to configure it properly.

Then we created a simple hook that creates a <style> tag containing the styles for our component. As soon as the component renders the first time, this hook appends the tag to <head>.

import { useLayoutEffect } from "react"
import styles from "../css/styles.css";

let styleElement;

const useStyleSheet = () => {
  useLayoutEffect(() => {
    if (typeof document !== "undefined" && !styleElement) {
      styleElement = document.head.appendChild(document.createElement("style"));
      styleElement.innerHTML = styles;
    }
  }, []);
};
Enter fullscreen mode Exit fullscreen mode

👍 Results

Since it no longer requires importing CSS separately, the usage of the react-colorful became much simpler. Now the component can be used with any CSS-in-JS project or component library.

The hook's gzipped size is about 150 bytes, and I suspect it's one of the smallest ways of shipping CSS for a React component.

❤️ Thanks for reading

We care about size, performance, and accessibility. I hope that the React community shares our values and will find react-colorful useful for further projects.

It will help us a lot if you star the repo on GitHub or share the link to this article with your friends 🙏

GitHub logo omgovich / react-colorful

🎨 A tiny (2,5 KB) color picker component for React and Preact apps

react-colorful is a tiny color picker component for React and Preact apps

Features

  • Small: Just 2,5 KB gzipped (14 times lighter than react-color).
  • Tree-shakeable: Only the parts you use will be imported into your app's bundle.
  • Fast: Built with hooks and functional components only.
  • Bulletproof: Written in strict TypeScript and covered by 40+ tests.
  • Simple: The interface is straight forward and easy to use.
  • Mobile-friendly: Works well on mobile devices and touch screens.
  • Accessible: Follows the WAI-ARIA guidelines to support users of assistive technologies.
  • No dependencies

Live demos

Install

npm install react-colorful --save

Usage

import { HexColorPicker } from "react-colorful";
const YourComponent = () => {
  const [color, setColor] = useState("#aabbcc");
  return <HexColorPicker color={color} onChange={setColor}
Enter fullscreen mode Exit fullscreen mode

Discussion (11)

pic
Editor guide
Collapse
fezvrasta profile image
Federico Zivolo • Edited

Why don't you simply render a <style/> tag? Emotion does the same and it works fine even on SSR.

Collapse
omgovich profile image
Vlad Shilov Author • Edited

Hi! What do you mean? By default Emotion adds <style data-emotion="css"> tags into the head. We do almost the same.

IMG

Collapse
fezvrasta profile image
Federico Zivolo • Edited

That only happens on development builds.

Collapse
shadowtime2000 profile image
shadowtime2000

That's a cool way to do this! As far as I can see, this would only work for client side and when SSRing the styles wouldn't be immediately shipped.

Collapse
omgovich profile image
Vlad Shilov Author

You are right. It is a drawback to this approach (and most other CSS-in-JS solutions). But in my case (a small color picker) it doesn't seem critical.

Collapse
vndre profile image
Andre

Honestly I feel this approach will insert many possible side effects to an app. One of the problems I have stumbled with bigger apps is css render blocking.

The team of Svelte did a similar approach: they insert all css in the head after Svelte finish loading, but sometimes it takes like a second to execute that code so all the app's layout loads naked and only then css is insert.

If a simple app (like an online color picker) visually depends on your package it would make more sense to have those styles already in the HTML file (critical css, SEO, SSR, etc). Now imagine if every package creator uses this approach, they will be a cascade of pending styles.

I feel we should let the package users decide the way they want their css to be handled. You could maybe export a function helper which loads the styles? (loadColorPickerCss())

Thread Thread
rschristian profile image
Ryan Christian

FWIW, react-colorful only ships 1.6kb of (minified) CSS. The amount of JS that would be needed to slow down this injection would be monumental, and even then, would only be noticed if this color picker was right at the top of the document.

I think that's searching for a solution before a problem has even appeared. The amount of JS we're talking about would already make the site really rough.

Thread Thread
vndre profile image
Andre

Yes, I know that for this specific library this is no issue. But a bigger app will probably use more packages, then we got the node_modules hell... Also not everyone creates packages with size and zero-deps in mind like you do.

Imagine an node app were my deps are: react-colorful (0 deps, 1kb css), react-slider (8 deps, 100kb css) and tailwindcss (64 deps, 3mb css). If those package creators all use the same method to inject css into the documents' head and react-colorful render happens deep in the tree the chain could be like: 1. tailwind injects code, browsers starts downloading 3mb of a css file... 2. react-slider does the same... 3. react-colorful does the same a little faster. In my fictional case there where only 3 main deps but you can see where a cascade of waiting requests could be created for slower connections or with more package deps...

Collapse
shadowtime2000 profile image
shadowtime2000

You could do it by making the CSS-in-JS-like solution optional so for people who SSR can just ship the styles separately. Though, yeah SSR may not matter as much in this case as it would in like a ui framework because a color picker a little more of a client side component instead of one that should be like fully crawlable.

Collapse
fkhadra profile image
Fadi Khadra • Edited

Nice one, I have the same thing with react-toastify and react-contexify. I wanted to provide a way for the users to load the style without importing the CSS.

I created a small CLI tool that generates a helper function to inject the minified css.
It's not perfect but it does the job really well, also it's not tied to react.

github.com/fkhadra/style2js

Edit: I've tried react-colorful, I'll definitely use it for the project I'm working on. Really neat 👌

Collapse
omgovich profile image
Vlad Shilov Author

Thanks for the kind words!
react-toastify is an awesome library!

The most difficult part of the migration to CSS-in-JS is a bundler configuration. I reached all of my goals by using Microbundle. Give it a try — I guess might cover your needs.