Ever felt like CSS is playing tricks on you? Despite its outward simplicity, CSS has layers of complexity that can even confuse the best of developers. Beginners often jump into using CSS without fully understanding the why and how behind its behavior. While it's not a bad practice to jump right in and start experimenting, the nature of CSS will eventually lead to confusion and frustration. 😤
In this article, I want to explore some of the generally more unknown and overlooked aspects of CSS. Throughout the article, I'll share common practices to bring your CSS under control and avoid falling down the "WTF, how did that happen?" rabbit hole.
I hope you'll find this article helpful no matter your level of expertise.
Let's get started! 🚀
Table of Contents
Understanding the core of CSS
Before we dive into more specific CSS traps, let's have a look at core concepts to build a fundamental intuition on how CSS works. Having the right mental model helps a lot in predicting how CSS will behave in different scenarios.
CSS started as a simple language to style documents. You could write paragraphs, headings, and lists, and then style them with CSS. But as the web evolved, so did CSS. With this evolution, we had to adapt to more and more requirements of the web. Here are a few examples:
- Responsive design: Making sure your website looks good on all screen sizes, browsers, and devices.
- Reliability: A small syntax error should not crash your entire website.
- Customization: Users should be able to customize your website to their liking. Example: dark mode, font size, etc.
- Accessibility: Making sure your website is accessible to everyone, including people with disabilities. Adapting to screen readers, keyboard navigation, and more.
- Reusability: Making sure that code is easily reusable as you have common components like buttons, inputs, etc.
- Looks: While considering all the above, your website should still look good.
- Developer experience: Making sure that developers can write and maintain CSS easily and quickly.
It's impressive how CSS has evolved to meet these requirements while still being simple to write. Most of the time! 😅
As you can imagine a lot is going on under the hood. There are so many rules that decide what to fall back to when a CSS feature is used in a way that was not intended. These fallbacks are decided based on the needs of the web as a whole, and not just your website. And this is where the confusion starts.
There are so many topics I could cover, but I'll only focus on a few and leave the rest up to you. At the end, after you have read this article you can have a look at the resources section to learn more.
Formatting context
The first core concept I want to talk about is the formatting context. If you have worked with CSS for a while, you should already have an intuition about how this works. And even if you do you'll benefit from knowing the details behind it.
On your website, you have a lot of elements. And each element "behaves" in the context of its formatting context. The two main formatting contexts are:
-
Block formatting context: In this context, the element will take up the entire width of its parent and will start on a new line. These elements are called block-level elements. Examples are
div
,p
,h1
,section
, etc. -
Inline formatting context: In this context, the element will only take up as much width as it needs and will continue on the same line if there is enough space. These elements are called inline-level elements. Examples are
span
,a
,strong
,em
, etc. You can't change the width or height of inline-level elements nor can you add top and bottom margins.
The main way of defining how an element behaves is the display
property. The newer syntax is display: <outer> <inner>
. The outer value can be block
or inline
defining the formatting context of the element. The inner value is how children of the element will behave. Example: display: inline flex;
(Older version for browser support: display: inline-flex;
). This will place the element in the same line as the last element if there is enough space and the children will behave like flex items. Here are a few more:
/* Current */ /* New Syntax */
display: block; /* display: block flow; */
display: inline; /* display: inline flow; */
display: inline-block; /* display: inline flow-root; */
display: flex; /* display: block flex; */
display: inline-flex; /* display: inline flex; */
display: grid; /* display: block grid; */
display: inline-grid; /* display: inline grid; */
display: flow-root; /* display: block flow-root; */
This brings us to the next core concept...
Layout Modes
The second part of the display
property is the layout mode. This is how the children of the element will behave. The default and most intuitive layout mode is flow
(Normal flow). Children in a normal flow layout will simply stack on top or next to each other based on the formatting context. This is happening by default when you start adding elements to your website. Besides the normal flow, there are a few more common layout modes:
- Flex layout: This is a one-dimensional layout. You can align items horizontally or vertically. This is great for navigation bars, sidebars, and more. This layout has a ton of features and gotchas that deserve an article on their own. If you want to learn more check out the resources section.
- Grid layout: This is a two-dimensional layout. You can align items in rows and columns. This is great for complex layouts like a dashboard, a gallery, and more. This layout also has a ton of features and gotchas that deserve an article on their own.
-
Positioned layout: This is a layout where you can position elements anywhere on the page. This is great for tooltips, modals, and more. While we don't use the
display
property to define this layout, it's still part of the layout modes and we'll cover its gotchas later. - Float layout: This is a layout where you can float elements to the left or right of inline-level elements. This is great for wrapping text around images. This layout is not used as much anymore and has a lot of gotchas. We'll cover this one too.
You probably asked yourself what flow-root
is. We will get to that when we talk about CSS Gotchas next.
CSS Gotchas
Margin collapse
Margin collapse is when the top and bottom margins of two elements collapse into one margin. It was originally intended to make the vertical spacing between your elements more consistent. There are a TON of rules that decide whether margins collapse or not. No one wants to remember all of them and many don't even know about margin collapse. So it's important to know how to avoid it and how to fix it when it happens.
Margins collapse happens in the following scenarios (these are not all of the rules):
- The elements are adjacent.
- No padding, border, or clearance separates the two elements.
- The elements are in the same formatting context. A new formatting context is created when:
- You have a float, flex item, grid item, or absolutely positioned element (with
absolute
orfixed
). - The element has a
display
ofinline-block
,flow-root
,flex
,grid
,inline-flex
,inline-grid
, and a few more. - The element has an overflow other than
visible
andclip
(hidden
,auto
,scroll
, oroverlay
). - ...
- You have a float, flex item, grid item, or absolutely positioned element (with
- If one of the elements is empty or its height is zero.
- ...
As you can see you don't want to ever see this list again. So let me just give you an interactive example of what margin collapse looks like and how to avoid it.
Generally, you shouldn't use margins for everything. When you have a card, section, button, header, or any other element where you always want to create space around the content but inside the element use padding. Margins are mainly for two things:
- Horizontal space between elements. Like icons in text for example.
- To create vertical space between sections and paragraphs. Here only use
margin-top
to avoid margin collapsing altogether.
A tip for avoiding margin collapse for content sections is to use a "Lobotomized Owl" selector. With it, you can add margin-top
to all children except the first one essentially putting a margin between all children just like the gap
property of flex.
/* Add margin-top to all children except the first one */
.section > * + * {
margin-top: 1rem;
}
If you use Tailwind CSS you can use the space-y
class to achieve the same effect.
<div class="space-y-4">
<p>...</p>
<p>...</p>
<p>...</p>
</div>
Another tip is to always check your elements with the developer tools to see where your margins end up. And if they collapse and you don't want them to, now you know how to fix it (for example by creating a new formatting context with display: flow-root;
). Often you accidentally create a new formatting context and your paddings look like they got bigger. Now you know why.
Stacking context
Visualize a series of nested boxes, where each box can contain several smaller boxes inside. Each of these smaller boxes can, in turn, contain even more boxes, creating a complex, multi-layered structure. This analogy shows the concept of stacking contexts in CSS.
In this metaphor, each box represents an element with its own stacking context. The z-index
property determines the stacking order of elements within their particular box. However, it's important to realize that z-index values only apply within the same box or stacking context. This means a smaller box nested inside cannot be placed above its containing box, regardless of its z-index value. This is where the gotcha comes in.
Many assume z-index is a universal scale, where higher values always appear on top of lower values across the entire page, similar to expecting a small, inner box to sit on top all outer boxes if it's marked with a higher number. The reality is that z-index only organizes elements within their immediate box or stacking context. An element with a z-index of 1000 inside a nested box won't necessarily be above an element with a z-index of 1 in another, outer box.
This is a common source of confusion, especially when working with complex layouts or nested components. I'm sure you've encountered a situation before where you put the z-index at 9999999 and it still didn't work. So let's see when these boxes (stacking contexts) are created.
Oh no, here we go again 💀:
- The
<html>
element creates a stacking context by default. - An element with an
opacity
value less than1
. - An element with one of these properties:
transform
,filter
,backdrop-filter
,perspective
,clip-path
,mask
. - An element with a
position
valueabsolute
orrelative
andz-index
value other thanauto
. - An element with a
position
value offixed
. - An element with a
mix-blend-mode
. - An element with an
isolation
value ofisolate
. - A flex or grid item with a
z-index
value other thanauto
. - An element with a
will-change
value of any of the above properties. - And a few more...
Oof, that's a lot of ways to create a stacking context. So how can we avoid this? Well, you can't really avoid it but you can decrease the chance of fighting with z-index in the following ways:
- Avoid using
position: absolute;
just to center elements. - Don't use
z-index
on non-positioned elements. (Elements without aposition
). - Use consistent
z-index
values. For example, use only10
,20
,30
,40
,50
, etc.
Lastly, keep stacking context in mind when working with properties like opacity
, transform
, filter
, and mix-blend-mode
.
Specificity
In CSS, specificity is the set of rules that determines which style declarations are applied to an element when more than one rule could apply. However, complexity in specificity can lead to a CSS labyrinth, making it challenging to predict and control which styles will win. To understand specificity, consider an example where we have an HTML element with both a class and an ID selector applied to it:
#product-highlight {
background-color: yellow;
}
.product {
background-color: blue;
}
Despite both styles applying to the same element, the background color will be yellow because ID selectors have a higher specificity than class selectors. The same goes for complex selectors like .product > .highlight
or .product.highlight
. The former has a higher specificity because it's more specific. Most of the time you'll be fine but as your project grows you will run into specificity issues more often and the solution is not always simple. To avoid these issues, it's best to keep specificity as low as possible.
To solve this while keeping CSS readability high you could adopt naming conventions like BEM (Block Element Modifier). BEM aims to make CSS more maintainable by reducing specificity conflicts through a flat structure of class names. This involves naming your CSS classes like this: .block__element--modifier
.
- Block: Standalone entity that is meaningful on its own. (Like a card, button, or header)
- Element: A part of a block that has no standalone meaning and is semantically tied to its block. (Like a title, subtitle, or button text)
- Modifier: A flag on a block or element. Used to change appearance or behavior. (Like a button with a primary color or a card with a shadow)
<div class="card card--highlight">
<h2 class="card__title">Product Name</h2>
<p class="card__description">Product Description</p>
</div>
.card {
...;
}
.card--highlight {
...;
}
.card__title {
...;
}
.card__description {
...;
}
By using only class selectors, all selectors have the same specificity level and you won't run into specificity issues. Another issue I've often seen is with using SCSS. People (especially beginners) often nest their selectors just like their HTML. This wouldn't only lead to a specificity nightmare but also to big CSS files. So avoid nesting your selectors if you don't benefit from it.
/* I'm sorry but this just hurts to look at */
section {
.card {
.title {
i {
font-size: 13px;
}
}
}
}
Floats
For the last gotcha, I want to talk about floats. Floats were mainly used to wrap text around images. They were also used to create complex layouts before Flexbox and Grid became a thing. But nowadays you should avoid using floats as much as possible. They have a lot of gotchas and are often misunderstood by newer developers.
Floats are often used to just put stuff on the left or right. This might work but will lead to headaches later on. Here is what happens when you use floats:
- They are removed from the normal flow.
- They are placed to the left or right of only inline-level elements.
- They become block-level elements.
- They create a new formatting context. (Meaning they disallow margin collapse)
- They have their own stacking rules (between non-positioned elements and positioned elements)
As you can see, they do a bit more than just putting stuff on the left or right. If you don't intend to absolutely position the element in a way where only inline-level elements are affected (like text) don't use floats.
Instead, use Flexbox or Grid. They are much more powerful and easier to use. For aligning inline-level elements use text-align
instead.
Conclusion
I'd love to continue listing more gotchas but I want to encourage you to explore the MDN Web Docs on your own. They are extremely valuable and when you understand more and more of the underlying concepts of CSS you will have fewer and fewer issues writing it. I hope this article was helpful and you learned something new 😊. If you have any questions or feedback, feel free to leave a comment. I'd love to hear from you. Otherwise, feel free to share this article and give it a like. Thank you for reading and happy coding! 🚀
Resources
- Box Model: MDN/box-model
- Stacking Context: MDN/stacking-context
- Layout Mode: MDN/layout-mode
- Display: MDN/display
- Margin Collapse: MDN/margin-collapse
- Containing Block: MDN/containing-block
- Specificity: MDN/specificity
- Float: MDN/float
- Flexbox: MDN/flex
- Grid: MDN/grid
- BEM: BEM/introduction
- Lobotomized Owl: CSSTricks/lobotomized-owls by ChrisCoyier
Top comments (7)
We should not forget, where it all came from. Do you know, why HTML can set a text to boldface, but not to red or blue?
HTML was initially designed with scientific text in mind, and the guis at CERN used terminals. HTML can show pretty much everything a terminal could show this times.
If you display a text in a browser without CSS, it looks pretty boring, the browser uses only the default formatting. CSS was designed to change this defaults. This was quite efficient, as you need to transfer the formatting only once. People used acoustic-couplers with 300 bit/s to transfer data, so a low bandwidth was a crucial factor.
HTML was created in 1991, CSS was in 1994, but the creators where no designers, they where both employed at the CERN, a scientific organization. Looking back, it would be far more logical to use some kind of full featured formatting language like HPGL, which was created in 1977. But HPGL is quite bulky, so this did not meet the requirements.
There are quite a lot of strange concepts that come from this history. There are no local definitions in HTML, ID´s and CSS-classes are always global. There is no way to repeat elements or build some kind of "macro". At least some kind of preprocessor would be great to control the expession of text. But all this is missing.
People tell you, there is a "separation of concerns". HTML is for the structur, CSS for the styling and JS for the reactivity. If you thoroughly analyze, what each part is used for, you will find that all the functions are mixed in the tools. If you need CSS to display or hide part of your text, this has nothing to do with styling.
The most strange thing to me is the fact, that browsers consume ASCII-text. It would have been far more efficient to define some kind of "object code" that is complied locally and is transferred to the browser, like it is done in most programming languages. Even text processors do not write down plain text. Then we would not use HTML and CSS at all today, as it was only one among several ways to build websites. But even this can probably only be explained from history.
Thanks for sharing your insights! CSS was definitely a ride in the past and still is. Let's see how it moves forward.
I like nesting selectors to avoid specificity issues, especially with my coworkers. It's clearer for anyone not knowing the project. I will read into BEM tho, sounds interesting.
Very insightful read 🔥
Thanks! 😄
Nesting isn't a bad thing overall when you know what you are doing. However for beginners in Sass projects it's very easy to over-nest and then fight with global styles or styles in another part of your file. Especially when you have a huge project and style files approach 1k lines. I love Sass but I've also seen projects written in it that were an absolute dumpster fire and Sass will not complain about it. Stylelint would help keep the code cleaner but from a beginner's perspective, it would probably just decrease DX by a ton. But this is mostly relevant for huge projects.
I’ve been meaning to write basically this exact post for a long time. Awesome job, great work highlighting some of the most common pitfalls and hidden assumptions CSS makes.
Thank you! 🙃
It was quite challenging to cover relevant parts without explaining the generally more boring theory but I think it's still very valuable to go into the details just to get into the right state of mind about writing styles defensively. While the article could have been written in a more structured way I wanted to highlight that exploring and researching is much better than reading a summarization and forgetting it the next day.
Great explainer post! Definitely prefer BEM over nesting, better readibility overall. TIL about stacking contexts (and how it affects z-index), definitely will need to experiment with it.