{"id":375144,"date":"2022-11-21T05:59:15","date_gmt":"2022-11-21T13:59:15","guid":{"rendered":"https:\/\/css-tricks.com\/?p=375144"},"modified":"2023-01-17T07:13:00","modified_gmt":"2023-01-17T15:13:00","slug":"taming-the-cascade-with-bem-and-modern-css-selectors","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/taming-the-cascade-with-bem-and-modern-css-selectors\/","title":{"rendered":"Taming the Cascade With BEM and Modern CSS Selectors"},"content":{"rendered":"\n

BEM. Like seemingly all techniques in the world of front-end development, writing CSS in a BEM format<\/a> can be polarizing. But it is \u2013 at least in my Twitter bubble \u2013 one of the better-liked CSS methodologies.<\/p>\n\n\n\n

Personally, I think BEM is good, and I think you should use it. But I also get why you might not.<\/p>\n\n\n\n

Regardless of your opinion on BEM, it offers several benefits, the biggest being that it helps avoid specificity clashes in the CSS Cascade. That\u2019s because, if used properly, any selectors written in a BEM format should have the same specificity score (0,1,0<\/code>). I\u2019ve architected the CSS for plenty of large-scale websites over the years (think government, universities, and banks), and it\u2019s on these larger projects where I\u2019ve found that BEM really shines. Writing CSS is much more fun when you have confidence that the styles you\u2019re writing or editing aren\u2019t affecting some other part of the site.<\/p>\n\n\n\n

There are actually exceptions where it is deemed totally acceptable to add specificity. For instance: the :hover<\/code> and :focus<\/code> pseudo classes. Those have a specificity score of 0,2,0<\/code>. Another is pseudo elements \u2014 like ::before<\/code> and ::after<\/code> \u2014 which have a specificity score of 0,1,1<\/code>. For the rest of this article though, let\u2019s assume we don\u2019t want any other specificity creep. 🤓<\/p>\n\n\n\n

But I\u2019m not really here to sell you on BEM. Instead, I want to talk about how we can use it alongside modern CSS selectors \u2014 think :is()<\/code>, :has()<\/code>, :where()<\/code>, etc. \u2014 to gain even more<\/em> control of the Cascade<\/a>.<\/p>\n\n\n\n\n\n\n

What\u2019s this about modern CSS selectors?<\/h3>\n\n\n

The CSS Selectors Level 4 spec<\/a> gives us some powerful new(ish) ways to select elements. Some of my favorites include :is()<\/code><\/a>, :where()<\/code><\/a>, and :not()<\/code><\/a>, each of which is supported by all modern browsers and is safe to use on almost any project nowadays.<\/p>\n\n\n\n

:is()<\/code> and :where()<\/code> are basically the same thing except for how they impact specificity. Specifically, :where()<\/code> always has a specificity score of 0,0,0<\/code>. Yep, even :where(button#widget.some-class)<\/code> has no specificity. Meanwhile, the specificity of :is()<\/code> is the element in its argument list with the highest specificity. So, already we have a Cascade-wrangling distinction between two modern selectors that we can work with.<\/p>\n\n\n\n

The incredibly powerful :has()<\/code><\/a> relational pseudo-class is also rapidly gaining browser support<\/a> (and is the biggest new feature of CSS since Grid<\/a>, in my humble opinion). However, at time of writing, browser support for :has()<\/code> isn\u2019t quite good enough for use in production just yet.<\/p>\n\n\n\n

Lemme stick one of those pseudo-classes in my BEM and…<\/p>\n\n\n\n

\/* ❌ specificity score: 0,2,0 *\/\n.something:not(.something--special) {\n  \/* styles for all somethings, except for the special somethings *\/\n}<\/code><\/pre>\n\n\n\n

Whoops! See that specificity score? Remember, with BEM we ideally want our selectors to all have a specificity score of 0,1,0<\/code>. Why is 0,2,0<\/code> bad? Consider a similar example, expanded:<\/p>\n\n\n\n

.something:not(a) {\n  color: red;\n}\n.something--special {\n  color: blue;\n}<\/code><\/pre>\n\n\n\n

Even though the second selector is last in the source order, the first selector\u2019s higher specificity (0,1,1<\/code>) wins, and the color of .something--special<\/code> elements will be set to red<\/code>. That is, assuming your BEM is written properly and the selected element has both the .something<\/code> base class and .something--special<\/code> modifier class applied to it in the HTML.<\/p>\n\n\n\n

Used carelessly, these pseudo-classes can impact the Cascade in unexpected ways. And it\u2019s these sorts of inconsistencies that can create headaches down the line, especially on larger and more complex codebases.<\/p>\n\n\n

Dang. So now what?<\/h3>\n\n\n

Remember what I was saying about :where()<\/code> and the fact that its specificity is zero? We can use that to our advantage:<\/p>\n\n\n\n

\/* ✅ specificity score: 0,1,0 *\/\n.something:where(:not(.something--special)) {\n  \/* etc. *\/\n}<\/code><\/pre>\n\n\n\n

The first part of this selector (.something<\/code>) gets its usual specificity score of 0,1,0<\/code>. But :where()<\/code> \u2014 and everything inside it \u2014 has a specificity of 0<\/code>, which does not increase the specificity of the selector any further.<\/p>\n\n\n

:where()<\/code> allows us to nest<\/h3>\n\n\n

Folks who don\u2019t care as much as me about specificity (and that\u2019s probably a lot of people, to be fair) have had it pretty good when it comes to nesting. With some carefree keyboard strokes, we may wind up with CSS like this (note that I\u2019m using Sass for brevity):<\/p>\n\n\n\n

.card { ... }\n\n.card--featured {\n  \/* etc. *\/  \n  .card__title { ... }\n  .card__title { ... }\n}\n\n.card__title { ... }\n.card__img { ... }<\/code><\/pre>\n\n\n\n

In this example, we have a .card<\/code> component. When it\u2019s a \u201cfeatured\u201d card (using the .card--featured<\/code> class), the card\u2019s title and image needs to be styled differently. But, as we now<\/em> know, the code above results in a specificity score that is inconsistent with the rest of our system.<\/p>\n\n\n\n

A die-hard specificity nerd might have done this instead:<\/p>\n\n\n\n

.card { ... }\n.card--featured { ... }\n.card__title { ... }\n.card__title--featured { ... }\n.card__img { ... }\n.card__img--featured { ... }<\/code><\/pre>\n\n\n\n

That\u2019s not so bad, right? Frankly, this is beautiful CSS.<\/p>\n\n\n\n

There is a downside in the HTML though. Seasoned BEM authors are probably painfully aware of the clunky template logic that\u2019s required to conditionally apply modifier classes to multiple elements. In this example, the HTML template needs to conditionally add the --featured<\/code> modifier class to three elements (.card<\/code>, .card__title<\/code>, and .card__img<\/code>) though probably even more in a real-world example. That\u2019s a lot of if<\/code> statements.<\/p>\n\n\n\n

The :where()<\/code> selector can help us write a lot less template logic \u2014 and fewer BEM classes to boot \u2014 without adding to the level of specificity.<\/p>\n\n\n\n

.card { ... }\n.card--featured { ... }\n\n.card__title { ... }\n:where(.card--featured) .card__title { ... }\n\n.card__img { ... }\n:where(.card--featured) .card__img { ... }<\/code><\/pre>\n\n\n\n

Here\u2019s same thing but in Sass (note the trailing ampersands<\/a>):<\/p>\n\n\n\n

.card { ... }\n.card--featured { ... }\n.card__title { \n  \/* etc. *\/ \n  :where(.card--featured) & { ... }\n}\n.card__img { \n  \/* etc. *\/ \n  :where(.card--featured) & { ... }\n}<\/code><\/pre>\n\n\n\n

Whether or not you should opt for this approach over applying modifier classes to the various child elements is a matter of personal preference. But at least :where()<\/code> gives us the choice now!<\/p>\n\n\n

What about non-BEM HTML?<\/h3>\n\n\n

We don\u2019t live in a perfect world. Sometimes you need to deal with HTML that is outside of your control. For instance, a third-party script that injects HTML that you need to style. That markup often isn\u2019t written with BEM class names. In some cases those styles don\u2019t use classes at all but IDs!<\/p>\n\n\n\n

Once again, :where()<\/code> has our back. This solution is slightly hacky, as we need to reference the class of an element somewhere further up the DOM tree that we know exists.<\/p>\n\n\n\n

\/* ❌ specificity score: 1,0,0 *\/\n#widget {\n  \/* etc. *\/\n}\n\n\/* ✅ specificity score: 0,1,0 *\/\n.page-wrapper :where(#widget) {\n  \/* etc. *\/\n}<\/code><\/pre>\n\n\n\n

Referencing a parent element feels a little risky and restrictive though. What if that parent class changes or isn\u2019t there for some reason? A better (but perhaps equally hacky) solution would be to use :is()<\/code> instead. Remember, the specificity of :is()<\/code> is equal to the most specific selector in its selector list.<\/p>\n\n\n\n

So, instead of referencing a class we know (or hope!) exists with :where()<\/code>, as in the above example, we could reference a made up class and the <body><\/code> tag.<\/p>\n\n\n\n

\/* ✅ specificity score: 0,1,0 *\/\n:is(.dummy-class, body) :where(#widget) {\n  \/* etc. *\/\n}<\/code><\/pre>\n\n\n\n

The ever-present body<\/code> will help us select our #widget<\/code> element, and the presence of the .dummy-class<\/code> class inside the same :is()<\/code> gives the body<\/code> selector the same specificity score as a class (0,1,0<\/code>)\u2026 and the use of :where()<\/code> ensures the selector doesn\u2019t get any more specific than that.<\/p>\n\n\n

That\u2019s it!<\/h3>\n\n\n

That\u2019s how we can leverage the modern specificity-managing features of the :is()<\/code> and :where()<\/code> pseudo-classes alongside the specificity collision prevention that we get when writing CSS in a BEM format. And in the not too distant future,\u00a0once :has()<\/code>\u00a0gains Firefox support<\/a>\u00a0(it\u2019s\u00a0currently supported behind a flag at the time of writing)\u00a0we\u2019ll likely want to pair it with\u00a0:where()<\/code>\u00a0to undo its specificity.<\/p>\n\n\n\n

Whether you go all-in on BEM naming or not, I hope we can agree that having consistency in selector specificity is a good thing!<\/p>\n","protected":false},"excerpt":{"rendered":"

BEM. Like seemingly all techniques in the world of front-end development, writing CSS in a BEM format can be polarizing. But it is \u2013 at least in my Twitter bubble \u2013 one of the better-liked CSS methodologies. Personally, I think BEM is good, and I think you should use it. But I also get why […]<\/p>\n","protected":false},"author":288720,"featured_media":375146,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"c2c_always_allow_admin_comments":false,"footnotes":"","jetpack_publicize_message":"Taming the Cascade With BEM and Modern CSS Selectors by Liam Johnston","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":[]},"categories":[4],"tags":[18909,9387,15914,730,896,1380],"jetpack_publicize_connections":[],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2022\/11\/is-where-funnels.jpg?fit=1200%2C600&ssl=1","jetpack-related-posts":[{"id":359886,"url":"https:\/\/css-tricks.com\/dont-fight-the-cascade-control-it\/","url_meta":{"origin":375144,"position":0},"title":"Don’t Fight the Cascade, Control It!","date":"January 10, 2022","format":false,"excerpt":"If you\u2019re disciplined and make use of the inheritance that the CSS cascade provides, you\u2019ll end up writing less CSS. But because our styles often comes from all kinds of sources \u2014 and can be a pain to structure and maintain\u2014the cascade can be a source of frustration, and the\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/12\/stephen-leonardi-LfDwhVBfqEk-unsplash-scaled.jpg?fit=1200%2C796&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":198754,"url":"https:\/\/css-tricks.com\/bem-101\/","url_meta":{"origin":375144,"position":1},"title":"BEM 101","date":"April 2, 2015","format":false,"excerpt":"The following is a collaborative post by guest Joe Richardson, Robin Rendle, and a bunch of the CSS-Tricks staff. Joe wanted to do a post about BEM, which we loved, and just about everybody around here had thoughts and opinions about BEM, so we figured we'd all get together on\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":364152,"url":"https:\/\/css-tricks.com\/manuel-matuzovics-css-specificity-demo\/","url_meta":{"origin":375144,"position":2},"title":"Manuel Matuzovic’s CSS Specificity Demo","date":"February 25, 2022","format":false,"excerpt":"If you're looking for a primer on CSS specificity, we've got that. And if you're trying to get ahead of the game, you should be aware of CSS Cascade Layers as well. One of the ways to help get a grasp of CSS specificity is thinking terms of \"what beats\u2026","rel":"","context":"In "Link"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2018\/12\/css-audit-specificity-graph.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":340991,"url":"https:\/\/css-tricks.com\/should-devtools-teach-the-css-cascade\/","url_meta":{"origin":375144,"position":3},"title":"Should DevTools teach the CSS cascade?","date":"May 21, 2021","format":false,"excerpt":"Stefan Judis, two days before I mouthed off about using (X, X, X, X) for talking about specificity, has a great blog post not only using that format, but advocating that browser DevTools should show us that value by selectors. I think that the above additions could help to educate\u2026","rel":"","context":"In "Link"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/05\/Screenshot_2021-05-02_at_14.32.22.jpeg?fit=1200%2C981&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":343613,"url":"https:\/\/css-tricks.com\/using-the-specificity-of-where-as-a-css-reset\/","url_meta":{"origin":375144,"position":4},"title":"Using the Specificity of :where() as a CSS Reset","date":"July 12, 2021","format":false,"excerpt":"I don\u2019t know about you, but I write these three declarations many times in my CSS: ul { padding: 0; margin: 0; list-style-type: none; } You might yell at me and say I can just put those in my CSS resets. I wish I could, but I don\u2018t want to\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/06\/where-specificity.jpg?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":352140,"url":"https:\/\/css-tricks.com\/cascade-layers\/","url_meta":{"origin":375144,"position":5},"title":"Cascade Layers","date":"September 21, 2021","format":false,"excerpt":"There is a new thing coming in CSS: @layer. This comes from Miriam Suzanne, who is really on a tear with influencing important new CSS stuff. I've been hearing about all this, but then all a sudden it just dropped in experimental browsers. Leave it to Bramus to really dig\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2018\/11\/cssstacks.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]}],"_links":{"self":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/375144"}],"collection":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/users\/288720"}],"replies":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/comments?post=375144"}],"version-history":[{"count":10,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/375144\/revisions"}],"predecessor-version":[{"id":376525,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/375144\/revisions\/376525"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media\/375146"}],"wp:attachment":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media?parent=375144"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/categories?post=375144"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/tags?post=375144"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}