lee.david.cs

Exploring CSS in JS

February 20, 2021

Disclaimer: This post reflects my exploration of and personal experiences with CSS-in-JS as a React TypeScript developer.

CSS-in-JS (or CiJ) refers to the technique of using JavaScript to write styles to generate CSS in a declarative and maintainable way.

These CiJ libraries aim to solve a variety of limitations of traditional CSS by providing benefits such as:

  1. Style-encapsulation (non-global selectors)
  2. Automatic-vendor prefixing (better browser vendor support)
  3. Benefits of the JavaScript / TypeScript ecosystem
  4. Integrations with React (some CiJ libraries)

This mostly comes at a performance trade-off since your styles are now computed at runtime, may possibly block the initial paint of your components, and will increase your client's JavaScript bundle.

There are numerous CSS-in-JS options to select from. Some may work better for you than others depending on your preferences and use-case.

Below are some of the things that factored into my decisions on CSS-in-JS.

The performance concern

There is a very real performance hit to the mounting and re-render times of React components when using CSS-in-JS versus not using any.

Example taken from A-gambit's CSS-IN-JS-BENCHMARKS

SolutionUse CSSUse Inline-StylesMount Time (ms)Rerender time (ms)
react (without styles)--17.2539.11
react (with inline-styles)-+32.851.68
styled-jsx-inline-styles++40.6754.32
emotion-css-mode++40.254.91
cxs++39.8755.28
react-css++39.355.39
aphrodite++42.2555.48
glam-inline-style++41.7557.22
glam-simple+-42.6772.69
merge-styles+-59.275.77
styled-components-inline-styles++97.678.03
styled-jss-w-o-plugins-v2+-119.978.11
styled-jss-w-o-plugins+-122.678.53
rockey-inline++98.5379.01
fela+-75.5284.82
styletron+-98.685.69
radium-+99.4789.19
styled-jss-v2+-165.491.07
styled-jss+-166.0593.51
emotion-extract-static++126.3393.66
emotion-decouple+-142.8596.36
emotion-simple+-138.0896.94
styled-jsx-dynamic+-155.1122.89
react-native-web++238.72140.16
styled-components+-182146.84
styled-components-decouple-cell+-213.53152.39
rockey-speedy+-114.3187.27
rockey+-213.32274.72
glamorous+-205.7283.68
react-jss+-198.97297.74
glamorous-glamour-css+-278.7407.91

sorted by rerender time

Looking at these benchmarks, you can see that Styled-Components and JSS, two very popular options within CiJ, are seeing increases of around x10 in the time it takes to mount/re-render compared to React without styles.

However, while the increases in times appear significant, I haven't felt that sites such as Reddit, AirBnB or Github, which all use Styled-Components, to feel less-usable as a result of increased render times.

Auto-completion + TypeScript support

I value these as a TypeScript developer, so if a library has lackluster intellisense, it will rank lower on my preferences.

Many well-maintained CiJ libraries ship with TypeScript definitions (examples: Styled-Components, JSS, Emotion, Aphrodite). So any use of styled will produce components with correctly typed props and any className generating APIs (e.g. css, createUseStyles, etc.) will return strings to be usable with your components' className props.

Example of Styled-Components

1import React from "react";
2import { styled } from "styled-components";
3
4export interface ButtonProps {
5 size: "small" | "large";
6}
7
8export const Button = styled.button<ButtonProps>`
9 width: ${({ size }) => {
10 switch (size) {
11 case "small":
12 return "80px";
13 default:
14 return "200px";
15 }
16 }}
17`;
18
19// Prop types are the union of HTMLButtonElement props and ButtonProps
20Button.defaultProps = {
21 size: "small"
22};

Example of React-JSS

1import { Theme } from "./theme";
2import clsx from "clsx";
3import React, { ButtonHTMLAttributes, DetailedHTMLProps, FC } from "react";
4import { createUseStyles } from "react-jss";
5
6export interface ButtonProps extends
7 DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
8 size: "small" | "large";
9}
10
11const styles = (theme: Theme) => ({
12 root: {
13 /* Unfortunately, untyped props from JSS */
14 width: ({ size }) => {
15 switch (size) {
16 case "small":
17 return "80px";
18 default:
19 return "200px";
20 }
21 }
22 }
23});
24
25const useStyles = createUseStyles<Theme, keyof ReturnType<typeof styles>>(styles);
26
27const Button: FC<ButtonProps> = ({ className, size, ...restButtonProps }) => {
28 /* classes will be typed with all class names */
29 const classes = useStyles({ size });
30
31 return <button {...restButtonProps} className={clsx(classes.root, className)} />;
32};

The styled syntax makes use of tagged template literals so that developers can write real css syntax, with interpolated values and functions in JavaScript. Since you're writing styles in a string, you may not have syntax highlighting or auto-completion in your IDE.

Thankfully, if you use VSCode like me, there is a bit of set-up you can do to get syntax highlighting and auto-completion working for your styled and css template literals, regardless of which CiJ you're using (as long as they are using this style of tagged template literals).

  1. Inside VSCode, press Ctrl + P, and enter ext install vscode-styled-components
  2. Install typescript-styled-plugin
1npm install --save-dev typescript-styled-plugin
  1. Add a plugins section to your tsconfig.json
1{
2 "compilerOptions": {
3 "plugins": [
4 {
5 "name": "typescript-styled-plugin"
6 }
7 ]
8 }
9}

Real CSS vs CSS as JSON

Simply put, being able to write real CSS is nice if you've already learned CSS or SCSS. In addition, we saw above that being able to write real CSS allows us to leverage IDE tooling such as syntax-highlighting and auto-completion from our IDE.

While CSS as JSON relieves you from needing to learn the syntax for CSS, you miss out on the benefits above which loses out on some DX.

However, it is not unusual to want to compose a CSS fragment expressed as JSON. Maybe the CSS fragment was derived from a JavaScript function such as those from polished.js.

So being able to do both is a huge benefit to DX.

Example of Styled-Components

1import { styled } from "styled-components";
2
3const borderStylesFragment = {
4 border: "1px solid black";
5 borderRadius: 4
6};
7
8export const Button = styled.button`
9 font-size: 0.875rem;
10 color: green;
11
12 ${borderStylesFragment};
13`;

Styled API or className?

Disclaimer: This section is purely my own preference, and something I do in my own projects. It's much simpler to adhere to a single convention in any shared project with multiple developers than do what I am about to suggest here:

There are many benefits with using the styled API. The Styled-Components docs list many of the benefits there.

Take these examples of using styled vs className:

Example of Styled-Components

1import { styled } from "styled-components";
2
3export const Button = styled.button`
4 background-color: blue;
5`;

Example of cxs

1import clsx from "clsx";
2import { cxs } from "cxs";
3import React, { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react";
4
5const classes = {
6 root: cxs({
7 backgroundColor: "blue"
8 })
9};
10
11export const Button = forwardRef<
12 HTMLButtonElement,
13 DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
14>(({ className, ...restButtonProps }, ref) => {
15 return (
16 <button
17 {...restButtonProps}
18 ref={ref}
19 className={clsx(classes.root, className)}
20 />;
21});
22
23Button.displayName = "Button";

In this simple example, the styled example certainly looks much more attractive than the className example, as it's much more declarative.

However, I like using a mix of styled and className in my own projects. So I like selecting a library that gives me the flexibility to do either or both.

Example using all styled components

1/* ... */
2
3const CustomerDataTable: FC = ({ onClose }) => {
4 /* ... */
5
6 return (
7 <Wrapper>
8 <HeaderWrapper>
9 <TitleWrapper>
10 <Typography as="h1">Customers</Typography>
11 </TitleWrapper>
12 <CloseButton onClick={onClose}>Close</CloseButton>
13 </HeaderWrapper>
14 <DataTableWrapper>
15 <DataTable customers={customers} />
16 </DataTableWrapper>
17 <ActionsWrapper>
18 <DeleteButton>Delete</DeleteButton>
19 <EditButton>Edit</EditButton>
20 </ActionsWrapper>
21 </Wrapper>
22 );
23};

Example using a mix of styled components and classNames

1/* ... */
2
3const CustomerDataTable: FC = ({ onClose }) => {
4 /* ... */
5
6 return (
7 <div className={classes.wrapper}>
8 <div className={classes.headerWrapper}>
9 <div className={classes.titleWrapper}>
10 <Typography as="h1">Customers</Typography>
11 </div>
12 <Button className={classes.closeButton}>Close</Button>
13 </div>
14 <div className={classes.dataTableWrapper}>
15 <DataTable customers={customers} />
16 </div>
17 <div className={classes.actionsWrapper}>
18 <Button className={classes.deleteButton}>Delete</Button>
19 <Button className={classes.editButton}>Edit</Button>
20 </div>
21 </div>
22 );
23};

In shared projects, I would stick to the first example of using all shared components, because it is easier to create a convention to adhere to. But in my own projects, I tend to prefer the second, mixed example because I find it easier to parse for myself.

Any JSX.IntrinsicElement such as div with a className can be assumed to be single-use and non-complex. Anything else is either a reusable atom, molecule or organism from my atomic design system, and may or may not be complex but warrants deeper understanding than a JSX.IntrinsicElement.

It also helps with parsability that my IDE's theme will color JSX.IntrinsicElements a different color than custom components from my atomic design system.

Final Decision?

There are other factors I considered for which CiJ library I decided to use in my projects, such as:

  1. Ease of setting up
  2. Documentation quality
  3. SSR support
  4. Dynamic styles with React props
  5. How responsive the maintainers are
  6. etc...

In the end, I've selected Linaria for my projects for the reasons below:

  1. Styles are transpiled at build-time, allowing your browser to evaluate it in parallel to your JavaScript as vanilla CSS.
  2. Provides a styled and css (className) API, with TypeScript support, syntax highlighting and auto-completion.
  3. Dynamic styles using React component props (dynamic styles evaluate to CSS variables)
  4. SSR support (styles are largely just vanilla CSS after all)
  5. Ease of setting up with next-linaria (to use with Next.js)

Some cons to my decision include:

  1. Lack of CSS fragment composition / JSON interpolation
  2. Infrequent library updates (maintainers may not be the most responsive?)
  3. Mediocre documentation, but simple-enough API

Linaria works really well for my preferred DX and use-cases.

There's no objective "best" tool. As long as your projects' styles are more maintainable and your developers are generally content with the DX, that's a winning decision.

Happy coding! -- David Lee