Go to the homepage

What makes CSS hard to master

by Tim Severien

Last week, Emma Bostian tweeted about how she was sick of “[CSS]’s shit.” She later apologised, but her wording suggests she still thinks it isn’t lack of knowledge. It’s fairly common for software engineers to complain about CSS. Interestingly enough, that portion of the web development community also thinks CSS is easy.

In contrast, Eric Meyer, Sara Soueidan, Bruce Lawson, and more (principal) web developers seem to agree that CSS is hard to master. It’s interesting how experts say CSS is hard, while a group of people who specialise in other technology say otherwise.

I feel we, the community, have to acknowledge that CSS is easy to get started with and hard to master. Let’s reflect on the language and find out what makes it hard.

Syntax And Common Properties

Back in 2013, Kitty Giraudel published CSS is Easy, in which they also write about how CSS is perceived as easy. In the article, Kitty makes an excellent point: “CSS is easy… syntactically.” I wholeheartedly agree. Consider the following example:

body {
	background-color: black;
	color: white;
}

@media (min-width: 64em) {
	body {
		background-color: white;
		color: black;
	}
}

In this snippet, you can see much of the syntax used in CSS. There are selectors, declarations, I even added an at-rule (@media). The properties and values in this example are straight-forward; they just work. The at-rule, although syntactically simple, adds some cognitive load to understanding this snippet. It creates a relationship between what’s inside and outside the at-rule. In other words, to determine what colour the body is, we have to parse the entire file mentally.

This snippet isn’t too complex, but as we get more code the number of relationships (like the at-rule) do too.

A Web Of Relationships

Syntax and basic properties are pretty easy, indeed. However, the learning curve of CSS quickly gets steeper when you start dealing with various relationships. Let’s look at some.

Property Sets

Many properties are part of a set, meaning you may need several others to make one work. For instance, the left, right, top, bottom, and the z-index properties are ignored until you add the position property and set it to anything but static. That means that you have to learn a couple of properties of a set and how they relate to each other before being able to use one. If you’re unaware which properties rely on others, it can be extremely frustrating because it may appear that a declaration just doesn’t work.

Additionally, it’s not always obvious which pieces of the set are declared. Consider the following example:

.snackbar {
	position: fixed;
	bottom: 1rem;
	left: 1rem;
	right: 1rem;
}

@media (min-width: 64em) {
	.snackbar {
		bottom: 2rem;
		left: 2rem;
		right: 2rem;
	}
}

If we look at the first rule, we can easily tell how an element with the snackbar class would be positioned on our page. If we look at the second rule, the one in the @media at-rule, we’re not so sure. We’d have to look up the .snackbar rule outside the at-rule. This example is fairly obvious, but it can get pretty obscure. The position: fixed declaration could be set elsewhere, on another selector, or inline.

The DOM

We can’t determine which styles are applied just by looking at your CSS. The HTML, or rather the DOM, also affects them. We have to consider the tag name of the element, its attributes, its siblings, its parents all the way up to the root node, and all relevant styles. If you render HTML server- or client-side, all of this may be dynamic and influenced by the application state.

The adjacent sibling combinator (+) is a simple example of how HTML influences CSS. The following style is only applied when a li is followed by another:

li + li {
	margin-top: 1rem;
}

This example is quite expressive but doesn’t have to be. Another example that took me a while to get as a beginner was using position: absolute on an element with a parent with position: relative. The element with position: absolute is positioned relative to the viewport or relative to an ancestor element with position: relative. In order to place this absolute element correctly in the interface, we have to know whether the element sits in a container with position: relative. Again, we’d have to look at the DOM, and at all it’s states, to be sure this is the case.

A similar, perhaps more familiar, example is the flex property. It only works if the element is a direct child of an element with the display: flex declaration.

When we write CSS, we always have to consider the DOM.

Cascade

Let’s say I want text and links in the <nav> element to be red. We might write the following CSS:

nav {
	color: red;
}
nav a {
	color: inherit;
}

These two simple declarations imply various relationships. The link colour is now dependent on the colour of the text. Because of that, we can set the text colour of the <nav> element and the link will inherit this colour. If we remove either of these two declarations, the link colour won’t be red anymore.

The flexibility cascade gives has its benefits, but it’s not hard to imagine that it can get difficult to track the relationships between all styles. It’s no surprise that many opt to use cascade as little as possible.

Compatibility

Designing an interface with CSS that doesn’t break is no easy task. There are numerous devices, browsers, resolutions, and users we have to take into account. We can also account for device settings, like dark mode using prefers-color-scheme: dark/light. On top of that, there are often various conditions in the interface that affect the styling as well.

Every combination of all these states and overrides have to be taken into account to produce the right output style. Again, this is no easy feat. It often involves a lot of tools and testing during and after development.

Maintainability

Like all other code, CSS should be clean, readable, and maintainable. We can group styles into various files, or prefix selectors, but all rules live globally. This is a challenge, but also greatly contributes to keeping the code DRY.

I earlier explained various types of relationships in CSS. They play a part in maintainability as well. Some of the methodologies used in CSS — like atomic design, BEM, ITCSS, and others — help to keep the coupling in check. They’re similar to paradigms and best practises in programming, and also steepen the learning curve and requires additional knowledge.

Then there are also preprocessors. We have a lot of them (e.g. PostCSS, Sass, LESS, and Stylus) at our disposal to do things we can’t do with vanilla CSS. This usually improves maintainability but does add another layer of complexity.

Finally, not unlike other languages, there often are various solutions to one problem. A common example is deciding between display: inline-block, the float property, flexbox, or grid to horizontally stack a list of items. Each has its pros and cons. When mentoring beginners, I found this is a challenge for them. It’s also difficult to convey the differences of these options — that are often subtle — and which is the most robust in a given situation.

All together, maintainability in CSS is a tradeoff. The code gets more complex syntactically and bound by self-imposed rules, but (hopefully) reduces the effort it takes to understand and modify the code.

Concluding

CSS may seem simple at first, but the learning curve isn’t linear; mastering it is hard. By no means is the language perfect, but it’s amazingly efficient at what it’s designed to do. Despite its simple syntax, there’s little it can’t do.

I think mastering CSS comes down to having a good amount of knowledge about it, recognising the subtle dependencies between different declarations, rules, and the DOM, understanding how they make your CSS complex, and how to best avoid them. Reaching that point is naturally frustrating because you will run into code that is a tangled mess where the tiniest edit might change the interface in unexpected ways.

I worry that the way we think about CSS might affect how beginners perceive the language. If they are taught that CSS is easy, they might be discouraged from seeking help when they struggle or just blame the language. Admitting you struggle with something is difficult enough. Let’s not make it worse for them.

I feel it’s better if we acknowledge the complexity of writing CSS. We might also stop looking down on CSS specialists. Today, they’re often considered half a developer, whilst (JavaScript) software engineers (or UI engineers, or whatever they’re called nowadays) that know little about the complexities of CSS are considered full-fledged front-end developers. We see this on social media, in job openings, and in pay.

Thanks to Danielle, Hidde, Sven, and Thijs for reviewing this post.