[ Jakob.fun ] [ Blog ] [ Speaking ] [ Fun ]
← Back

April 19, 2025 ・ Jakob Endrestad Kielland

A beautiful city that nobody wants to live in

When I was a student I shared an apartment with one of my closest friends, who has a degree in city planning. You’d never know it, but it’s amazing how many thunderous 4 AM speeches on why Brasilia sucks and how Jan Gehl is the second coming of Jesus Christ you can fit into a 3-year period when you combine alcohol with enthusiasm for the field of city planning.

Specifically, there is an outstanding TEDx-talk that we kept coming back to, in which the speaker, the aforementioned architect, slams the aforementioned capital by describing something he calls Brasilia syndrome:

“Brasilia is great. It looks fantastic from the airplane — it’s an eagle [
] and the head of the eagle is the parliament. Also from the helicopter Brasilia is absolutely great. [
] They have their various architectural monuments and they have their enormous parks. Great.

But what is not great in Brasilia is how people are treated, because nobody thought about that there would be people walking and moving about. [
] They never thought about that there would be no money to give everyone a helicopter so they really could enjoy Brasilia.”

He is saying that the city planners of Brasilia, a city that was planned and built from scratch in the 50s, seemingly forgot that they needed to make a city that was actually pleasant to live in, not just a city that looks good from above. The city planning doesn’t serve those who actually live and work there. If we look at a few pictures of Brasilia it’s easy to see what he means. Sure, it looks sick on a map, but if you zoom into a random street there’s a good chance it will feel barren, lifeless and downright uncomfortable because of the huge open spaces, enormous sight lines, and massive roads.

A satelite view of Brasilia, a city shaped like an eagle, next to a shot from street level showing a barren, desolate pathway

Brasilia in theory versus Brasilia in practice

The reason I bring this up isn’t only so you have a new topic to impress your dinner party guests with (or bore, if they don’t all happen to be city planners), it’s because we programmers also have a tendency to get Brasilia syndrome. We imagine these grand designs, these beautiful, elegant abstractions that will let us expand our code with a flick of the wrist, but get tangled up in our own spider web of complexity and end up with something that feels awful to work with. We end up with huge streets and empty parks.

You’re probably using CSS wrong

I’d wager that I care more about CSS than the average developer, and this blog post is about how I believe we should be thinking about CSS in projects that meet the following three criteria:

In other words, most of today’s frontend projects.

Now, if you, upon reading the title of this section, rolled your eyes and went “and I bet you think you can show me the right way to use CSS?” then consider your expectations successfully subverted. On the contrary, the whole point is that using CSS right is a trap that most developers should try to avoid falling into. It’s the frontend version of Brasilia syndrome, and we should actually be misusing the language even more than we are already doing. But before we get to that, let’s talk a bit more about the cascade.

The (genuinely) fantastic cornerstone of CSS

The idea behind the cascade is fantastic, it truly is. For those of you who primarily know the cascade as the first letter in CSS, a quick recap:

The cascade is the mechanism that decides which styles should be applied to each element. Styles clash in CSS all the time, and the browser needs to know how to prioritize one set of styles over another. Specificity is one part of the cascade, and probably the one we spend the most calories worrying about, but there are other parts too; cascade layers and the !important flag, for instance.

The idea behind the cascade, and arguably the central idea behind CSS, is a really powerful one: To provide us with the tools to let us specify how our site should be styled, using a predictable and powerful mechanism that lets us conditionally prioritize styles from different sources based on the structure of the elements being styled. The implications of these capabilities should not be underestimated: It means we can have one stylesheet, apply it across our entire site, and provided we haven’t messed up somewhere we’re free to add as much markup as we want, wherever we want, without having to also define the style for all of that markup. Or in other words, it enables a separation of concerns for our markup and styling, as this blog post repeatedly points out the importance of. As an aside, that post is actually what inspired me to write this one (sort of in the same way that an unnecessarily smelly lunch in the office inspires my eyes to roll backwards into my head).

This type of total separation is awesome in theory, but very difficult in practice, especially with today’s frameworks and use cases making our web applications vastly more complex than those seen at the dawn of the World Wide Web.

Unless you’re someone who’s good at (and interested in) CSS, and who at the same time wants to act as a maintainer of a design system, it’s really hard to create these universal stylesheets. Because you aren’t really just writing CSS, you’re creating a design system. These are styles that are applied to all the markup in the project (or at least a significant part of it) and so any reusable classes or styling that uses element selectors has to work for all the places it could conceivably be used. My experience is that this task becomes nigh-impossible when you have multiple developers working on different parts of the application at the same time, while also leveraging reusable components (be it with React or something else).

In other words, I think that this genuinely awesome idea is the root of why CSS has a tendency to get painful as a project grows. The reusability and potential for modularity inherently built into CSS is our Brasilia eagle; something beautiful and tempting which in practice makes us forget to write code that’s actually pleasant to live with in the years to come.

The Multi-Dimensional Dependency Matrix of Pain and Death

Successfully constructing and maintaining the eagle without frustrating current and future developers is difficult, but if you introduce reusable code components without scoping your CSS, you will almost inevitably construct The Multi-Dimensional Dependency Matrix of Pain and Death (TMDDMPD, really rolls off the tongue).

CSS, when not scoped, is a fully-featured reusability mechanism for your styling, it gives you everything you need to reuse your styles anywhere you might wish to do so. Let’s think of our interface as a set of “blocks”, or sets of elements that belong together. These could be forms, modals, navigation panels, or what have you, the point is that you’d typically work on one block at a time, whether you’re making one from scratch or changing an existing one. Each block can use many CSS classes, and likewise, each class can be used in many blocks — or in entity-relation-speak, they have an N:N-relationship.

Diagram showing three blocks (Block 1, Block 2, Block 3) each connected to one or more CSS classes (.class-1, .class-2, .class-3). Block 1 connects to .class-1 and .class-2, Block 2 connects to .class-2, and Block 3 connects to .class-3.
This is fine!

As an application grows large, these sets of dependencies can become quite the pain point, especially when you have to go back and change an existing part of the application. Now, lets introduce React components! On top of letting you create reusable modules of structure and functionality, they practically also serve as a reusability mechanism for your styling.

You probably see where this is going. Each block of functionality can contain several React components, and each React component can be used in several “blocks” of functionality. In addition, React components and CSS classes naturally have the same relationship as our blocks and CSS classes had earlier. In other words, we have two sets of N:N-relationships stacked back-to-back. Yikes!

Expanded diagram introducing a middle layer of React components between blocks and CSS classes. Each block (Block 1, Block 2, Block 3) is connected to one or more React components (React component 1, 2, 3), which in turn are connected to corresponding CSS classes. The dependencies are interlinked, showing shared usage of components and styles.

This Is Fine

Having complex dependency graphs in a codebase doesn’t necessarily need to be untenable, especially if you have tooling that helps you navigate it. In our case though, sadly, we don’t really have that — something which is especially noticable when you’re trying to make changes to existing components or styling in this sort of environment. My experience is that trying to figure out exactly which pieces of UI would be affected by changing something in this sort of multi-dimensional dependency hell quickly devolves into the ctrl-f equivalent of a bar fight where your opponent is every developer who has ever touched the codebase (including past and future versions of yourself). The natural consequence of this is that everyone starts making small one-off-classes or unbelievably specific selectors instead of editing the generic styles, and at that point you’ve already lost the advantages of having reusable, generic CSS classes. You’ve successfully managed to combine the tedium of scoped styling with the mental overhead of reusable styles. Does this sound familiar? (If this is you, know that I don’t judge you. This has happened to me too, and like a boiling frog I didn’t realize what was happening until everything was already very unpleasant.)


 And you should use it even more wrongly

“Okay smart-ass, so what do you figure the solution is?”

- You, probably

So far, the only constructive thing I’ve done has been to hint at the solution to this. The worst part (for you) is that even after enduring this almost-2000-word rant you won’t become privy to some grand secret. On the contrary, the idea that’s gonna help us solve these problems is so popular and unoriginal that there are about a million tools and libraries we can use to implement it: Scoped CSS.

Using the full power of the cascade is tempting, because it creates a beautiful, minimalistic abstraction that minimizes duplication and maximizes reusability, but in my opinion it’s simply too costly to do it properly. By conceding that we’ve lost this battle, that global stylesheets are dead, and that from here on out CSS is only to be used when scoped, we will end up with less complex code bases, spend less time fighting with CSS, and ship products faster.

I don’t think the exact tool you use to scope your CSS matters very much, the point is that the CSS can only apply to the stuff in its immediate vicinity. The styling belonging to a component should only ever be able to affect elements that are a part of that component — nothing above and nothing below. Personally I’m a big fan of using Tailwind for this, partially because its tendency of making you repeat yourself is a great way to force you to be conscious about exactly how you leverage components. Tailwind, being the most polarizing topic in all of frontend development, is a blog post for itself though, and I’ve had great experiences using CSS modules to achieve the same thing. Since CSS modules still use classes it means we as developers can still have a little bit of (contained) reusability, as a treat. Another benefit versus something like Tailwind is that you do not need to learn any new syntax since it’s just CSS, but scoped.

Refined dependency diagram with a layered structure: Blocks on the left, React components in the middle, and CSS classes on the right. Each block links to one or more components, and each component links to one or more CSS classes. The diagram shows a cleaner, more hierarchical structure compared to the second figure.

Bigger, yes, but much less stress-inducing

If we look at what this does to our dependency graph from earlier, it’s clear that while we have some code duplication, it’s a much less complex environment to make changes in. If it’s very important that the .class-2 should always be the same for React components 1 and 2, then it should probably be a React component instead. If it’s not that important, then it means we had the wrong abstraction (if you haven’t read that Sandi Metz blog post, trust me when I say it’s impossible to regret spending 5 minutes reading it).

Going from a project with “normal CSS” to one where all CSS is scoped feels like going from JavaScript to TypeScript, seriously. You might not notice a very clear difference as you switch, but you’ll sure as hell notice it if you have the misfortune of going back. This one change will remove heaps of mental overhead, make the act of changing existing styling feel safe and easy, and guarantee that you never have to worry about what to name a CSS class again. The only thing it will cost you is that you’ll have to write the same CSS a bit more often. In a sense you can think of it as trading away a bit of convenience when writing code in order to gain a lot of convenience when changing and removing code, and that’s a trade I’m willing to make any day.

It’s sort of a sad conclusion, because it really does feel like we’re giving up on something cool, useful and sleek and admitting that we’re not good enough to make it work, but I sure as hell know that I wouldn’t wanna go back after having seen the dark side.

Let’s stop writing CSS that satisfies our inner 1950’s city planner, and let’s start writing CSS that’s human scale instead.

Epilogue

This is obviously a nuanced topic. I’m not saying you should never reuse any CSS, but I do think there’s a good argument for saying that using tools that let you scope your CSS should be the default, especially in larger projects with multiple developers. I think that much of the frontend world is slowly realizing the same thing, which is probably why frameworks like Astro require you to explicitly opt-out of scoped styling, instead of the other way around. So, haven’t tried Tailwind yet? Give it a shot. Tried it and still want to scoop out your eyes with a rusty spoon when you see a 200-character class attribute? Maybe go with CSS modules instead.