When I was starting to learn web development, pure CSS often remained in the realm of theory. When it came to practice, especially working on real projects, pure CSS was a rarity. The market and the industry itself dictated that styles should be written using preprocessors.
Fortunately, over time, this trend has almost disappeared. Pure CSS now includes many features that were previously missing, causing people to prefer preprocessors.
Even though I have not used preprocessors for a long time, at least in my personal projects, there is one thing I missed while working with them: using the parent selector to create modifiers when using the BEM methodology. I thought for a long time that I would never be able to avoid duplicating the full selector when writing modifiers in pure CSS. However, while redesigning my website, I found a convenient way for myself to write BEM modifiers using native CSS nesting.
My Path with BEM & SCSS
I hate preprocessors. Be it SASS, SCSS, LESS, Stylus, or any other. Really, without any exceptions. Though, I think my hatred for preprocessors is not because of the technology itself, but because of how other people use them. Throughout my development career, I have often encountered tickets where a seemingly simple task, like changing the text size, which should take minutes, ended up taking me hours. This is because people often want to use the tool to its full extent. As a result, to find where I needed to change the text size in projects using preprocessors to the max, I had to go through several mixins, maybe a loop or even nested loops, and several nested selectors using the parent selector. Brr, just thinking about it gives me the shivers.
Like many who started their careers during the peak popularity of preprocessors, I initially did not spend much time on pure CSS. The industry, including the job market, dictated the need for preprocessor knowledge, and mentions of pure CSS in job listings were rare. I was no exception, and almost from the beginning of my learning journey, I began writing styles using SCSS. I was very fortunate with my mentors, as they instilled in me a great love for pure CSS, despite the use of preprocessors. SCSS was chosen deliberately because it closely resembles pure CSS compared to other preprocessors. Additionally, out of all the features preprocessors offer, we used only two: compiling all .scss
files into one file and nesting with the parent selector, exclusively for pseudo-classes/pseudo-elements and creating modifiers using the BEM methodology. All other preprocessor features, such as loops and mixins, were forbidden, as they were meant to solve specific problems rather than being used just because they exist.
Later on, after my training, almost all of my work projects involved some preprocessor. It was during these experiences that I developed my strong dislike for preprocessors. In my personal projects, however, I never used preprocessors and wrote everything in pure CSS, adding precise enhancements through plugins for my .css
bundler. For example, in the past, when I was bundling my .css
files using PostCSS, I used a simple plugin called postcss-import to bundle all .css
files into one final file (such as in this blog. Here is one of the first commits in this repository). I rarely used any additional plugins for CSS.
I always tried to avoid nesting in preprocessors wherever possible, but with the introduction of native CSS nesting, I gradually began incorporating it into my personal projects. It seems to me that native CSS nesting works more intuitively and correctly compared to nesting in preprocessors. One key difference with native CSS nesting compared to preprocessor nesting is that it does not have the functionality of a parent selector to create new selectors. This is what I used during my training for BEM modifiers, and it was perhaps the only thing I started to miss when using native CSS nesting.
Native CSS Nesting Modifiers
Before native CSS nesting became available, I had to describe all class modifiers with separate selectors. During those times, I particularly missed the functionality of creating a new selector using the parent selector in preprocessors. This was because the full selector for a modifier could be duplicated dozens of times.
.tag-list__tag {
--background-color: var(--color-red-200);
padding-block: 2px;
padding-inline: 6px;
background-color: hsl(var(--background-color) / 30%);
}
.tag-list__tag--html {
--background-color: var(--color-green-100);
}
.tag-list__tag--github {
--background-color: var(--color-gray-100);
}
.tag-list__tag--ts {
--background-color: var(--color-blue-200);
}
But even with the introduction of native CSS nesting, I did not immediately solve this problem because native CSS nesting simply does not have anything functionally similar to creating a new selector through the parent selector functionality found in preprocessors.
The first thing I started using more actively was various attributes, such as aria-current="page"
, rel="prev"
/rel="next"
, name
, and others. Just this alone significantly helped reduce the number of class modifiers. However, not all modifiers can be covered with attributes, and in my code, there still remained a sizable number of places where the selector was duplicated entirely to create a BEM modifier.
I tried googling for solutions because BEM methodology markup is quite popular, but all the code examples and repositories I found were doing the same thing – simply duplicating the entire selector.
One day, as I was adding new modifiers and duplicating the entire selector, I decided to experiment. Like everyone starting out with CSS, I learned about attribute selectors. I was no exception and went through that section, but truthfully, I rarely used these selectors. However, I completely forgot that attribute selectors can be used with any attribute, including class
(because it is odd to use an attribute selector for a class
when you can just use a class selector, right?). Then it struck me – I also remembered about additional operators available in attribute selectors, specifically substring matching selectors, which work perfectly with native CSS nesting. Here is how it looks:
.tag-list__tag {
--background-color: var(--color-red-200);
padding-block: 2px;
padding-inline: 6px;
background-color: hsl(var(--background-color) / 30%);
&[class*='--html'] {
--background-color: var(--color-green-100);
}
&[class*='--github'] {
--background-color: var(--color-gray-100);
}
&[class*='--ts'] {
--background-color: var(--color-blue-200);
}
}
It turned out quite close to the modifiers that I used to write using the parent selector in SCSS at the beginning of my learning, didn't it?
Perhaps not as concise as using the parent selector to create new selectors in preprocessors, but personally, I prefer the approach with native CSS nesting and attribute selectors paired with substring matching selectors much more. As I mentioned earlier, native CSS nesting is much clearer and more logical to me in understanding.
Here you can find the PR where I applied this trick across my entire project.
Conclusion
I love CSS and all its functionality. It is gratifying to see how the foundational aspects of CSS from the very beginning seamlessly complement the new functionalities emerging within it. Moreover, this synergy works in both directions.
I have never been part of the group of people who say that native CSS should incorporate everything found in tools like SASS or other preprocessors. To me, these are different tools, and this small trick using attribute selectors together with substring matching selectors in native CSS nesting for BEM modifiers &[class*="--modifier"]
has finally fulfilled all my wishes that I had when using SCSS and other preprocessors. However, CSS continues to evolve, and on the horizon, we can already see native CSS mixins (one of the reasons why I always reluctantly talk about SCSS or other preprocessors).
Once upon a time, when native CSS nesting was just starting to be discussed, I thought, "Nesting? In pure CSS? I will never use that!" But over time, I got used to it, and now I even like it. Will the same happen with native CSS mixins, or, heaven forbid, native CSS loops? I want to say no, but I will not make predictions. At the very least, with experience, I have become acquainted with a wonderful tool like Stylelint and its life-easing rules such as max-nesting-depth and others. Hopefully, it will prevent me from becoming a hater of pure CSS someday.
Top comments (18)
At this point, what's the point of using BEM in the first place? Why not just do
.tag-list__tag.github
?Also, have you considered modifies being prefixes of one another? In that case,
.tag-list__tag--github
would also match a rule like[class*="--git"]
if you happen to have both of those, so you'd have to make sure no modifier is a prefix of another.@darkwiiplayer
The only downside that comes to my mind is, if someone else in a big project adds something like a global
.github { border: 1px solid red; }
which then will also be applied to<div class="tag-list__tag github"></div>
, whereas with the author's approach thing would stay isolated.I would say not with the author's approach, but with the BEM's approach 😁
Hey @darkwiiplayer !
You are right, that option is also possible. But for me personally, that wouldn't be BEM modifiers. Plus, in that case, "github" would be considered as a BEM block, which I don't really like.
There are other substring matching selectors. For example,
^
(the end of attribute value) can be used like[class^="--git"]
and[class^="--github"]
, and everything will work. I have shared snippets from my project and I am totally happy with it. However, I do share your concern that this is not a very safe option compared to using preprocessors.Ideally, CSS would have a selector that combines
[attr~=value]
and[attr$=value]
where you could write[attr~$=value]
to match any element whereattr
is a space-separated list of keywords and one of them ends withvalue
.That way one could just check
[class~$="--modifier"]
in a relatively safe way. At that point, the only problem would be.foo-list__foo--test
and.bar-list__bar--test
would both match the--test
selector, but that's probably a good bit less likely.I've got confused with a thing here when you wrote "would be considered as a block". Maybe I misunderstood something here, but I understand this
.tag-list__tag.github
to look like this in HTML:<div class="tag-list__tag github" />
Then I don't see how this would result in a new block (or maybe in the
css
file? 🤔). I think I get something wrong here.I think what they mean is that in BEM notation, a single class would correspond to a block.
Think of a class like
github__element--modifier
, wheregithub
is the block part; so on its own,github
is just the surrounding block level item without any modifiers. I don't use BEM so I might be misunderstanding some of the nuance here.Hey @latobibor !
Thank you for your comment. Indeed, without context, it's hard to understand which block I meant. I have revised the comment a bit, and @darkwiiplayer has described everything correctly as well, thank you!
Overall, BEM is primarily about conventions, and nothing stops you from adapting them to the needs of your project. Personally, in this project where I used this approach, each BEM block is in a different CSS file. If I were to use
github
as a separate BEM block, I would need to put it in a separate file as well, which I don't like.Thank you everyone for the explanation! Once they pointed out "B" is standing for
block
I understood it immediately.I also didn't get it as I'm more pragmatic about it; I can tolerate some files containing other things as long as the file is small and no one is reusing the code in it.
It's not so much about tolerating it, but more about the fact that it violates the BEM rules, which state that styles of one block should not be present in the files of another block.
Great article 🙌 Thanks for sharing it
Hey @lizaveis ! Thank you! Use it with pleasure!
That's a great approach! I'll be using this from now on 😄
Hey @mitevskasar ! Thank you! I really like it too! When I found it, I wanted to share it because I was looking for something similar, but I couldn’t find any information. Use it with pleasure! ❤️
Tailwind are free up form CSS name declaration slavery.
Hey @peter-fencer !
But it also frees up from other features available in CSS. Although I am not a fan of atomic (class-based) CSS technologies. Here, I cannot support or oppose you 🥲
Very rare case to found where Tailwind is not able to solve CSS problem. Even I can use slice-9-grid image scaling with them. Maybe some animation is the pure CSS is better.
But for dark/light them switching and overall layout is 2 important area where TW is truly shining.
Yes, you are right, dark/light theme switching is one example. Most of the CSS standards being developed now are not for atomic CSS. However, we'll see, if the Tailwind team will find a way to use the new things in CSS!