Writing good CSS: 11 Best Practices
- css
- best-practice
Introduction
Through my years of using CSS, I have found some principles to stand the proof of time while others did cause me many headaches in the long run. I want to share with you the those that stood the test of time. If you have some good advice that is absent from this list please let me know, I am as much willing to learn as to share.
CSS is curious in that it is very easy to learn but near impossible to master.
Note: The advice in this article is concerned with the developer experience of CSS, and not with design, performance, or accessibility (though these are important). The following points will especially be of value to you if you need to maintain and extend your CSS codebase over longer periods.
Contents
- Keep specificity low
#id
selectors are evil- Treat
!important
like raw eggs - Avoid chaining (or nesting) of selectors
- Don’t combine element and class selectors
- Organize your code by increasing specificity
- Use few properties per class
- Don’t worry about sorting your properties
- Follow a naming convention like BEM
- Avoid using shorthands if possible
- Keep presentation (CSS), semantics (HTML) and behavior (JS) separate
1. Keep specificity low
It is really important to understand CSS specificity first, here is an excellent article. In essence, whenever you want to overwrite styles applied by one selector, you need to exceed or at least match its specificity.
Bad: specificity of first selector is too high
#main .nav li {
color: white;
}
.nav li.is-active {
color: red; /* dominated by #main */
}
Good: using as few selectors as possible
.nav-item {
color: white;
}
.nav-item.is-active {
color: red;
}
To avoid the specificity wars, you want to avoid adding selectors that are not necessary. Instead of .nav ul li
use only .nav li
or better just .nav-item
. Points 2. - 6. of this article are hands-on practices to help you to keep your specificity low.
#id
selectors are evil
2. Id selectors have the highest specificity. This means whenever you use one, you are forced to use it again every time you want to modify the element’s style. This might work well when you write your code in the first place but causes a headache whenever you want to extend it. I recommend generally avoiding #id
selectors in your stylesheet.
Bad: forced to use the #footer selector repeatedly
#footer .box {
background-color: #333;
}
#footer .box:hover {
background-color: #444;
}
Good: avoid using #id selectors in you stylesheet
.box {
background-color: #333;
}
.box:hover {
background-color: #444;
}
Further Reading: This article of Harry Roberts shows how you can use ids without increasing specificity
!important
like raw eggs
3. Treat !important
is the ace of spades of CSS, it is the ultimate trump card, and it should be treated as such. If you use it to escape the frustration of dealing with specificity which has gotten out of hand, then you are using it wrong. Use it sparingly, with selectors that have a very narrow scope and where you are certain that you want it to overpower all other styles.
Bad: using !important to deal with specificity frustration
#header .nav ul li a {
border-bottom: 2px solid transparent;
}
.nav a:hover {
border-bottom-color: black !important;
}
Good: calculated use with narrow scope
.text-center {
text-align: center !important;
}
Note: If you want to customize a CSS framework like Bootstrap that sometimes uses selectors with high specificity you may not have a choice
4. Avoid chaining (or nesting) of selectors
Especially with pre-processors like Sass, it is very easy to nest selectors which, when compiled to actual CSS, will be chained like this: .a .b .c .d { ... }
This might seem attractive because it helps you to write very specific CSS, but it also leads to bloated CSS. Let’s say you have a .list
class which is used inside .header
and also inside .footer
.
Bad: selectors are unnecessarily chained
.list {
list-style: none;
margin: 0;
}
.list .list-item {
display: inline-block;
}
.header .list .list-item {
margin-right: 1em;
}
.footer .list .list-item {
font-weight: bold;
}
Good: using as few selectors as possible
.list {
list-style: none;
margin: 0;
}
.list-item {
display: inline-block;
}
.list--spaced .list-item {
margin-right: 1em;
}
.list--bold .list-item {
font-weight: bold;
}
There are two problems with this:
- To style the
.list-item
, the.list
has to be carried along to match specificity. - After a while you may be afraid to touch
.list .list-item
for fear that you break the.list-item
in.header
or.footer
and have to undo it. You will probably end up writing any changes in.header .list .list-item
or.footer .list .list-item
directly, thus bloating your CSS and making it harder to keep a good overview.
The solution is not to use chaining/nesting with .header
and .footer
but to add modifier classes which can be used in connection with the .list
class instead. Now you can write an element like this inside your .footer
:
<ul class="list list--bold">
<li class="list-item">&copy; Lukas Hermann</li>
</ul>
Not only does this reduce specificity (why use .list .list-item
if you can just use .list-item
), it only introduces higher specificity where we want it: when using a modifier.
A note concerning this specific example: In the case of lists it is probably easier to use .list li
(and .list--bold li
respectively) to save yourself some classes in your HTML markup, but I wanted to make an example which is easy to understand.
Note: This principle lies at the heart of naming conventions like BEM which will be addressed more specifically below.
5. Don’t combine element and class selectors
When you start writing CSS it seems intuitive to keep your classes very specific. We all have been burned by the cascading nature of CSS and it can be upsetting if you make a change in your CSS intended for one small part of your site only to discover that you messed up the design of five other places.
With this background, it makes sense to write selectors like div.card a.link
because we want to make sure that only anchor tags with .link
inside a div with .card
will receive this styling. But doing this is like fighting windmills, but CSS features are not our enemy. As a rule, don’t combine element and class selectors, ever! If you want to modify the styling of an element in a particular place, use a modifier class instead.
Bad: mixing element and class selectors
a.link {
color: blue;
}
div.card a.link {
color: green;
}
Good: using a modifier class instead
.link {
color: blue;
}
.link--green {
color: green;
}
The advantages of modifier classes:
- The purpose of the modification is easy to understand
- It’s easy to change or remove
- It’s easy to find all the places in your code where this modifier is used by doing a simple search for
link--green
in your whole project
Note: A probable exception to this role might be a .btn
component. If you want to make sure that an anchor tag a.btn
looks identical to a button tag button.btn
because these two elements behave a little different in browsers. But even then you may just as well add all the necessary styling to .btn
.
6. Organize your code by increasing specificity
If two selectors have the same specificity, the latter will be used. Therefore, to play to the strengths of the CSS specificity feature, it makes sense to organize your CSS file by increasing specificity. Put your resets and element selectors first, your normal classes afterward, and finish with your utility classes like .text-center
or .nowrap
where you can then safely use !important
.
Bad: unordered styles makes it easy to miss overwrites
.text-center {
text-align: center !important;
}
.btn {
background-color: green;
}
a {
color: blue;
}
.btn {
/* overwrites green */
background-color: red;
}
Good: ordering your styles roughly by specificity
/* Elements */
a {
color: blue;
}
/* Components */
.btn {
background-color: green;
}
.box {
border-radius: 3px;
box-shadow: 0px 3px 8px black;
}
/* Utilities */
.text-center {
text-align: center !important;
}
The advantages of ordering styles roughly by specificity:
- You will always overwrite less important styles with more important ones
- You have a rough guideline for where your things go inside your CSS file
Note: This principle was introduced as ITCSS by Harry Robers. It is a really useful CSS methodology to handle large-scale products. Here is the article introducing ITCSS. Harry even created a course on Skillshare just recently.
7. Use few properties per class
As a general rule, the more properties a class has, the less likely you are going to reuse it. Simply because of the fear that some of the many properties will not fit your other use case and you have to undo them, thus you might even end up writing another class with very similar properties.
A good way to know if your classes are too bloated:
- Simply observing yourself how many resets (like
margin: 0;
) you are writing to undo other classes properties - If you feel uncomfortable about reusing classes or if feel bad about adding another property to a class, it’s a sign this class is too bloated
Classes become bloated because many different concerns (aesthetics, spacing, typography) are packed together. A great way to improve this, even without dropping any properties, is to simply break it up into its different concerns.
Bad: bloated, properties of different concerns are mixed
.card {
border-radius: 3px;
box-shadow: 0 5px 10px black;
padding: 2rem 1rem;
font-size: 14px;
line-height: 1.3;
margin-bottom: 2rem;
}
Note: The bloated example is not overly bloated yet, but I think it brings the point across.
Good: fewer properties keeps classes reusable
.card {
border-radius: 3px,
box-shadow: 0 5px 10px black;
padding: 2rem 1rem;
}
.spacer {
margin-bottom: 2rem;
}
.text-small {
font-size: 14px;
line-height: 1.3;
}
In the good example, the .card
class only contains the properties which specifically define its aesthetics. The aspects of spacing and typography are outsourced into their own classes. Having broken up the class like this you can easily see how to reuse each of those classes and even extend them and add more classes like .spacer-large
or .text-tiny
which will be very useful.
Yes, this means that you have to add more classes in your HTML like so <div class="card spacer text-small">
, and that’s okay. It is much more comfortable, especially in the long run, to use such classes.
Some classes need a lot of properties, for example, a .button
class has to unset many user agent (browser) styles for both, the <button>
and the <a>
tag. Therefore this rule should not be followed religiously for every case.
Tipp: Most CSS frameworks come equipped with a good set of reusable utility classes, watch out for them and use them!
8. Don’t worry about sorting your properties
This one is a bit opinionated. Brad Frost did a survey on twitter how people order their CSS properties:
Having fun debating different clustering of CSS properties. How do you do it? Alphabetical? Clustered by type (box model, positioning, typography, etc)? Train wreck?
— Brad Frost (@brad_frost) February 5, 2019
Most people answered that they are grouping properties by some common traits like positioning, display, or color. Some may be OCD about it and sort it alphabetically. Personally, I have tried these different methods and haven’t found any of them particularly useful in the long run. I basically always do scrambled egg … or how Brad Frost put it, “train wreck”.
If your classes have many properties some sorting rules usually become imperative to maintain an overview. But if you keep your classes slim, like suggested in point 7, it is really not necessary. Simply abide by some simple rules (like layout first, aesthetics last), and don’t worry too much about the order otherwise.
9. Follow a naming convention like BEM
Some of the code examples in this article are using the BEM naming convention. I found it to be very helpful and recommend it wholeheartedly. It’s easy to learn! Quickly head to this small introduction with good code examples of the BEM naming convention to get the idea.
Here are two points I struggled with at the beginning with BEM.
Don’t nest elements
If you have a title within a header within a card, only “nest” maximum one level deep like so:
Nesting BEM selector produces ugly classes
.card {}
.card__header {}
.card__header__title {}
Nesting BEM only one level deep is better
.card {}
.card__header {}
.card__title {}
It’s best to use modifiers only on root elements
Modifiers on children can be used, but they make it hard to remember which child has which modifier. If possible, apply the modifier to the root element, this will keep your HTML markup and your CSS cleaner and therefore easier to see which modifiers are used or available.
Using BEM modifiers on children get's messy
.card__header--large { padding: 2em; }
.card__content--large { padding: 0 2em; }
.card__footer--large { padding: 2em; }
Restricting BEM modifiers to root elements is cleaner
.card--large .card__header { padding: 2em; }
.card--large .card__content { padding: 0 2em; }
.card--large .card__footer { padding: 2em; }
10. Avoid using shorthands if possible
When defining a background color it is very common to just use shorthands like background: gray;
. It is important to know, however, that this shorthand reset’s all other background properties, like background-position
and so on, to initial
. Similar case with other shorthands like border
and margin
.
This causes problems only in the rarest of cases, but if it does, it will be very hard to debug. It is therefore best practice to only change as little as necessary. Be safe and use background-color
etc., the prevented headache is worth the extra characters.
Using shorthands can have unintended side effects
.card {
background: lightgray;
border: 0;
margin: 0 2rem;
}
Using verbose properties helps prevent side effects
.card {
background-color: lightgray;
border-width: 0;
margin-left: 2rem;
margin-right: 2rem;
}
Further reading: Harry Roberts makes a great case for this in CSS Shorthand Syntax Considered an Anti-Pattern
11. Keep presentation (CSS), semantics (HTML) and behavior (JS) separate
I often see styling tied to ids or elements (like #nav
or .nav ul li
), or javascript behavior tied to a class (like document.getElementsByClassName(".nav")
). However, if you do this you rob yourself of the flexibility to keep presentation (CSS), semantics (HTML), and behavior (JS) separate. You want to be able to style something as a button, regardless if it is an <a>
, <button>
or <div>
(note that the latter is not good for accessibility). Or if you create a navbar, you want to be able to use <li>
-tags or <div>
-tags depending on the necessary semantics, without losing your styling or have to change the CSS.
To achieve this use ids for javascript (as originally intended), or better, data
-attributes like so: document.querySelectorAll('[data-nav]');
and don’t mingle the button
element selector with your styles. This gives you the freedom to change either semantics, presentation, or behavior independently without having to worry about the other two.
Bad: mixing presentation and behavior
<button class="btn btn-login">Login</button>
<script>
const el = document.getElementsByClassName('.btn-login');
el.addEventListener('click', function (event) { ... }, false);
</script>
<style>
.btn-login {
color: white;
background-color: green;
}
</style>
Good: presentation and behavior are kept separate
<button class="btn btn-green" data-login>Login</button>
<script>
const el = document.querySelectorAll('[data-login]');
el.addEventListener('click', function (event) { ... }, false);
</script>
<style>
.btn-green {
color: white;
background-color: green;
}
</style>
Exception: There are reasons to use element selectors for styling. For example, if you intentionally set global presets, like a reset or change the default link color. But be careful not to set presets that you find yourself overwriting all the time.