Making a tiny zero-cost CSS-in-JS solution for React components (and apps)
Tags: javascript, react, css
Published: 2020-11-13 03:48:41

I've been extensively using [Emotion](https://emotion.sh/docs/introduction) on a large project at work and warmed to the idea of CSS-in-JS. However I've always been skeptical of a few of things:

1. Is the performance really as "blazing fast" as they claim?
2. Do I want to add 28KB min to my very lightweight component library?
3. Is it really such a good idea to litter CSS with props and logic, or can the same thing be achieved with conditional styles, classes and CSS Variables in a much more performant and logic-less fashion?
4. Is using dynamically generated class names really that great for a component library which people might want to style themselves?

Let's address performance. At first the cost of [Emotion](https://emotion.sh/docs/introduction) was unnoticeable but as the product grew we started to notice there was significant lag in our [Storybook](https://storybook.js.org/). The page displaying all the Button variations for example can freeze for up to 10 seconds now. I checked Chrome Devtools and 90% of that time is spent in [Emotion](https://emotion.sh/docs/introduction) parsing functions… I have yet to do a full analysis on the actual app but a cursory check revealed that on the development build at least, about 20% of scripting time is spent in [Emotion](https://emotion.sh/docs/introduction) functions, and I know for a fact that there is virtually no compile time optimization done and it's only React that gets faster in production builds (e.g. faster hooks that aren't called twice). What's more is that [Emotion](https://emotion.sh/docs/introduction) uses a context Consumer in every component, and wraps components which use styled in HOCs.

Especially when developing re-usable components it makes sense to make them as small and efficient as possible. At the same time asking your users to import CSS is a bit of a drag, and looses the convenience of being able to theme with JS Objects.

Well it turns out you can build a zero-cost CSS-in-JS solution with theming if you're ok with ditching logic in CSS and hashed class names (in favour of something like [BEM](http://getbem.com/naming/)). You can even keep the nice syntax highlighting using the [same plugins](https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components) as you would for [Emotion](https://emotion.sh/docs/introduction) or [Styled Components](https://styled-components.com/).

Creating the solution involves 3 things:

1. A `useStyles` hook for inserting css into the DOM.
2. A `useTheme` hook used once in your project for setting and updating [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
3. A `css` template literal function that doesn't do anything except give you the same syntax highlighting as when using [Emotion](https://emotion.sh/docs/introduction) or [Styled Components](https://styled-components.com/).

## The useStyles hook

```ts
import { useLayoutEffect } from 'react';

const styles = new Map<string, HTMLStyleElement>();

export function useStyle(uid: string, rules: string) {
  useLayoutEffect(() => {
    if (styles.get(uid)) {
      return;
    }

    const style = document.createElement('style');
    style.innerHTML = rules;
    style.setAttribute('id', uid);
    document.head.appendChild(style);
    styles.set(uid, style);

    return () => {
      if (style && document.head.contains(style)) {
        document.head.removeChild(style);
        styles.delete(uid);
      }
    };
  }, [uid, rules]);
}

```

We simply take css (as a string) and insert it into a style tag. In this case `useStyle` is decorative, we're not actually using hooks but it feels more natural to hook users.

## The useTheme hook

We'll want to use CSS Variables for theming and we also want our users to be able to pass a theme JS Object for convenience.

```ts
import { useLayoutEffect } from 'react';

type AnyTheme = Record<string, string>;

function makeCssTheme<T = AnyTheme>(prefix: string, theme: T) {
  return Object.keys(theme).reduce((acc, key) => {
    const value = theme[key as keyof T];
    if (value) {
      return acc + `${`--${prefix}-${key}`}: ${value};\n`;
    }
    return acc;
  }, '');
}

export function useTheme<T = AnyTheme>(prefix: string, theme: T, selector = ':root') {
  useLayoutEffect(() => {
    const style = document.createElement('style');
    const cssTheme = makeCssTheme(prefix, theme);

    style.setAttribute('id', `${prefix}-theme`);
    style.setAttribute('data-selector', selector);
    style.innerHTML = `
        ${selector} {
          ${cssTheme}
        }
      `;

    document.head.appendChild(style);

    return () => {
      if (style && document.head.contains(style)) {
        document.head.removeChild(style);
      }
    };
  }, [prefix, theme, selector]);
}
```
See the example below for how it's used.

## css template literal

Finally we want a `css` template literal function purely for syntax highlighting. It just smooshes your template string (which could have variables) into one string.

```ts
export const css = (strings: TemplateStringsArray, ...args: unknown[]) =>
  strings.reduce(
    (acc, string, index) => acc + string + (index < args.length ? args[index] : ''),
    ''
  );
```

See the example below for how it's used.

## Putting it all together

You now have a super fast and lightweight solution for CSS-in-JS! Let's see how it all fits together:

```tsx
import * as React from 'react';
import { useTheme, useStyle, css } from 'aneto';

const defaultTheme = {
  appFont: 'sans-serif',
  buttonBg: 'red',
  buttonPadding: '10px',
  buttonPaddingSmall: '5px',
};

export function App({ theme = defaultTheme }) {
  useTheme('xx', theme);
  useStyle('app', componentStyles);

  return (
    <div className="app">
      <Button size="small">Some button</Button>
    </div>
  );
}

const componentStyles = css`
  .app {
    height: 100%;
    font-family: var(--xx-appFont);
  }
`;
```

And a component:

```tsx
import * as React from 'react';
import { useStyle, css } from 'aneto';

export function Button({ size = 'normal', children, ...attrs }) {
  useStyle('button', componentStyles);

  return (
    <button className={`button button--${size}`} {...attrs}>
      {children}
    </button>
  );
}

const componentStyles = css`
  .button {
    background: var(--xx-buttonBg);
    padding: var(--xx-buttonPadding);
  }
  .button--small {
    padding: var(--xx-buttonPaddingSmall);
  }
`;
```

Runnable example: https://codesandbox.io/s/simple-zero-cost-css-in-js-example-cifhi

NPM package: https://www.npmjs.com/package/aneto
GitHub: https://github.com/DominicTobias/aneto

Full featured alternatives for compile time CSS-in-JS:
https://github.com/callstack/linaria
https://github.com/atlassian-labs/compiled