Remember the first time you wrote some html/css? It was amazing, right?
<div style="color: blue">Hello world</div>
So simple, so pure.
Then, of course, the senior developer™ told you not to write your css like this and put it a separate file.
<div class="blue-text">Hello world</div>
/* style.css */
.blue-text {
color: blue;
}
As you build more elements and pages, your style.css
starts getting long, so you split it into multiple files.
/* landing-page.css */
.heading {
font-size: 64px;
}
button {
background: orange;
}
/* forms.css */
button {
background: blue;
}
Soon, you realise that styles intended for one element start clashing and overwriting others. You adopt some form of name-spacing to create a scope for these styles.
Maybe something as simple as this:
<form>
<input placeholder="email" />
<button>Submit<button>
</form>
/* form.css */
form button {
background: blue;
}
Or something more advanced like BEM:
<form class="form">
<input class="form__input" placeholder="email" />
<button class="form__button">Submit<button>
</form>
/* form.css */
.form__button {
background: blue;
}
I really liked BEM (and other convention based methods like OOCSS, ITCSS, etc.). You can solve a hairy problem by merely adopting a common convention in your team.
Either way, the problem you are solving here is of scoping styles to a specific context.
The same problems and solutions are carried into React land as well. Take this LoginForm
for example:
function LoginForm() {
return (
<form className="form">
<input placeholder="username" type="text " />
<input placeholder="password" type="password" />
<button className="button">Submit</button>
</form>
)
}
We have no idea if that button is going to clash with another button in the application somewhere else. We should use some sort of scope here. You can still use namespacing like BEM here with React.
<button className="form__button">Submit</button>
This is where the story get's interesting. In a React component, we aren't writing plain HTML
anymore, we are writing JSX
.
The above line of JSX is converted to this block of javascript at build time:
React.createElement(
'button',
{ className: 'form__button' },
'Submit'
)
You have the full power of a programming language (javascript) at your disposal now. You can do things which wouldn't be possible with pure CSS.
The promise of CSS-in-JS
You can defer the job of creating a scope or name-spacing to the language instead of doing it manually.
CSS Modules is the gateway drug the of css-in-js ecosystem.
This is what you write:
/* form.css */
button {
/* look ma, no name spacing */
background: blue;
}
import styles from './form.css'
function LoginForm() {
return (
<form>
<button className={styles.button}>Submit</button>
</form>
)
}
And this is what styles.button
get's compiled to:
function LoginForm() {
return (
<form>
<button className="form__button__abc1">Submit</button>
</form>
)
}
This is very similar to what you would write by hand, but it frees you up from the responsibility of avoiding conflicts. I find it incredibly liberating to be able to write my styles as if they were locally scoped.
The next wave of CSS-in-JS libraries
We were able to leverage the power of the language in the tooling/automation bit, can we also bring it to writing styles?
This is where it becomes controversial. Each CSS-in-JS takes a slightly different approach to enable a certain new feature by making a tradeoff.
For example: jsxstyle
lets you write styles that look like classic inline styles on the element but extracts them out into a file through a webpack plugin.
<Block component="button" backgroundColor="blue" />
On the other hand, styled-components
lets you mix runtime logic inside css. This means you can start using it without touching your config but you can't extract the styles out.
const Button = styled.button`
background: ${getBackground};
`
function getBackground(props) {
if (props.appearance === 'primary') return 'blue'
else if (props.appearance === 'disabled') return 'grey'
else return 'white'
}
linaria
takes an interesting middle route with its css
tag, you can create classes right next to the component and these extract them out during build using a babel plugin. This means you can still use javascript but it can't depend on runtime logic like the previous example.
const button = css`
background: ${colors.blue};
`
function MyComponent() {
return <button className={button}>Click me</button>
}
As you can see, all of these libraries bring something to the table in exchange of a something else. This is why it's so controversial, you can take any library and find flaws in it.
css-modules
automates the work of namespacing styles but requires some setup which can be looked as over-engineering (we already have manual BEM that works without any setup)
styled-components
on the other hand does not require any babel/webpack setup but it requires the library to be present on runtime, increasing the size of your javascript bundle by a small amount.
You must choose the tradeoff that works for your project.
With the design system in Auth0, we chose styled-components
because it helped us create flexible components based on a set of underlying tokens and design patterns.
Last week, I had to build a bunch of pages with some form logic in them and really enjoyed using css-modules
because it helped me write both global and page specific styles without adopting a manual methodology like BEM.
You can use this comparison table that my friend Michele Bertoli created.
Hope that was helpful on your journey
Sid