Writing CSS is really simple and straightforward, so why is there a need for principles and best-practices while writing CSS?
As the project scope increases and as the number of people working on the project increases, the problems become more and more apparent and can cause serious issues down the line. Fixing issues may become harder, duplicated code, complex override chains and use of !important
, leftover / unused code (removed elements or features), code that is hard to read, etc.
Writing CSS at a professional level will make the CSS code more maintainable, extensible, understandable and cleaner. We're going to look at the five simple and very effective principles that will take your CSS to the next level.
Naming principle
"There are only two hard things in Computer Science: cache invalidation and naming things." -- Phil Karlton
Properly naming and structuring your CSS selectors is the first step to making your CSS more readable, structured and cleaner. Establishing rules and constraints in your naming convention makes your code standardized, robust and easier to understand.
This is why concepts like BEM (Block-Element-Modifier), SMACSS (Scalable and Modular Architecture for CSS) and OOCSS (Object Oriented CSS) are popular among many frontend developers.
Low specificity principle
Overriding CSS properties is very useful, but things can go out of hand pretty quickly on more complex projects. Overriding chains can get really long and complex, you might be forced to use !important
to solve the specificity issue and you could get really easily lost when debugging or adding new features.
/* Low-specificity selector */
.card {}
/* High-specificity selectors */
.card .title {}
.blog-list .card img {}
.blog-list .card.featured .title {}
#js-blog-list .blog-list .card img {}
Browser and specificity
One of the benefits of following the low specificity principle is performance. Browsers parse the CSS from right to left.
Let's take a look at the following example:
.blog-list .card img {}
Browsers parse the selector like this:
- Find all
img
elements on the page - Keep selected elements that are the descendants of
.card
class - Keep selected elements that are the descendant of
.blog-list
class
You can see how high-specificity selectors impact performance, especially when we need to globally select generic elements like div
, img
, li
, etc.
Using the same level of specificity
By using low specificity CSS class selectors in combination with BEM or one of the other naming principles mentioned in the previous section, we can create a performant, flexible and understandable code.
Why use CSS classes? We want to keep the same level of specificity, stay flexible and be able to target multiple elements. Element selectors and id selectors do not offer the flexibility that we need.
Let's rewrite our previous example using BEM and keeping specificity low.
/* Low-specificity selector */
.card {}
/* Fixed high-specificity selectors */
.card__title {}
.blogList__image {}
.blogList__title--featured {}
.blogList__img--special {}
You can see how these selectors are simple, understandable and can be easily overridden and extended if needed. And by keeping them low-level (a single class), we are guaranteed optimal performance and flexibility.
DRY Principle
DRY (Don't repeat yourself) principle can be also applied to CSS. Duplicated code in CSS can cause code bloat, unnecessary overrides, reduce maintainability, etc. This issue can be fixed by structuring the code appropriately and having high-quality documentation.
Storybook is a great free tool that enables you to create an overview of available frontend components and write high-quality documentation.
/* Without DRY Princple */
.warningStatus {
padding: 0.5rem;
font-weight: bold;
color: #eba834;
}
.errorStatus {
padding: 0.5rem;
font-weight: bold;
color: #eb3d34;
}
.form-errorStatus {
padding: 0.5rem 0 0 0;
font-weight: bold;
color: #eb3d34;
}
Let's refactor the code so it follows the DRY principle.
/* With DRY Principle */
.status {
padding: 0.5rem;
font-weight: bold;
}
.status--warning {
color: #eba834;
}
.status--error {
color: #eb3d34;
}
.form__status {
padding: 0.5rem 0 0 0;
}
Single responsibility principle
By using the single responsibility principle in our CSS, we can ensure that our CSS classes are easily extended and overriden. Let's take a look at the following example.
.button {
padding: 1rem 2rem;
font-size: 2rem;
border-radius: 0.2rem;
background-color: #eb4934;
color: #fff;
font-weight: bold;
}
.button--secondary {
border-radius: 0;
font-size: 1rem;
background-color: #888;
}
We can see that if we want to extend .button
class with .button--secondary
, we are doing lots of overrides to achieve what we need, when we only want to apply a different background color and keep the default styles.
The problem is that our .button
class is having several roles:
- Sets layout (
padding
) - Sets typography (
font-size
,font-weight
) - Sets presentation (
color
,background-color
,border-radius
)
this makes our CSS classes very hard to extend and combine with other CSS classes. By keeping this in mind, let's use BEM and OOCSS to improve our CSS.
/* Shared styles */
.button {
padding: 1rem 2rem;
font-weight: bold;
color: #fff;
}
/* Style extensions */
.button--radialBorder {
border-radius: 0.2rem;
}
.button--large {
font-size: 2rem;
}
.button--primary{
background-color: #eb4934;
}
.button--secondary {
background-color: #888;
}
We have broken down our button
styles into several classes that can be used to extend the base button
class. We can optionally apply the modifiers and add new ones as the design changes or new elements are being added.
Open/Close principle
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
We've already used the open/close principle in the previous examples. All new features and options need to be added by extension. Let's take a look at this example.
.card {
padding: 1rem;
}
.blog-list .card {
padding: 0.5em 1rem;
}
The .blog-list .card
selector has few potential issues:
- Some styles can be applied only if the
.card
element is a child of.blog-list
element. - Styles are forcibly applied to the
.card
element if placed inside the.blog-list
element, which can produce unexpected results and unecessary overrides.
Let's rewrite the previous example:
.card {
padding: 1rem;
}
.blogList__card {
padding: 0.5em 1rem;
}
We've fixed the issue by having a single class selector. With this selector, we can avoid unexpected effects and there are no conditional nested styles.
Conclusion
We've seen how by applying these few simple principles we have significantly improved the way we write CSS:
- Standardized naming and structure, and improved readability by using BEM, OCSS, etc.
- Improved performance and structure by using low-specificity selectors.
- Reduced code bloat and improved code quality with DRY principle
- Flexibility and maintainability by using open/close principle
- etc.
These articles are fueled by coffee. So if you enjoy my work and found it useful, consider buying me a coffee! I would really appreciate it.
Thank you for taking the time to read this post. If you've found this useful, please give it a ❤️ or 🦄, share and comment.
Top comments (21)
Adrian I have another principle that conflicts with some of your own, but I feel would solve some of the problems you're sharing.
Don't Unset Yourself.
That is – do not contradict a previous style that you have applied.
Your example:
Would be much better served as:
This way you do not have to worry about the order of properties to ensure that your classes work as intended. You only ever add styles, instead of setting and then unsetting them.
Nice overview - It's good to know the actual names of these tips (like "Low specificity"); that helps with remembering it :)
Also, didn't realize that browsers search for selectors from right to left! That's interesting... I wonder how much overhead it saves once you realize that - do you know of any studies / tests that show that?
Thank you. Glad you found the article useful.
Regarding the performance studies, I am not aware of any, but if you come across any, please link them.
"Writing CSS is really simple and straightforward" = BS.
CSS is getting better but it is still a mess. Brower compatibility probs, compiler-generated CSS, resets, browser pre-fixes, box type, and old legacy days from when they let print designer sit at the table to develop specs...
Oh ya and silly issues reading selector from right to left when we read it top to bottom and the browser typically reads everything else from start to finish . . . etc etc.
CSS is almost as much of a mess as JavaScript.
This article deals with the basics of how we write CSS (selectors and properties concept) and how to write flexible and scalable CSS, not browser compatibility issues, CSS compilers, vendor prefixes, etc.
I wasn't saying anything like that. I'm just saying that CSS is not simple and straightforward anymore. I still enjoyed your article though and look forward to seeing another.
Thank you for clarifying and I'm glad you've enjoyed the article.
CSS indeed had a messy history and the syntax suffered for it, but it keeps improving year after year.
Thank you for the very detailed and insightful answer. I guessed as much regarding the performance and the worst-case scenario. In any case, I think that having a single selector is the best way to go in terms of performance, code readability and flexibility.
In any case, having several levels of CSS selectors brings up some other issues, as described in "Open/Closed principle".
I teach webdesign basics and this was incredibly helpful and gave me some ideas on how to approach things with my students. Specificity often is something that leave them scratching their head.
Thanks!
Thank you, glad you found it helpful. I wish my teachers would show me these best practices from the start.
I use BEM at work but I prefer ECSS at home. ecss.io/
Nice. Thank you for sharing. I might use it as well on my projects
There is definitely some good takeaways, I highly recommend it.
Everytime i see BEM I can't stop thinking how ugly, inconvenient and repetitive it is.
That is very true for BEM, but generally it's not a problem if you don't nest classes more than 2-3 levels.
I sometimes combine BEM with the "old ways" in order to not have deep nesting of BEM on complex elements. Works fine, still namespaced (but you need to have control of the whole project and be reasonable enough not to write a global ".specificelement" class.
.block__element--modifier .specificelement {}
Very helpful. Thanks!
Thank you. Glad you've found it helpful.
Excellent article, thank you very much.
Thank you
Great one ! Clean and simple explanation.
Thank you