Global and cascading nature of CSS
CSS is global by design! It is that way because we want to bring consistency across the website.
html {
font-family: Roboto, sans-serif;
}
By writing the above code every text on your website has a font-family
set. And this is by design instead of setting font-family
on each item.
This very nature of being global and cascading creates problems when styles in parents cascade into children. The problem doesn’t end there the devil called “specificity” in CSS is always there to bring you surprises.
<style>
#wrapper {
background-color: green;
}
.background-red {
background-color: red;
}
.background-orange {
background-color: orange;
}
</style>
// case 1
<div id="wrapper" class="background-orange">
Box 1
</div>
// case 2
<div class="background-orange background-red">
Box 2
</div>
- As the
id
has higher specificity overclass
the background color is green. - As the occurrence of
background-orange
is later in the style so it will override even though inclass
attributebackground-red
is used later.
Scoping of CSS per component should solve the global issue of CSS. The scoping CSS problem is solved differently by different technologies.
Web components provide scoping CSS into shadow dom which doesn’t leak anything outside and also doesn’t let other things affect it i.e scoping it into the component and closing at the same time. Frameworks like Vue also have scoped style solutions in place. The web components provide a very strict boundary and sometimes we don’t need such a strict boundary.
The architecture naming of CSS classes using BEM i.e .block_element--modifier
tries to solve the issue by following naming conventions but this isn’t absolute there are ways in which scope leaks can happen.
A framework like Vue and Angular has its own way of scoping styles to elements/components built in.
What CSS-in-JS does do for you?
It scopes all the styles into a unique class-name thereby solving the problem of global scope. Now each item has its CSS scoped to a unique class-name.
import { css } from "css-in-js";
function Flex(props) {
return (
<div className={css({ display: "flex" })}>
{props.children}
</div>
);
}
In the above code:
- The
css
function from the CSS-in-js lib takes instyle-object
- A
style-object
is nothing but a way of writing CSS with javascript objects.
.text-center
// following is how css is declartion is written in a .css file -- 1
{
text-align: center;
}
// the same object as style-object in javascript is written as -- 2
{
textAlign: 'center' // split on changed casing then joined by "-" and lowercased
}
- The
style-object
is converted into a valid CSS declaration and is scoped to a unique class-name andcss
function then returns that unique class-name.
const uniqClassName = css({ textAlign: 'center' });
// is converted as
// .css-123 { text-align: center; }
console.log(css({ textAlign: 'center' }); // css-123
The class-name is cached for the given
style-object
and whenever the same style is passed{ textAlign: 'center' }
tocss
function it will always yieldcss-123
, this is an optimization step.On the problem of specificity, it cannot solve for
case-1
because usingid
for styling indicates poor CSS architecture. For the latercase-2
it will solve it as:
css([
{ display: 'flex', backgroundColor: 'red' },
// conflicting declarations backgroundColor
{ flexDirection: 'column', backgroundColor: 'orange' }
])
// styles objects in [] are merged and thus resultant style object is
/*
{
display: 'flex',
flexDirection: 'column',
backgroundColor: 'orange'
}
*/
The above solves the specificity by applying the last declaration overriding others that came before it.
Problems with CSS-in-JS
- Repetition of styles. Even though the two style-object vary by a very small bit there is an entirely new class-name for two.
css({ display: 'flex', flexDirection: 'row' })
// .css-1234 {
// display: flex; // <-- same declaration for display
// flex-direction: row;
// }
css({ display: 'flex', flexDirection: 'column' })
// .css-9876 {
// display: flex; // <-- same declaration for display
// flex-direction: column;
// }
If you notice, the two declarations vary only in values of flexDirection
values but they will have entire different class-name, this is not a problem as it is by design to have all styles scoped uniquely under a unique class-name but the fact that the property display
is repeated means something better can be done.
- We generate a unique class-name for a style-object and as the same structured style-object always returns the same class-name.
const styleObject1 = {
textAlign: 'center'
};
const styleObject2 = {
textAlign: 'center'
};
styleObject1 === styleObject2 // false: object ref is diff
css(styleObject1) === css(styleObject2) // true: class-name is same
This can be only achieved by when there is a phase of stringifying the style object followed by hashing to generate a unique name.
JSON.stringify(styleObject1) === JSON.stringify(styleObject2);
// true: same stringified object value for the same structured object
// now hashing will return the same output as the input
// JSON.stringify(styleObject1), JSON.stringify(styleObject2) are same
Depending on the logic of stringifying the object the complexity & time may vary.
const object1 = { a: 1, b: 2 };
const object2 = { b: 2, a: 1 }; // <-- looks same but structerly different
JSON.stringify(object1) === JSON.stringify(object2) // false: diff structure
And so, depending on the logic of stringifying algorithm we can make object1
and object2
string representations look the same. It may or may not be a concern for lib to output the same class-name for same looking object and that will require some work! Most of the time it won’t be a concern as repeating a few class-name doesn’t matter much, but do note that there is always a hashing step involved usually these are quick and insecure hashing algorithms to optimize for speed.
When using React with CSS-in-JS there is a cost of injecting styles on every render along with the phase of stringifying to generate a class-name that will happen on every render. The new libraries like stitches and vanilla-extract-css are looking promising by making everything build time process so this is not going to be a problem in future. A framework like tailwind with atomic-css is something that is missing in CSS-in-JS world. I’m hopeful for stylex a Facebook internal atomic CSS-in-JS to provide the best of both world.
Love for preprocessors and pure CSS is not going to die that easily and it shouldn’t for the fact that CSS-in-JS is not needed for every website or it is just hard for UI-dev to wrap their minds around CSS-in-JS or view-encapsulation may just not be a problem for your project or you CSS architecture (or even using BEM naming convention) may just not have a need for it.
Also, CSS itself is evolving and I'm hopeful for a future where scoping will be built into CSS.
Conclusion
The CSS-in-JS library solves problems of global nature of CSS and of specificity by providing scoping in a unique class-name. It has some cost attached to it i.e run-time which is being solved by order libs vanilla-extract-css. I'm a big fan of tailwind and I honestly believe it is enough for your project. If you also need dynamic styles then CSS-in-JS is better over tailwind, though there are solutions like twind which provide a flavor of tailwind with the CSS-in-JS approach they do have all cons of any CSS-in-
JS libraries. I'm very excited about styles by Facebook and waiting for the day it will be open-sourced or CSS itself evolves to me provide scoping and be more modular, until that day comes I'm betting on CSS-in-JS with stitches and vanilla-extract-css.
Top comments (1)
is there any atomic-css-in-js framework ?