BEM Modifiers in Pure CSS Nesting

Content

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.

An example of my code with BEM and SCSS that I wrote when I started my developer journey.
For what purpose I used SCSS when I was learning.

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.

Webmentions

If you liked this article and think others should read it, please share it. Leave comments on platforms such as dev.to, twitter.com, etc., and they will magically appear here ✨

  • 0 reposts
  • 18 comments
  • @lizaveis portrait.

    @lizaveis

    Great article πŸ™Œ Thanks for sharing it

  • @mitevskasar portrait.

    @mitevskasar

    That's a great approach! I'll be using this from now on πŸ˜„

  • @what1s1ove portrait.

    @what1s1ove

    Hey @lizaveis ! Thank you! Use it with pleasure!

  • @what1s1ove portrait.

    @what1s1ove

    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! ❀️

  • @darkwiiplayer portrait.

    @darkwiiplayer

    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.

  • @what1s1ove portrait.

    @what1s1ove

    Hey @darkwiiplayer !

    At this point, what's the point of using BEM in the first place? Why not just do .tag-list__tag.github … see more

  • @darkwiiplayer portrait.

    @darkwiiplayer

    Ideally, CSS would have a selector that combines [attr~=value] and [attr$=value] where you could write [attr~$=value] to match any element where attr is a space-separated list of keywords and one of them ends with value.

    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.

  • @latobibor portrait.

    @latobibor

    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 />

    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.

  • @peter-fencer portrait.

    @peter-fencer

    Tailwind are free up form CSS name declaration slavery.

  • @darkwiiplayer portrait.

    @darkwiiplayer

    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, where github 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.

  • @what1s1ove portrait.

    @what1s1ove

    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 πŸ₯²

  • @what1s1ove portrait.

    @what1s1ove

    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! … see more

  • @schmoris portrait.

    @schmoris

    @darkwiiplayer

    At this point, what's the point of using BEM in the first place? Why not just do .tag-list__tag.github? … see more

  • @what1s1ove portrait.

    @what1s1ove

    I would say not with the author's approach, but with the BEM's approach 😁

  • @latobibor portrait.

    @latobibor

    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.

  • @what1s1ove portrait.

    @what1s1ove

    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.

  • @peter-fencer portrait.

    @peter-fencer

    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.

  • @what1s1ove portrait.

    @what1s1ove

    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!

These are webmentions via the IndieWeb and webmention.io. Mention this post from your site:

Not an Easter Egg (but actually it is, yes 😁)