DEV Community

loading...

Why I left CSS-in-JS and returned to good old CSS preprocessors

Aleksei Berezkin
Fullstack dev: Java, JS, TS, React
Updated on ・7 min read

Edited on May 30, 2021. Added information about CSS vars kindly suggested by Junk.

I used to be a big fan of CSS-in-JS (JSS), but now I'm back to preprocessed CSS. Regression? Technophobia? Or justified choice? Let me explain.

1. Problems JSS solves

First, JSS isn't just a proof-of-concept: it solves two hard problems. Literally “solves”, not just provides the means to mitigate them.

1.1. Module-scoped CSS

CSS is global by nature. Importing a CSS file into a module may seem like it's module-scoped, but in fact it's not.

A.css

.a {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

A.jsx

import './A.css'
function A() {
  return <span class='b'>Hi</span>
}
Enter fullscreen mode Exit fullscreen mode

Do you see the problem?

Answer A.jsx uses b class not even mentioned in A.css.

With JSS and TypeScript that kind of error is not even possible:

A.tsx

const useStyles = createUseStyles({
  a: {
    color: 'red';
  }
})

function A() {
  const styles = useStyles()
  return <span class={styles.b}>Hi</span>
}
Enter fullscreen mode Exit fullscreen mode

A.tsx won't compile.

1.2. Sharing variables between CSS and JS

One possible non-JSS solution is css-modules supported in css-loader which require some setup. For new browsers there are CSS custom properties which work together with getComputedStyle.

With JSS things are as simple as possible: you just have normal JS variable — use it however you want!

const itemHeight = 72
const useStyles = createUseStyles({
  item: {
    height: itemHeight,
  },
})

function Showcase({items, topItem}) {
  const styles = useStyles()

  return <div style={{translateY: -itemHeight * topItem}}>
    {
      items.map(item =>
        <div class={styles.item}>{item}</div>
      )
    }
  </div>
}
Enter fullscreen mode Exit fullscreen mode

2. The price

2.1. Performance penalty

Bundle overhead is 33 kB minified for styled-components and 61 kB minified for react-jss. There is also a runtime overhead, which is not argued even by libs authors.

2.2. Dev experience is actually worse

Editors know CSS. They offer syntax highlight, code completion and other helping services. With JSS you miss much of them because IDE sees no more than JS object.

const styles = createUseStyles({
  btn: {
    border: '1px dark gray',
    boxSizing: 'border',
    padding: '4px 12px',
    whiteSpace: 'nowrap',
  },
});
Enter fullscreen mode Exit fullscreen mode

Looks boring and error-friendly. Btw, did you spot one?

Answer

Color must be darkgray, not dark gray. IDE won't help; but, with CSS, it would.

Styled-components syntax is yet worse IMO:

const Btn = styled.button`
    border: 1px dark gray;
    boxSizing: border;
    padding: 0 12px 6px;
    whiteSpace: nowrap;
`
Enter fullscreen mode Exit fullscreen mode

2.3. Libs may contain frustrating bugs

See for example this one. Sometimes this simple query doesn't work:

const styles = createUseStyles({
  item: ({param}) => ({
    '@media (min-width: 320px)': {
      // ...
    },
  }),
})
Enter fullscreen mode Exit fullscreen mode

An issue is 1 year old; it's trivial usage, not a corner case, yet it's still open making devs suffer. What a shame!

3. So does JSS worth it?

I understand that picking a technology is a question of tradeoffs; someone may find pros outweigh cons. Personally I doubt JSS worths performance and dev experience.

But how to live without JSS? Let's look at some popular options.

3.1. CSS modules

CSS modules also generate class names, but, unlike JSS, they do it in compile time allocating no runtime overhead. Assuming you configured everything correctly, it goes like this:

Showcase.css

.showcase {
  display: flex;
}
.item {
  width: 33%;
}
.highlighted {
  background-color: lightgray;
}
Enter fullscreen mode Exit fullscreen mode

Showcase.css.d.td (generated)

export const showcase: string
export const item: string
export const highlighted: string
Enter fullscreen mode Exit fullscreen mode

Showcase.tsx

import styles from './Showcase.css'

type Props = {items: string[], highlighted: number}

function Showcase({items, highlighted}: Props) {
  return <div className={styles.showcase}>{
    items.map((item, i) => {
      const c = `${styles.item} ${i===highlighted ? styles.highlighted : ''}`
      return <div className={c}>{item}</div>
    })
  }</div>
}
Enter fullscreen mode Exit fullscreen mode

That looks nice! It has benefits of JSS but with runtime penalties removed. However, as you see, there are type definitions generated, so for smooth develop process you need to make proper setup, and to have your dev server always running while writing a code. Of course that discounts dev experience.

3.2. BEM

BEM is perhaps the best known CSS classes naming convention. Though full spec may seem elaborated, it's essence is quite simple:

  • BEM stands for “Block, Element, Modifier”
  • Block is a top-level DOM element in the component
    • Block names must be unique within a project
  • Element is something inside a block
    • Element name is block__element
  • Modifier is a class that tweaks a block or an element
    • Block modifier name is block_modifier
    • Element modifier name is block__element_modifier

With CSS preprocessors and JS classes prefixers you don't need to repeat names constantly:

Showcase.scss

.showcase {
  display: flex;
  &__item {
    width: 33%;
    &_highlighted {
      background-color: lightgray;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Showcase.jsx

import './Showcase.scss';
import {withNaming} from '@bem-react/classname';

const cn = withNaming({e: '__', m: '_', v: '_' })

const showcaseCn = cn('showcase');
const itemCn = cn('showcase', 'item')

function Showcase({items, highlighted}) {
  return <div className={showcaseCn()}>{
    items.map((item, i) => {
      const c = itemCn({highlighted: i===p.highlighted})
      return <div className={c}>{item}</div>
    })
  }</div>
}


Enter fullscreen mode Exit fullscreen mode

Can BEM classes be simplified?

I appreciate BEM but using prefixers or long names seems verbose to me. What if we replace them with CSS combinators? Let's give it a try:

Showcase.scss

.b-showcase {
  display: flex;
  >.item {
    width: 33%;
    &.highlighted {
      background-color: lightgray;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Showcase.jsx

import './Showcase.scss';

function Showcase({items, highlighted}) {
  return <div className='b-showcase'>{
    items.map((item, i) => {
      const c = `item ${i===p.highlighted ? 'highlighted' : ''}`
      return <div className={c}>{item}</div>
    })
  }</div>
}
Enter fullscreen mode Exit fullscreen mode

IMO that looks more natural. Notes:

  • b- prefix is needed to avoid clashes with non-block names
  • Descendant combinator is not used because it may unexpectedly select an element from nested block
  • When element depth is unknown, you may fallback to BEM
  • In very large apps child selectors may work somewhat slower than simple BEM classes; on the other hand, you save some runtime not using prefixers

How to make sure block classes are unique in large apps?

That's perhaps the hardest part of BEM. However, with the help of scss-parser it's possible to write a program (or webpack plugin) that parses and validates SCSS files.

Validate.ts (simplified)

import {parse} from 'scss-parser'

const clsToFile = new Map<string, string>()
for await (const file of walkDir(__dirname)) {
  const cn = getTopLevelClass(String(await fs.promises.readFile(file)))
  if (!cn) {
    throw new Error(`No top level class: ${file}`)
  }
  if (clsToFile.has(cn)) {
    throw new Error(`Duplicate class '${cn}' in ${clsToFile.get(cn)} and ${file}` )
  }
  clsToFile.set(cn, file)
}

// Walks a dir recursively yielding SCSS files
async function* walkDir(dir: string): AsyncGenerator<string> {
  // ...
}

// Returns top-level class if there is one
function getTopLevelClass(scss: string) {
  const ast = parse(scss)
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Full Validate.ts
import {parse, Node} from 'scss-parser'
import fs from 'fs'
import path from 'path'

main()

main() {
  const clsToFile = new Map<string, string>()
  for await (const file of walkDir(__dirname)) {
    const cn = getTopLevelClass(String(await fs.promises.readFile(file)))
    if (!cn) {
      throw new Error(`No top level class: ${file}`)
    }
    if (clsToFile.has(cn)) {
      throw new Error(`Duplicate class '${cn}' in ${clsToFile.get(cn)} and ${file}` )
    }
    clsToFile.set(cn, file)
  }
}

async function* walkDir(dir: string): AsyncGenerator<string> {
  const entries = await fs.promises.readdir(dir, {withFileTypes: true})
  for (const e of entries) {
    const file = path.resolve(dir, e.name)
    if (e.isFile() && /\.scss$/.exec(e.name)) {
      yield file
    } else if (e.isDirectory()) {
      yield* walkDir(file)
    }
  }
}

function getTopLevelClass(scss: string) {
  const ast = parse(scss)
  if (Array.isArray(ast.value)) {
    const topLevelClasses = ast.value
      .filter(node => node.type === 'rule')
      .flatMap(ruleNode => ruleNode.value as Node[])
      .filter(node => node.type === 'selector')
      .flatMap(selectorNode => selectorNode.value as Node[])
      .filter(node => node.type === 'class')
      .flatMap(classNode => classNode.value as Node[])
      .filter(node => node.type === 'identifier')
      .map(identifierNode => identifierNode.value as string);
    if (topLevelClasses.length === 1) {
      return topLevelClasses[0];
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

What with variables sharing?

It's not that straightforward, but there are options:

If these doesn't fit your needs you may introduce some naming conventions:

Showcase.scss

$shared-pad-size: 6px;

.showcase {
  padding: $pad-size;
  // ..
}
Enter fullscreen mode Exit fullscreen mode

Showcase.jsx

const sharedPadSize = 6;

export function Showcase() {
   // ...
}
Enter fullscreen mode Exit fullscreen mode

3.3. Tailwind CSS

Honestly, I don't like it, but it's not possible not mentioning it speaking of CSS in 2021. It's controversal. Devs not only argue about it, but also about the way of giving it a critique. That's fun but I'd stay aside 😉

3.4. Web components

It's a completely different world. It's not new yet not fully supported by all major browsers. Perhaps it's the future mainstream, who knows 🙂

4. So finally... What to choose?

It's tough. There's no silver bullet, there are compromises and tradeoffs. I prefer BEM-without-concatenations or just BEM. And you?

Discussion (17)

Collapse
violet profile image
Elena

You can master css and you can use it for your entire career, or learn every 2-3 years a new css framework/library like bootstrap, tailwind, css-in-js, sass, etc, and not actually know much css.

I'm not saying, don't learn css-in-js or the flavor of the month css preprocessor, but it's important to understand the basics and how css works in its vanilla form.

Collapse
alekseiberezkin profile image
Aleksei Berezkin Author

Thanks for your point. You are right, I’m not css expert and don’t plan to become one: I’m fullstack, and my job is shipping the product as a whole thing leaving hard parts to experts. Getting all parts together is one of my tasks; i.e., I can make the project setup, implement mainstream logic then invite narrow specialists to complete the thing. And to get everything and everyone work together some sensible arrangement needed. When I ask CSS engineer to style some fancy button I must give them reliable and convenient environment so they don’t think about anything unrelated, like “how to not to break other button”. That’s the place where frameworks are our friends. Good choice may facilitate other people’s work. And that’s why it’s important not only having good CSS (JS, any other code) but also the way it’s used in a project.

Collapse
violet profile image
Elena

When I ask CSS engineer to style some fancy button I must give them reliable and convenient environment so they don’t think about anything unrelated, like “how to not to break other button”.

Then there's even a greater need to separate the domains, as in keep the css separate in different files and the js in its own file and structure, and then html completely separate in a different folder.

Now with react, css-in-js, it's just a soup of react js code with classes and html inside of the class with css at the top, all mixed together. This makes it even harder to specialized people to just change the files they need without touching lots of unneeded files.

Thread Thread
alekseiberezkin profile image
Aleksei Berezkin Author

Well, “separating domains” is a bit arguable 😉 Do you separate domains or technologies? Component-oriented approach suggest separating components, that is, functional parts of an app. Discussions are endless, so I'm not going to proceed. More important IMO is that these days you won't see anywhere pure (isolated) HTML and CSS.

Thread Thread
violet profile image
Elena

So domains would be the structure domain where you use HTML, the styling domain where you use CSS and the logic/behavior domain where you use Javascript.

The domain separation is one of the key principles of good programming, in the same manner as MVC is, where you separate the model, view and controller.

At another level you could further separate the application into components, but I think these components should also be placed in their separate domain, as in keep the css styling separate from the html structure of the component and the javascript logic code of the component.

Thread Thread
alekseiberezkin profile image
Aleksei Berezkin Author

So do you mean whoever uses React is completely wrong?

Thread Thread
violet profile image
Elena

This is not something specific to React, but to the way your write code. Even with React, you can separate your render function into a different file, so that would be your HTML only, your view.

Then you can simply use CSS in its vanilla format, and have a couple of css files to build your styles, and then merge them together with webpack.

And as for the js logic, that would be kept into the main component class and that would be separate from the html template or the css files.

Collapse
oenonono profile image
Junk • Edited

CSS variables are readable and writable with script, which takes care of sharing. Shadow DOM provides scoping that is better than any build time workarounds like JSS or CSS modules, because it prevents leaks inside or outside.

Also, web components are supported in all major browsers that are still maintained. The only browser that doesn't have them is the browser that hasn't had ANYTHING new for ~7 years, Internet Explorer. And there are polyfills available.

itnext.io/theres-no-need-to-hate-w...

Collapse
alekseiberezkin profile image
Aleksei Berezkin Author • Edited

Thanks for your comment. So why do you think Web Components are not a mainstream yet?
(EDIT: updated a post with CSS vars)

Collapse
oenonono profile image
Junk • Edited

They're used by Google, Apple, Microsoft, GitHub, Salesforce, ING Bank, Atlassian, and more. Giant software companies and banks? That's mainstream to me. In part. The other part is how popular.

They're not exactly popular. They may never be really popular in themselves due to the shift toward framework and away from platform level development. They may only become popular under the hood of or via wide compatibility with frameworks and libraries. They may actually already have this type of popularity, but it depends on what and how you're counting.

My opinion is that the community acceptance of web components has seemed to go worse than most successful standards. I think the above is part of why.

I also think there are other reasons. The evangelists got discouraged; when they first did the marketing push it was a bit too early and they had to deal with a lot of hostility about web components. For a while after that there were technical annoyances (like Custom Elements relied on native JS class support and Shadow DOM polyfills had challenging limitations) and as soon as someone senior got that far, they saw how much other standards were needed to realize the full potential (such as AOM, constructable stylesheets, participation). Worst of all, there has been a huge industry "pendulum swing" away from valuing web standards as people who have never experienced a world without a web or the browser wars began to outnumber those who had (also, of course, $$ monetization opportunities of walled gardens). I also can't ignore the massive impact of the fact that the most popular UI library is still uniquely incompatible with web components. Other frameworks and libraries are doing okay with compatibility and many are experimenting with the additional potential. But the most popular one is not, which has disproportionate impact on overall web component popularity.

But despite all that, Shadow DOM is now viable for CSS scoping for some use cases (design system libraries, ads, and some others).

Thread Thread
alekseiberezkin profile image
Aleksei Berezkin Author

Thanks for such an insightful comment essay 🙂 I completely agree that Web Components will benefit a lot from deep integration with frameworks. For the moment seems there's little to no interest from frameworks, like if they don't see any value of incorporating Web Components, i.e. they don't see how frameworks will benefit from that. Do they miss something? What are your thoughts?

Thread Thread
oenonono profile image
Junk • Edited

Sure: that's not true enough. I mentioned this aspect near the end. There are only two frameworks whose maintainers have made official anti-web-component statements, as I recall. There is only one framework that is intransigent about supporting web components.

  • Check out custom-elements-everywhere.com/
  • Angular 2.x did its own sorta-polyfill of a pseudo Shadow DOM and initially did take some intentional inspiration from web components. Then got super into Typescript and functional reactive. But under the hood, modern Angular's ViewEncapsulation is driven by either that psuedo-Shadow-DOM implementation or by native Shadow DOM, depending on settings.
  • Polymer, Lit, and Stencil are based on web components. All of them are fascinating projects in different ways, IMO.
  • Despite the infamous blog post, Svelte has made the effort to be a pretty good tool for authoring web components, although it doesn't use them by default for components. Still, parts of its API are based on them. (What the post doesn't sufficiently acknowledge is that some of the issues are only issues if you are insistent on having a JS-first-and-focused DX. That's factually not how the web works. Chasing that DX is a major reason for startup performance issues in most existing frameworks and for their issues with accessibility and SEO. The industry didn't wait for those problems to be solved before jumping onto the framework bandwagon; it took years to solve them to some extent and they're still issues today. Confirmation bias is a hell of a drug. Web components aren't unique in some of their JS-design-caused issues, they have some of those issues because of they're not entirely HTML. They still embrace HTML more than many other solutions. You're creating an HTML-or-DOM interface, just like standard HTML has. IMO, the pragmatic and reasonable choice by the standards to focus on the JS interfaces has nevertheless hurt their potential in this respect.)
  • Most pre-existing frameworks will probably need to await features that (last I checked) are called "declarative Shadow DOM" and "constructable stylesheets" for it to be sufficiently desirable to disrupt their design to production-ize a web component foundation. You see that more in new tools than old ones with a lot of prior investment. There are experiments being done and discussed between open source maintainers and standards people about web components. I don't know exactly how or how much, because not everything in standards or open source is actually entirely out in public, sadly. But I've at least seen some productive threads on Twitter and GitHub with those kinds of conversations and participants, for example. That's interest. But also, most of them (especially the major ones) allow you to create and integrate web components already. They didn't make that effort for no reason. They did it because they acknowledge the value of producing and integrating web components. That is interest by definition, clearly expressed by hours of work.
  • Application frameworks for UIs ain't shit without UI libraries. And UI libraries based on web components are already prolific and on the rise. Those are a perfect example of the web component value proposition and is something the current state of the standards supports well enough (though those additional standards, "named parts", and "form participation" will still improve it). The adoption reflects that.

This is a pretty good article that while less optimistic than me I still think is pretty accurate and unbiased.
blog.logrocket.com/what-happened-t...

The main things I recommend web components for reflect current status and adoption. I recommend publishing web components via a framework/library for some use cases. Those components can be used with multiple frameworks with less painful lifecycle synchronization. The use cases where that becomes high value include UI libraries, widely shared but strongly encapsulated components (like customer service support chats), and microfrontend components that don't need SEO. (It's possible to have good SEO with web components, but it's an art and has limitations on nesting levels for now.)

Collapse
ivanjeremic profile image
Ivan Jeremic • Edited

I never used Tailwind but it feels the like the best choice, with scss you don't have a whole ecosystem that supports you with Accessibility and so on also headless-ui is something nice to not reinvent the wheel every project.

Collapse
hugekontrast profile image
Ashish Khare😎

I bet your next post must be titled : Why I left css preprocessors and returned to good old css with methodologies like CUBE etc.?

Just joking. Great article. Only I hate css preprocessors, and love to write plain css in CUBE style. Good day man!

Collapse
alekseiberezkin profile image
Aleksei Berezkin Author • Edited

Thanks, got your point 😉 Perhaps it's the matter of taste but I'm not a fan of “global” CSS, like, you know, in CUBE you define btn or wrapper and use it globally; I'm rather into component/block way where there's little or no global styles, and every component has its own one. So, chances are I'm not going to write a post about CUBE vs BEM or preprocessors; however, I'd be happy to read one 🙂

Collapse
puruvj profile image
PuruVJ

I love this article. I faced these problems myself too, so I took the red pill and jumped from styled-components to CSS modules. Heck I even wrote a blog post about this dev.to/puruvj/why-i-moved-from-sty...

Collapse
alekseiberezkin profile image
Aleksei Berezkin Author

Thanks, will definitely take a look!