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;
}
A.jsx
import './A.css'
function A() {
return <span class='b'>Hi</span>
}
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>
}
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>
}
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',
},
});
Looks boring and error-friendly. Btw, did you spot one? Color must be
Answer
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;
`
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)': {
// ...
},
}),
})
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;
}
Showcase.css.d.td (generated)
export const showcase: string
export const item: string
export const highlighted: string
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>
}
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
- Element name is
- Modifier is a class that tweaks a block or an element
- Block modifier name is
block_modifier
- Element modifier name is
block__element_modifier
- Block modifier name is
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;
}
}
}
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>
}
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;
}
}
}
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>
}
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)
// ...
}
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];
}
}
}
What with variables sharing?
It's not that straightforward, but there are options:
- With getComputedStyle you may get any effectively applied CSS value, including that of custom property (new browsers only)
- To get element size and offset you may query getBoundingClientRect
- Instead of scheduling anything based on animation timing you may use onanimationend and ontransitionend (new browsers only)
If these doesn't fit your needs you may introduce some naming conventions:
Showcase.scss
$shared-pad-size: 6px;
.showcase {
padding: $pad-size;
// ..
}
Showcase.jsx
const sharedPadSize = 6;
export function Showcase() {
// ...
}
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)
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.
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.
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.
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.
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.
So do you mean whoever uses React is completely wrong?
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.
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...
Thanks for your comment. So why do you think Web Components are not a mainstream yet?
(EDIT: updated a post with CSS vars)
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).
Thanks for such an insightful
commentessay 🙂 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?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.
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.)
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.
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!
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
orwrapper
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 🙂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...
Thanks, will definitely take a look!