{"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 ( There are actually exceptions where it is deemed totally acceptable to add specificity. For instance: the 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 The CSS Selectors Level 4 spec<\/a> gives us some powerful new(ish) ways to select elements. Some of my favorites include The incredibly powerful Lemme stick one of those pseudo-classes in my BEM and…<\/p>\n\n\n\n Whoops! See that specificity score? Remember, with BEM we ideally want our selectors to all have a specificity score of Even though the second selector is last in the source order, the first selector\u2019s higher specificity ( 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 Remember what I was saying about The first part of this selector ( 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 In this example, we have a A die-hard specificity nerd might have done this instead:<\/p>\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 The 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
: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
: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
: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
: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
\/* ❌ 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
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
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
Dang. So now what?<\/h3>\n\n\n
: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
.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
.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
.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
.card { ... }\n.card--featured { ... }\n.card__title { ... }\n.card__title--featured { ... }\n.card__img { ... }\n.card__img--featured { ... }<\/code><\/pre>\n\n\n\n
--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
: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