We are learning CSS the wrong way.
CSS looks deceptively simple. We just declare how something should look in a couple of rules, and the browser works its magic. This results in many courses and learning resources teaching CSS in a practical, easy to follow, quick results approach based pretty much in memorizing rules like a robot.
Even some of the most popular resources fall short when it comes to teaching CSS theory: the extremely awesome freeCodeCamp just brushes through it, always-trusty W3Schools pretty much just lists the properties and values, and even premium courses out there just focus on building something fast without even mentioning some of the core concepts.
Well, that’s exactly where the problem lies. For starters, we humans are not that good at memorizing a ginormous (and ever-growing) set of rules, but also, this leads into skipping through CSS theory concepts, which comes back to bite us every time. You know how the joke goes:
Two CSS properties walk into a bar.
A barstool in a completely different bar falls over.
And there’s some merit to that, the declarative nature of CSS makes it act in unexpected ways, especially for developers used to working with imperative languages (a.k.a. “a proper programming language”) and well-defined scopes.
But there are four core concepts that I believe, if taught correctly, would save us from 99% of CSS issues: block formatting context, box model, stacking context, and the cascade (particularly specificity).
I am pretty sure the block formatting context is the lesser known, so let´s start there.
Block formatting context
The classic method of CSS layout is flow layout, a.k.a. “normal flow”. Considering western languages, block layout simply means “inline” elements (such as <span>
) flowing left to right, “block” elements (such as <p>
) flowing one below the other.
Seems extremely simple… until we introduce floats and margin-collapsing to the picture.
When two block-level elements are stacked, their vertical margins collapse together. This makes sense, for instance, in paragraphs where we would like to have a 16px margin at the top and bottom, but definitely not a 32px margin between them. Collapsing margins between siblings seem like a reasonable decision and one that saves us quite a bit of work for the given use case. But in other instances, especially when dealing with negative margins, it causes all kinds of “weird” behaviour.
Margins also collapse between parents and children which, more often than not, is not something we would like to see.
To add insult to injury, sometimes we make use of margin collapsing, go on with our work, and all of a sudden something makes it not collapse anymore…
As for floats, we have all been there; when the content is smaller than the float it will overflow the element we intended to use as a container (or in other words, the container background and border now fall short of the floated element). Most of the time we have been sorting this out with the clear property, commonly adding an extra (pseudo)element to inject a clearfix.
Well, it turns out that if we were using block formatting contexts properly, we wouldn’t be facing these issues.
But what are block formatting contexts?
A block formatting context defines the scope on which margins will collapse and floats will be contained. It’s a mini-layout in your layout. It contains everything inside of the element creating it.
When we create a block formatting context, we’re telling the browser “treat this as an autonomous piece of layout for floats and margins concerns”.
I wish I could say “We can create one by setting the rule display:flow-root on the containing element” … but that’d be an oversight. This is a relatively new addition to the specs, and browser support for it is still far from perfection.
For a long time, BFCs were created as a side effect for other properties, or in the worst case, as an accidental side effect of applying other properties or using some HTML elements.
Which properties and elements? Lots of them:
- The root of the document
- Table cells, table captions, and any element on which we set the display to a table part (display: table, display: table-cell, display:table-row, etc)
- Inline-block elements
- Absolute and fixed positioned elements
- Multicolumn containers (elements with column-count, column-span, or column-width other than auto, including column-count:1) — technically a column formatting context
- Grid items and Flex items — technically grid formatting context and flex formatting context
- Elements where overflow has any value other than visible
There are lots of ways of (perhaps accidentally) creating a new block formatting context. The most popular is using overflow: auto, since it shouldn’t cause any other issues except potentially adding scrollbars when a specific size is added to the element and content overflows it.
A personal favourite of mine is going with column-count: 1, as it has almost no side effects.
The best thing to do is use whatever you feel like the better approach for the given use case, but make sure to leave a comment stating that you’re using that rule to force a new BFC. Otherwise, it’s highly likely that other developers misinterpret the rule as wanting to create scrollbars, or column layout, or whatever, and might even choose to delete that (since column-count:1 will probably not make sense to them)
Here’s a quick demo of what happens when we apply BFCs to different elements of a typical layout that include a float:
As you can see, applying the BFC is really handy to prevent the floats from overflowing the container. It’s also great to stop text from wrapping around the float. I’ve seen people use a side margin equal to the width of the float to accomplish this, but using the BFCs approach allows us to work with an unknown-width float.
Knowing how a BFC works gives us much better control of our floats and margins, allow us to make better layouts, and prevents us from accidentally breaking them. So use it consciously and carefully. For a deeper dive, I recommend this article by Rachel Andrew.
Cascade and specificity
Browsers use an algorithm known as the Cascade (the C in CSS) to decide which rules apply to each element when they encounter conflicting declarations (i.e. more than one rule assigning different values to an element’s property).
The first factor in the algorithm is source order. Max Stoiber broke the Twitter web development community some months ago with a simple CSS question:
If you answered “both blue”, congrats, that’s the right choice. But don’t feel bad if you missed it… you certainly are not alone.
The real trick in the question is that browsers don’t look at the order on which the classes are declared in the HTML, but in the CSS.
If you come from an imperative programming language background, it might help to think about each CSS declaration as a conditional statement, against which all your HTML elements (“objects”) will be compared, to determine which values will be applied to the different properties of that object.
In a quick and dirty pseudo code, this
.red { color: red; }
.blue { color: blue; }
could be thought of as
if element matches ".red" then element.color = red;
if element matches ".blue" then element.color = blue;
So the final value for the color property of an element that matches both “.red” and “.blue” will be “blue”. Remember, it’s the order in the CSS that matters, not in the HTML. Even repeating “red” a million times in the HTML won’t make it change its color.
This
testing
.red{color:red}
.blue{color:blue}
still results in a blue output.
But the order in the CSS source is only one of many factors in the cascade algorithm…
On CSS specificity
A really big part of determining which rules should apply to an element is known as specificity. Different types of CSS selectors carry different weights. In order of priority:
- id selectors ( #something)
- class selectors ( .something) & pseudo-classes (:hover)
- elements (p)& pseudo-elements (:before)
Combinators (+, > and ~), universal selectors (*
) and the :not pseudo-class have no effect on specificity.
A rule defined via an ID will always take precedence over one defined via a class, and a class over an element selector.
When we use combined selectors (such as body #login button .red{}) the browser will count the number of ids, (pseudo)classes, and (pseudo)elements and assign a specificity value to the rule, in order to compare it to the others and decide which one to use. This is normally represented as three counters:
ids, classes, elements
For instance, the rule body #login button .red{} has a specificity of 1 id (#login), 1 class (.red) and 2 elements (body + button). So the rule’s specificity can be represented as 1, 1, 2.
Considering this, given two conflicting rules, the one with higher specificity will be applied.
In
.btn .big { height: 40px }
.btn { height: 20px }
the former is going to be applied, as it has a specificity of 0, 2, 0 vs 0, 1, 0 on the latter.
It’s important to remember that the more specific type of selector always takes precedence. A rule that has 1, 0, 0 specificity (that is, a single id, such as #red{color: red}) will be considered more important than another with 0, 20, 10 (0 ids, 20 classes and 10 elements… not that you would really want to write such a ridiculous selector anyway).
So in order to overwrite a rule defined with 1 id, you’ll need to have another with at least 1 id and 1 element/class. Or at least match the specificity and let the order in the source take control. This is why it's generally considered a good practice to avoid using ids for styling , as it could be extremely hard to overwrite.
Going back to the algorithm, we can imagine this as each HTML element having a related styling table where it writes each property, the current value, and the specificity of the rule that had set it.
<h1 id="main-title" class="red"> This is a title </h1>
.red{ color: red; }
h1#main-title{ border-bottom: 2px solid black}
This will give the H1 the following styles
If our stylesheets had additional, conflicting declarations, the Cascade will check the specificity that had set the current value for a given property, compare it to the specificity of the new rule, and change the value only if the specificity of the new rule is higher or equal to the previous one.
Specificity is not a bug but a feature and can be really handy when used properly. It also can be extremely frustrating when not. There are plenty of architectures that help to deal with the cascade in order to write scalable, maintainable CSS. So if there’s one CSS concept you should really master, this is it.
Some methodologies such as BEM encourage using flat selectors (i.e. avoiding complex selectors and just using a class for each thing) to prevent specificity wars altogether, while others such as the original OOCSS let you write higher specificity code in a mindful way. On the other hand, functional CSS tends to simply avoid conflicting declarations.
It doesn’t really matter which architecture your team choose, the important part is having an architecture that everyone follows to avoid issues.
There are other factors in the cascade algorithm, such as higher priority to inlined CSS and declarations using the !important keyword.
Benjamin Johnson has a great article on the cascade in this publication, so that’s a great place for further learning on the subject. I also really liked Emma Wedekind’s recent article.
Box model
Each element in the HTML creates a rectangular “box” to represent it, which will have a set of rules applied to it to determine the width and height it should occupy in the browser as well as how much of it is content vs spacing.
The only exceptions are lists and tables that create two boxes each, and elements with display: noneor display: contents which creates no box at all.
The box has the following measures applied to it:
In modern CSS, we have two main ways to define the box, which we can switch using the value of the property box-sizing.
The traditional, and therefore, default one is called content box. In content-box, the declared width and height is interpreted as the dimensions of the content, and padding and borders can add to the element actual rendered size, which can be extremely frustrating.
For instance, if we set a width of 50% on two left floated elements, they’ll line up perfectly side by side. But the moment we add some padding or border to them, each one will occupy more than 50% of the parent, and therefore, the second one will wrap below the first.
This regularly meant using weird calcs and pre-processor variables to compensate, and in the old times, actually having to calculate and adjust the widths every time we changed the borders or padding.
IE6 to the rescue
Yup, I just said IE6. Believe it or not, the solution to this issue was inspired by a weird bug in the old browser. When IE6 was set to quirks mode, the **width
property would set the total rendered width of the element** , while paddings and borders would be taken out from the content area, keeping a stable and predictable total width. This is pretty much what we now know as “border box”.
Nowadays most developers choose to reset everything to border box, so it’s extremely likely that you’ll find some version of this snippet in your codebase
*, *:before, *:after {
box-sizing: border-box;
}
There are some variations on the above, with some people arguing for inheriting box-sizing
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
It’s funny how the most used sizing in CSS can be thought as “remember that weird bug in IE6?… really wish we could do that”.
For further research, there’sa great guide on the box model and box-sizing at CSS-tricks.
Stacking context
CSS is primarily a 2d styling system, but it can also be very powerful in dealing with 3d and sorting things in the z-axis. As a rule of thumb, when two or more elements overlap, they will be stacked by their order in the source: elements that appeared last in the HTML will be above (“closer to the user”).
Positioning the elements (setting position to any value other than the default static) and using the z-index property allows us to control how layers of our design stack against each other. But if you got more than five minutes of experience with it, I’m sure you have run into its issues. Many times we set the stack in perfect order, then something breaks it inexplicably.
Turns out several properties create a stacking context, which is a local stack of elements amongst which z-index applies. We can move elements inside each local stack, as well as the stacks around in the broader stacks, but we can’t interpolate elements from a stack with elements from another.
Some of the properties that create a stacking context are:
- Positioned elements with z-index other than auto
- Elements with an opacity below 1
- Elements with mix-blend-mode other than normal
- Elements with clip-path, mask, filter, or transform other than none
- other properties
Here’s a classic challenge that helps to grasp the concept: given the following code, try to move the red box behind the green without changing its z-index, position, or the HTML source.
Hint: the trick is in the containing divs.
The solution is creating a new stacking context in the div containing the red span, so the z-index of the red span won’t affect the main stack, letting the source order take over.
Notice that even if we gave the red span a z-index of one million, it will not move in front of the green and blue ones as it is isolated in a local stack. If we want to move it, we need to change the position and z-index of the containing div.
Again, Benjamin Johnson covered Stacking Contexts in depth for this publication, so that’s a great source for further understanding of how this works. I also recommend Phillip Walton’s article on z-index, that’s the source for the above challenge and a great dive into how stacking contexts works.
Conclusion
CSS is an extremely powerful styling tool.Whether you choose to go with a pre-processor and “vanilla” CSS or opt for a CSS-in-JS solution, these core concepts are something you should really master to avoid recurring issues.
With many people coming from boot camps that put the emphasis on JS (as is the industry itself), and with the proliferation of pre-packaged solutions such as Bootstrap, the quality of HTML and CSS has taken a toll.
It’s been ten years since Chris Coyier said it, but this quote is still relevant:
CSS is like chess. You can learn the basics in a day and spend a lifetime mastering it.
Some people are convinced the newcomers disrespect HTML and CSS and consider it “below” them, precisely for how easy it is to learn the basics. While I believe some of those people exist, I tend to disagree. I’m convinced most people actually are trying to master it, only they are lost between no longer relevant info and way too many bad resources, and that’s where this frustration comes from.
The real trick to mastering CSS is how you approach it. If you can use a bit of advice, stay away from the “quick results” tutorials, stay away from the frameworks, and stay away from property lists. Get hands-on experience and focus on learning the theory, give the specs a good read, dive into MDN reference on every CSS thing you learn, no matter how small it seems to be, you’ll always find some interesting concepts that makes it “click” into place.
No pre-processor, no CSS-in-JS, and no framework will get CSS to make sense otherwise.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.
The post [The only reason your CSS fails](https://blog.logrocket.com/the-only-reason-your-css-fails-8e4388d562af/ appeared first on LogRocket Blog.
Top comments (0)