John Holdun

Just write CSS

A colleague started a discussion at work today about this article by Josh W. Comeau about styled-components. It's quite thorough, and for someone that's deep in the world of styled-components, or other CSS-in-JS libraries, it could potentially be a revelation.

I don't much care for CSS-in-JS, or really anything-in-JS. I use React all the time, and sometimes I even use it on personal projects, but in general I strongly believe that if you're putting something in a browser, you ought to start with HTML, then layer on some CSS, and then finally—when everything works!—enhance your interactions as desired with some optional Javascript.

I'm very tired of trying to convince people that this is actually a good and sensible way to build websites, but here we go again: don't write Javascript that writes CSS! Just write CSS!

There are some really good pieces of advice in Josh's article for writing reusable and maintainable styles. This advice has nothing to do with styled-components.

Have you tried using CSS variables (also known as Custom Properties)? You can define some values—colors, measurements, ratios—just once, and then reference them later, or change them dynamically, and everything just follows along. The MDN docs on Custom Properties are an excellent resource if you want to learn more. And check out that browser compatibility chart—these work everywhere (except IE, which is fine). You can just drop custom properties in a .css file, right now, and they'll do exactly what Josh is doing in these Javascript snippets, but without a build step or whatever. Just use them. They'll save you time and you'll be able to do new things.

Another great piece of advice in here is about margins. Margins in CSS are weird. The box model is weird. It's a very good idea to apply margins very carefully, or avoid them altogether when a more nuanced solution, like grid, is available to you. Josh—Josh!!!—has published a really good guide to how margins of adjacent elements interact, called The Rules of Margin Collapse.

Avoiding setting margin properties on elements that are meant to sit inside various containers is definitely a useful practice. The way I think about this, generally, is that every element that owns content will take up 100% of the space it’s given, and then a parent container will define the allowable space with margins or grids or flexbox or whatever. Centralizing the logic for how things flow in your layout makes life easier.

Again, this has nothing to do with Javascript. But that leads us to this piece from Max Stoiber, linked in Josh's article, which recommends avoiding the margin property altogether in favor of using "spacer components." The example given is <Stack> from the Braid design system. It's a React component that will space out its children in a variety of ways.

“Don’t use (this CSS property), use (this React component) instead” makes me sad! Looking at how <Stack> is actually rendered in the DOM is kind of interesting, and provides one good example of how to solve this problem with alternative CSS, but leaning on helper components from some library is a good way to not learn how CSS works[^1].

[^1]: It’s also very difficult to actually tease out each component’s responsibilities by inspecting the DOM, but that’s a topic for another time.

In the first example here, <Stack> seems to be setting identical padding-top properties on every child, then wrapping them all in another element, and using the :before pseudo-element of that wrapper with a negative margin-top to cancel out the padding-top of the first child.

It's a fine way to go about solving this problem. Personally, I would have used :first-child to remove the padding-top property being set by the container instead of setting a negative margin on an additional container element that only seems to exist for this purpose. One could also just use margin-top on the children and set overflow: hidden on the container so that the margin calculations don’t bleed out of the container, or set margin-bottom on all the children and just let them bleed over if you’re also setting a margin-bottom on your container that is greater than or equal to the children’s margin-bottom.

One perspective is: there are so many ways to do this, with weird gotchas for each one, and isn't it great that this one component can just handle all of it? Another perspective is: there are so many ways to do this, and each one has its own tradeoffs, and isn't it important to understand the compromises we're making in choosing one over the other?

Lots of how CSS works is not particularly intuitive, and it’s not necessarily easier to just write the CSS for this instead of using some component, especially if you’re not already familiar with how these properties work together. But if you're adding this component—or even this whole library—to your project so you can solve this problem, you still have to learn how it works. This library might be incompatible with some future project. I’d argue that reading the docs for margin is more valuable, and will be useful to you for much longer, than reading the docs for <Stack> or whatever.

This brings us to my main argument, I think: CSS-in-JS and Virtual DOMs and all these layers of abstraction don't actually allow a developer to skip understanding what's underneath them, because once your code lands in a browser, it's going to be HTML, styled by CSS, with interactions enhanced by JS. If something goes wrong, or just turns out in a way you didn't expect, there's no guarantee that you can use the tools you've chosen to change the thing that needs changing. And there's no guarantee that your tools will even help you understand what's wrong.

You're still responsible for what's in the browser. Why not make your life a little simpler and just work your way up from the most basic ingredients? Why not just write CSS?