I Just Can't with CSS Anymore

Four years ago, I worked at an agency and oh my did I write a lot of CSS. From project to project, a lot of CSS would have a lot of commonality. Patterns emerged, I needed custom buttons, a page wrapper, a flexible grid system, a type scale. I open sourced them on GitHub (because who hasn't ran down that rabbit-hole in the name of GitHub stars and immortality) and made them easy for me to find and update (well not so much update). I started to find that I'd need these in almost ever project I touched, from job to job. A lot of these patterns never changed much.

In building these patterns, I found the language underneath really rigid to work with and the environment pretty unintuitive. A lot projects tried to fix this and no matter what preprocessor or abstraction came along, the underlying problems remained:

  • Flawed language features
  • Admittedly bad early design features
  • Browser adoption of critical features
  • The Cascade
  • String matching as inheritance

CSS has a low barrier to entry. It's easy to read and doesn't have a whole lot of different variation to the language. You define a selector (an element, an id or a class) and attach a map of declaration styles to it. With that low barrier to entry, some of these "problems" come from having that simplicity come with a lot of power, like the cascade.

Don't get me wrong. This is the only real option we have to style the web. In one way or another, either through a pre-processor, a CSS-in-JS solution, we eventually get CSS somehow. But that begs the question, how do we work in it if it's riddled with flaws? I've gotten to a point of real frustration and solutions that don't tackle the issues at hand and I'm honestly not sure what will.

So I'd like to outline what hasn't historically worked well for me and document why I feel it hasn't. A lot of this is for the purpose of collecting my own thoughts in one consolidated place versus shitposting to the nth degree on Twitter versus suggesting what might help going forward.

Finger Pointing

Wait, if you know CSS you solve all those things you just listed as "problems". I would say that's true, also in the same sense if you rig an election, you can also say you won the race. Of course, knowing CSS, having experience with language and the various hacks and CSS patterns will give you the ability to be semi-productive in shipping; that part isn't in dispute.

String Matching

Okay, so we can't do much about browser adoption or about what happened in 1996 so please explain the inheritance issue.

So let's take this given CSS and markup:

.Module { background: goldenrod; }
.ModuleOther { background: aqua; }
.Module { background: pink; }
#uniqueID { color: blue; background: red; }
<div class="Module ModuleOther" />

Here's a demo.

Dollars to doughnuts, that <div> is a wonderful shade of browser default pink and not aqua much less goldenrod. Classes aren't immutable values. They can be overwritten very easily.

The order of declarations being written in the stylesheet matters so much more than when it's being "invoked" onto the element. The element should have been aqua.

Now that coupled with you can increase the specificity of the selector:

div.Module.ModuleOther#uniqueID {}

Side note: I will never understand why developers who wrote CSS ever felt the need to call a selector div.className. I know what it achieves, but it always looks and feels like a hack. You don't gain anymore semantic value and you make your elements harder to refactor. I don't always know that this module will be a <div> and, I don't know, it's always felt weird to me. But then again I didn't start writing CSS until 2011.

This is by far my biggest issue of the design and implementation of CSS. I understand how to work around this through a discipline (either a naming structure), it's just very difficult to enforce or communicate those disciplines. Linters are one thing, but it's also the easiest thing to ignore when you're trying to ship a feature.

On top of that, when we depend on class names to be present or absent in our UI we're trying to application logic, we're setting ourselves to have unpredictable application state. I mean something like detecting if an element contains and .active class. I've shipped code like that to prod and I've regretted it and it's created really WEIRD BUGS.

Cascade

Okay, here we go again, let's attempt to find some territory that all the other the blog posts and conference talks and think-pieces haven't.

I like the Cascade, but only when it helps and only to a point. I just don't like how inconsistent it is. At the root of the document I can set a global typographic style to my application. Something like:

html {
    font: 400 100%/1.6 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

input, textarea, select, button {
    font-family: inherit;
}

With that snippet, everything in my application is now set to use the same typeface, it's a nice default to work with. But it only applies to a subset of surface properties, not layout properties.

For instance if I also styled html like this:

html {
    padding: 1rem;
}

Demo.

I could use the wildcard selector * to set everything to have a padding, but it applies to everything and isn't a component of the Cascade feature of the language. Simply put, it works, just not as a result of the cascade, just because it targets everything. The cascade is strongest when the relationship between nodes in the tree is very clear and it's often not in dynamic applications.

This doesn't seem like a problem when looking at the simple demos. At any scale beyond that magnitude, the cascade falls apart.

As the codebase grows and the number of components and different parent + child dependencies develop in your CSS, you fight the cascade constantly. We need the default text color to be #333 but this module has a white background and the text inside of it needs to be #202020, and more more needs and changes arise without time to curate or take inventory.

At this point, I'm always wondering with a codebase: how do you know when to delete CSS that've you written? Because we have a cascade to work with, you have to run the code in order to know what it does. Over time, your stylesheets only grow, it's hard to know how to prune them.

Immutability would change this issue almost right away.

Abandoning Solutions

Okay, I get it, this is a tough a garden to tend and weed; it's hard to manage and work with but still people manage and applications still get shipped almost continuously. Why not just cut straight to how to do that?

Okay, I will. Hang on.

The most common solutions to dealing with the language and implement flaws of CSS are two fold: strict naming methods and a rigid architecture.

Naming Schemes

This is a common practice. It sounds super complicated but it's just a pattern that you choose to follow when you write a selector.

BEM is one such pattern.

.Block__element--modifier {
  /* Declarations */
}

This makes it clear to the author, you're intending for a new set of styles, that are distinctly attached to a set of elements and variations. The relationship between the styles and markup are clarified.

Implementing a naming scheme in your CSS has just enough barrier to entry as authoring more CSS. It's something you can just chose to do.

Side note: In practice using Pascal casing makes scanning your work a lot easier.

Like I said before you can lint for it, but it's not an easy thing to enforce. Seriously, I've never had a manager who was cool with blocking a pull request because it failed some lint rule in the middle of a sprint and it's certainly not gonna make the rest of the team happy. But that's more an issue with the development more than the language, but still doesn't get fixed.

The bigger issue here is that these are strings, they're not immutable. I can very easily overwrite one of them. There are tools to solve this type of analysis but that only points to one source of your CSS, your application can consume more CSS from third-party stylesheet.

Architecture

Naming your CSS selectors is one thing, maintaining it over time is another. Maintaining CSS is a tough needle to thread, it's difficult on an agile team and it's more difficult when you don't decide how to add new CSS or how your CSS is layered together.

ITCSS is an approach that was put together by Harry Robert and I've outlined some details here. ITCSS defines "layers" of where and how certain styles should be grouped together. You have 6 layers:

  1. Settings & Tools: Global Variables, mixins, functions
  2. Generic: A reset or normalize for your CSS
  3. Elements: Styles devoted to default HTML elements without classes
  4. Objects: Non-visual modules, a grid, page container
  5. Components: Distinct visual UI modules, cards, buttons, input groups
  6. Utilities: Single use classes to override defaults
/* Objects */
.Wrapper {
  margin: 0 auto 16px;
  max-width: 56.24rem;
  width: 100%;
}

/* Components */
.Button {
  background: #147AAD;
  color: white;
  border: 0;
  border-color: transparent;
  padding: 8px;
  border-radius: 4px;
  transition: color 250ms linear, background 250ms linear, border 250ms linear;
  cursor: pointer;
  display: inline-block;
  font-weight: 500;
}

/* Utilities */
.u-red { color: #D04D36 }

Employing an architecture ensures that the cascade is kept in check, because the order of each layer purposes overrides the previous and gives sufficient context to other engineers on where and how to add CSS into the codebase.

But again like a naming convention, it only matters when it's maintained / enforced / applied. This is also prone to error when you start to load in CSS after your main outputted bundle. If you're not in control of those styles, it's harder to know whether your structure works and because it's a practice, it's not easy to test or lint against.

Functional CSS / f(css)

Functional CSS or f(css), is a completely different solution to working with this language. It's a simple concept that treats every selector, like a pure function. A pure function, has a single input and output:

function squared(x) {
    return x * x
}

When that idea is applied to CSS, it translates into a selector only having a single or common value.

.bg-red { background: red; }
.px3 { padding-left: 16px; padding-right: 16px; }
.tl { text-align: left; }

This works because it only applies styles to the element that you made a conscious choice to apply versus inherit. Tachyons and BassCSS are some of the front-running solutions in this space.

Adopted Solutions

This is more about how I've adopted some of those solutions and my approach to authoring (or not authorizing CSS). For some perspective, I've worked on two agile teams that focused on shipping product bi-weekly, done agency work and have spent a lot of time prototyping solutions and features over the last 6 years.

Level.css

For my mileage, having sensible defaults to work with at the beginning of a project is much easier. This addresses the first three problems I listed at the beginning of this post (back when I had credibility and hope). By using box-sizing: border-box and having forms inherit the global styling of the document, it's easier add modules with confidence and have the right things cascade (again not evil, just not always helpful).

Link

Obsidian.css

Obsidian.css is a solution that follows the ITCSS architecture and BEM naming convention and collects a lot of the early patterns from at least 1500 words ago before this blog post, well became this blog post.

Obsidian.css is a framework like Bootstrap but, I've mostly used it as a way to communicate to teams how we could and might structure our project and provide a point of reference to work from as we onboard new team members or start a new project.

Link


One thing I'm sure you're wondering at this point, is there anything beyond naming pyramid schemes and buying into architectures. And to be honest I'm not sure.

If human discipline is the only thing maintaining your API, you're trusting the worst and an incredibly flawed & brittle system to maintain your solution. And if don't believe that humans are the worst, I have a case study for you. It's called: Twitter.com.

My only real conviction at this point is removing the human decision making component (read bike shedding) from shipping UI. Meaning that automating, testing, and experimenting with new solutions should be a regular practice.

So you might be wondering if I specifically mean a CSS-in-JS solution. But can one of them salvage this at all? No, it cannot. Although, if you image CSS as a Diet Coke that's both flat and warm, essentially those solutions can give the illusion of bubbles, so kinda touches on one of those problems; but that's a different blog post.

I think the weakest part of CSS-in-JS, most CSS-in-JS solutions are coupled to an implementation like React or Vue, or to a build pipeline like through Babel plugins or certain Webpack settings. But this only seems to an issue for engineers writing applications that are bolted onto an existing application that doesn't have one of those pieces already. If you're building something more contemporary, you're already using one of these pieces.

But things like styled-components or JSXStyle where you're defining a component that represents a set of style and invoking that component applies those styles to the element, makes it much clearer as to how and when you can remove or refactor your UI. So at least checks a few of those boxes on my list.


2200 words later and you never really arrived a point other than solutions aren't all that great. Do you have any hope at all?

GIF of Sherlock turning over in frustration to ignore people
Nope.

Honestly, my non-snarky answers are: be aware of the limitations of CSS, pick / enforce / maintain a solution whether it's ITCSS or styled-components or TypeStyle or BEM or Sass or some hybrid approach. None of these solutions is going be a silver bullet, there are no silver bullets to cast.

Side note: But what doesn't solve the problem is picking a fight with a maintainer of one of these solutions and suggest they don't know CSS or too stupid to understand CSS; that never helps anyone.

The answer that I keep hoping for is better primitives to work with on the web. I think React exposes a nicer developer experience and empowers a lot of great UI to be built without a lot of effort but I think the answer is not being bound to the DOM or to stylesheets and replacing them with something that isn't bound to either and exposes a different set of conventions.

Further Reading