We can analyze the problems with modern CSS by looking at how popular frameworks use specificity and the cascade:
The simplest criticism is the use of div as a structure element in the HTML. Take this example from Bootstrap:
<div class="container">
<div class="row">
<div class="col">Column</div>
<div class="col">Column</div>
<div class="w-100"></div>
<div class="col">Column</div>
<div class="col">Column</div>
</div>
</div>
This simple example shows how a large proportion of modern CSS frameworks rely on div to create structure and layouts when the div has absolutely nothing to do with the content. Another example from bootstrap is their image carousel:
<div id="carouselExampleSlidesOnly" class="carousel slide" data-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
<img class="d-block w-100" src="..." alt="First slide">
</div>
<div class="carousel-item">
<img class="d-block w-100" src="..." alt="Second slide">
</div>
<div class="carousel-item">
<img class="d-block w-100" src="..." alt="Third slide">
</div>
</div>
</div>
This snippet of code is 13 lines long of which only 3 are actually image content. It also contains both id and class specificity, making it nearly impossible to override when used locally. If you compare that my Carousel demo you can see it much simpler:
<carousel>
<figure class="active">
<img alt="Stock photo" src="...">
<figcaption>Image 1</figcaption>
</figure>
<!-- this image is not seen until set active -->
<figure>
<img alt="Stock photo" src="...">
<figcaption>Image 2</figcaption>
</figure>
<prev><Icon name="arrow-left" size="48" /></prev>
<next><Icon name="arrow-right" size="48" /></next>
</carousel>
This is the same numbe of lines of code, but every line is clearly idenfitied and easy to understand. Each part of the display is named, matches its use in the display, and is semantically related to the concept of images.
CSS was designed to be a style sheet language used for describing the presentation of a document. You can read from Mozilla their own explanation as well:
While HTML is used to define the structure and semantics of your content, CSS is used to style it and lay it out. For example, you can use CSS to alter the font, color, size, and spacing of your content, split it into multiple columns, or add animations and other decorative features.
The original intent of CSS--and still a major factor in its design--is that the HTML content doesn't have to change to alter the display for different situations. CSS Zen Garden is the classic example of the principle, with a single HTML document being styled in many different ways using only CSS.
The above examples from Bootstrap (and most other CSS frameworks) subverts this original CSS design by forcing you to add non-semantic div tags only to create a display structure (not information structure) for their CSS properties.
What's the impact of this div.class heavy design? By subverting CSS's original separation from the HTML you end up with the following usability problems:
A succinct way to put all of the above is this infection of CSS rules into the HTML document means you no longer have a single place to go when there's a problem with one, but instead have to constantly work in both to fix anything. If a button isn't the right size, you can't simply go to the CSS and fix it. You have to go to:
This turns one target area for debugging the presentation (the CSS), into 3 potential interacting locations making it more difficult to solve the problem. If you stick to "layout and style is in CSS" then when you have a problem with the layout or style...you just go to the CSS.
Let's take a look at a simple example from Bulma to explain what's happening:
<button class="button">
Button
</button>
<button class="button is-primary">
Primary button
</button>
In this example we see that Bulma is using both tags and classes to style buttons. This means that Bulma effectively corrupts 2/3rds of the specificity rules for its design (not to mention is needlessly repetitive). If you used this code in a part of your app, but needed to slightly change the padding, you'd be forced to struggle with Bulma taking over 2 of the 3 slots reserved for specificity:
Mozilla's recommendations are another source of confusion in CSS frameworks and is most likely the cause of frameworks using structure divs. Mozilla recommends that instead of using !important you add a structure div to get one more level of specificity:
<div id="test">
<span>Text</span>
</div>
div#test span { color: green; }
div span { color: blue; }
span { color: red; }
In this "fix" for !important your only choice is to wrap the component you want to modify with even more div structure and then create a more specific path with the new class. The problem with this fix is it overcomplicates the HTML but also breaks separation of content from style. Rather than keep your content in HTML, and your style in CSS, you're now forced to infect your HTML with helpers for CSS, all because someone else thinks you shouldn't use !important to override their classes.
The other odd hack they recommend is simply doubling the class:
#myId#myId span { color: yellow; }
.myClass.myClass span { color: orange; }
There's no explanation regarding why this odd quirk of browsers isn't just a hack, but it's put forward as superior to the very clear !important. It'd be incredibly easy to miss this double-class hack if you were trying to find it in the CSS, but finding !important is trivial to find and fix later if you want. In fact, I can't actually see why this is better if it does the same thing as !important but in a sneaky way.
The end result of these recommendations from Mozilla is people solve specificity by adding on more and more divs because they've used class everywhere and can't get around the specificity calculations any other way. You can see this in how Bulma uses extra divs in its forms:
<div class="field">
<label class="label">Subject</label>
<div class="control">
<div class="select">
<select>
<option>Select dropdown</option>
<option>With options</option>
</select>
</div>
</div>
</div>
You have three levels of divs just to style a plain select tag. Most likely the use of classes forces Bulma to add more divs to increase specificity because they have hijacked the class specificity everywhere.
Meanwhile, similar styling is done in MVP.css and other "classless" frameworks by using perfectly normal and valid tag based selectors:
select {
display: block;
font-size: inherit;
max-width: var(--width-card-wide);
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
margin-bottom: 1rem;
padding: 0.4rem 0.8rem;
}
We can also say that if there is some failing of CSS that requires extra class divs then where is the explanation for this limitation? Why can't we just use a simple tag selector to completely alter the appearance of a select? Why is it suddenly these problems go away when I slather on a mountain of divs? If this is really the situation then that means CSS has a serious flaw as the "styling and layout" component of the web and everyone advocating for it needs to be honest and stop blaming others for not knowing it well enough. My understanding though is CSS is perfectly fine at styling a select tag all you want, and it's the way CSS is used today that's causing the problems.
At first glance most of the criticism might seem minor. Who cares if a single .class in a single .css file overrides my local CSS? That'd be fairly easy to find right? Where this turns into a usability nightmare is when the cascade is added to the system. The cascade's purpose is to allow for CSS styles to come from different sources, but combine in a kind of hierarchy of importance. From Mozilla's documentation we can see an initial problem:
Only CSS declarations, that is property/value pairs, participate in the cascade. This means that at-rules containing entities other than declarations, such as a @font-face rule containing descriptors, don't participate in the cascade. In these cases, only the at-rule as a whole participates in the cascade: here, the @font-face identified by its font-family descriptor. If several @font-face rules with the same descriptor are defined, only the most appropriate @font-face, as a whole, is considered.
While the declarations contained in most at-rules — such as those in @media, @document, or @supports — participate in the cascade, declarations contained in @keyframes don't. As with @font-face, only the at-rule as a whole is selected via the cascade algorithm.
Finally, note that @import and @charset obey specific algorithms and aren't affected by the cascade algorithm.
So the cascade combines properties from different sources but only properties that aren't @at-rules containing descriptors, and @import or @charset follow another set of rules entirely. Got it? Great. So easy and consistent, and we're not done yet.
Mozilla goes on to define the order these rules are processed (minus all the edge cases of @at-rules):
- It first filters all the rules from the different sources to keep only the rules that apply to a given element.
- Then it sorts these rules according to their importance, that is, whether or not they are followed by !important, and by their origin.
- In case of equality, the specificity of a value is considered to choose one or the other.
What is this ordering of rules? Why a handy list in this order (which isn't clearly described as being in most or least important ordering):
- user agent == normal
- user == normal
- author == normal
- animations ==
- author == !important
- user == !important
- user agent == !important
- transitions
However, these calculations are completely pointless because user style sheets have been slowly phased out. A "user style" is a piece of CSS that someone who is using a browser adds--on their own computer--to change the defaults set by the author of the site. The catch is, Chrome does not support user style sheets, and it's the most popular browser. That means they are completely dead for practical purposes. That leaves the user agent, which is usually reset by designers anyway, and "author" styles which is simply, "all of the CSS you write".
The confusing part of this description is it makes it seem like someone writing a web page is having to contend with these rules that include user styles when you actually have zero control over them, so they don't matter. The real calculations for practical purposes should be (from least to most important):
That's it. User agent styles are so low level they are easily replaced with a reset style. No user agents have !important rules (that I know of), and if they do this means you probably can't change them anyway. You can't control user styles as that's added by the user out of your control, so trying to add them to your calculations is meaningless. That leaves only the rules for cascade precedence in the CSS you write, and this simplifies the ordering.
Remember how you were admonished to never use !important? If you follow that edict, then, that means we finally arrive at only two sorting rules:
But finally, the last rule of sorting by order is only if there's a tie in the previous sorting. That means there's really only 1 rule, or maybe 1.5 rules, where you sort by specificity only and then order wins in a tie.
I call these simplified practical rules the "real cascade" because they're the practical sorting rules you actually have to deal with, and they also help finally describe how modern CSS breaks this sorting cascade to make CSS harder:
Effectively, this situation promotes CSS frameworks winning over your own styles, encourages convoluted nested div structure to hack specificity, and complicates cascade calculations needlessly by changing the priority of an author style that uses div.class such that it's difficult to modify design elements locally.
Can we simplify CSS usage to avoid as many pitfalls as possible while still allowing for modern visual presentation? Since CSS lacks clear rules for the cascade, and has overly complicated rules governing the calculations of specificity using these unclear cascade rules, a good approach is to simply avoid as many of these rules as possible. We can do this by realizing that...we can just not use most of this: