Modular CSS, an example
If my last article about avoiding nested selectors for more modular CSS left your head spinning, you are not alone. After all, one of the coolest things about Sass is that you can nest selectors. Why would you want to give that power up?
I'm glad you asked. Perhaps an example would help. For myself, these principles really started to make sense when I was building a menubar that looked something like this:
To build this menubar, I started out with the following HTML:
<ul class="menubar">
<li>
<a href="#">File</a>
<ul>
<li><a href="#">Open</a></li>
<li><a href="#">Save</a></li>
<li><a href="#">Save as…</a></li>
<li><a href="#">Close</a></li>
<li class="separator"></li>
<li><a href="#">Exit</a></li>
</ul>
</li>
<li>
<a href="#">Edit</a>
<ul>
<li><a href="#">Cut</a></li>
<li><a href="#">Copy</a></li>
<li><a href="#">Paste</a></li>
</ul>
</li>
<li>
<a href="#">Help</a>
<ul>
<li><a href="#">About</a></li>
</ul>
</li>
</ul><ul class="menubar">
<li>
<a href="#">File</a>
<ul>
<li><a href="#">Open</a></li>
<li><a href="#">Save</a></li>
<li><a href="#">Save as…</a></li>
<li><a href="#">Close</a></li>
<li class="separator"></li>
<li><a href="#">Exit</a></li>
</ul>
</li>
<li>
<a href="#">Edit</a>
<ul>
<li><a href="#">Cut</a></li>
<li><a href="#">Copy</a></li>
<li><a href="#">Paste</a></li>
</ul>
</li>
<li>
<a href="#">Help</a>
<ul>
<li><a href="#">About</a></li>
</ul>
</li>
</ul>This is fairly standard markup for a dropdown menu system. Lists within lists seem to be the semantic method of choice. To minimize the impact on my HTML I decided to try to code it with only one class ("menubar") which I applied to the outer element.
Using nested selectors
I then styled it using nested selectors:
ul.menubar {
background: white;
list-style: none;
padding: 0 10px;
> li {
display: inline-block;
position: relative;
> a {
color: black;
display: block;
padding: 10px 14px;
text-decoration: none;
&:hover {
background: #29a7f5;
color: white;
}
}
> ul {
display: none;
position: absolute;
top: 100%;
background: white;
padding: 10px 0;
> li > a {
color: black;
display: block;
padding: 8px 20px;
text-decoration: none;
&:hover {
background: #29a7f5;
color: white;
}
}
}
&.is-selected {
> a {
background: #29a7f5;
color: white;
}
> ul {
display: block;
}
}
}
}ul.menubar {
background: white;
list-style: none;
padding: 0 10px;
> li {
display: inline-block;
position: relative;
> a {
color: black;
display: block;
padding: 10px 14px;
text-decoration: none;
&:hover {
background: #29a7f5;
color: white;
}
}
> ul {
display: none;
position: absolute;
top: 100%;
background: white;
padding: 10px 0;
> li > a {
color: black;
display: block;
padding: 8px 20px;
text-decoration: none;
&:hover {
background: #29a7f5;
color: white;
}
}
}
&.is-selected {
> a {
background: #29a7f5;
color: white;
}
> ul {
display: block;
}
}
}
}To get it to work correctly, I found myself using a lot of child selectors (">"). This was necessary because of the lists within lists. I didn't want the same styles for the menubar to be applied to the dropdown menus, so I had to explicitly indicate which "li" or "a" tag I was refering to. Child selectors let me do this. Without child selectors it's actually an even bigger mess to style a menu like this because you must turn off styles that were set for parent "li" elements on decendants. Which makes it much harder.
A more modular approach
The menu system I was working on was actually much more complicated than the code shown above. In fact, the code spanned several screens which made it even harder to follow with all of the nesting. To maintain my own sanity I decided to add a couple of classes to the HTML to make it easier to style and evolved my CSS to something more like this:
.menubar {
list-style: none;
font-size: 14px;
background: white;
padding: 0 10px;
> li {
display: inline-block;
position: relative;
}
}
.menubar-item {
color: black;
display: block;
padding: 10px 14px;
text-decoration: none;
&:hover,
.is-selected & {
background: #29a7f5;
color: white;
}
}
.menu {
display: none;
position: absolute;
top: 100%;
background: white;
list-style: none;
width: 15em;
padding: 10px 0;
.is-selected & {
display: block;
}
}
.menu-item {
color: black;
display: block;
padding: 8px 20px;
text-decoration: none;
&:hover {
background: #29a7f5;
color: white;
}
}.menubar {
list-style: none;
font-size: 14px;
background: white;
padding: 0 10px;
> li {
display: inline-block;
position: relative;
}
}
.menubar-item {
color: black;
display: block;
padding: 10px 14px;
text-decoration: none;
&:hover,
.is-selected & {
background: #29a7f5;
color: white;
}
}
.menu {
display: none;
position: absolute;
top: 100%;
background: white;
list-style: none;
width: 15em;
padding: 10px 0;
.is-selected & {
display: block;
}
}
.menu-item {
color: black;
display: block;
padding: 8px 20px;
text-decoration: none;
&:hover {
background: #29a7f5;
color: white;
}
}By flattening the structure of the CSS I was able to make it much easier to understand and modify. It also helped me conceptually to think about my styles in terms of objects instead of lists. In this example I have a "menubar" and a "menu". Each of those objects have their own child objects called "items" which are distinguished with the "menubar-item" and "menu-item" classes.
Not only is my CSS easier to read, but I was also able to combine two of the rules into one:
&:hover,
.is-selected & {
background: #29a7f5;
color: white;
}&:hover,
.is-selected & {
background: #29a7f5;
color: white;
}Above I was able write one rule for the hover and selected states of "menubar-item". Before I had written two rules because it was easier with all of the nesting. Keeping your selectors simple will make it easier for you to combine rules when necessary.
Another thing to note is with a tiny bit of modification I could probably make the "menu" styles work for context menus, not just menubars. Which is what modularity is all about.
Striking a balance
Some of you may have noticed that in the code above I still have a couple of nested selectors. In particular the pseudo class for hover and the class for the selected case. These are both state classes and are a very good use of nesting.
But I also have one selector that is still referencing a child "li" element ("> li"). I wrote it this way because it seemed to be the best balance between the number of classes required and the added weight to my markup. Not everyone would agree with me that this is the best way to mark it up. Some would say that you should ONLY style using classes and avoid tag names entirely. You'll have to find your own balance for your own projects. My point is simply that avoiding nesting can make your Sass code easier to read and maintain.
The Examples
To write this article I created several CodePen projects to illustrate various levels of modularity:
- Simple menu 1 - the non-modular, nested approach.
- Simple menu 2 - a more modular approach.
- Simple menu 3 - an ultra-modular approach (using only class selectors).
Let me know which approach you like the best.
Originally published on The Sass Way.
Comments
You might also like…
Avoid nested selectors for more modular CSS
We've written before about the dangers of nesting your CSS selectors too deeply. The Inception Rule is a good one for getting you to avoid some mangled CSS selectors. But there's actually a lot of benefit to taking this concept a couple of steps farther. What happens when you avoid nesting for almost all of your major selectors?
Modular CSS naming conventions
The more you write your own stylesheets, the more you begin to value using good names in your code. Naming is by far one of the most difficult and debated activities of a developer. To many, naming is an art form.
AI, design, and the modern frontend on the Changelog & Friends podcast
Back in January, I posted on LinkedIn about using AI to build a documentation site for Opine. This prompted Adam Stokoviak to reach out about joining him on the Changelog & Friends podcast to talk about AI and the modern frontend. Adam and I have been friends for a very long time now, having collaborated on The Sass Way together. He's a great host, and really put me at ease as we talked about how I'm using AI in my own workflow as a designer. On the show we discussed how I'm integrating AI into my workflow as a designer. We also talked about Framer and Figma, Next.js, and other topics.
Introducing the ContextStore CLI
Your AI agent can read local files, but it doesn't know about your other ContextStore spaces. The cstore CLI bridges that gap — giving any agent access to all your context from any project.
