Home >Web Front-end >CSS Tutorial >Taming the Cascade With BEM and Modern CSS Selectors
BEM. Like almost all technologies in front-end development, writing CSS in BEM format can cause polarization. But in my Twitter social circle, it is at least one of the more popular CSS methods.
I personally think BEM is good and I think you should use it. But I can also understand why you might not use it.
Whatever your opinion of BEM, it offers some benefits, the biggest benefit is that it helps avoid specific conflicts in the CSS cascade. This is because, if used properly, any selector written in BEM format should have the same specificity score (0,1,0). Over the years, I’ve designed CSS for many large websites (think government, universities, and banks) and it’s these big projects that have made me discover where BEM really shine. Writing CSS is more fun when you are sure that the style you are writing or editing does not affect other parts of the website.
In fact, in some cases, adding specificity is considered completely acceptable. For example: :hover
and :focus
pseudo-classes. Their specificity scores are 0,2,0. The other is pseudo-elements—for example, ::before
and ::after
—their specificity scores are 0,1,1. However, in the rest of this article, let us assume that we do not expect any other specific spread. ?
But I don't really want to sell you BEM. Instead, I want to talk about how we use it in conjunction with modern CSS selectors (e.g. :is()
, :has()
, :where()
, etc.) for stronger cascade control.
The CSS Selector Level 4 specification provides us with some powerful and relatively new ways to select elements. Some of my favorites include :is()
, :where()
and :not()
, which are all supported by all modern browsers and are now safe to use in almost any project.
:is()
and :where()
are basically the same, except that they have different effects on specificity. Specifically, the specificity score of :where()
is always 0,0,0. Yes, even :where(button#widget.some-class)
is not specific. At the same time, the specificity of :is()
is the specificity of the most specific element in its parameter list. Therefore, we already have the difference in cascade finishing between two modern selectors that can be used.
The powerful :has()
relational pseudo-class is also quickly gaining browser support (in my opinion, this is the biggest new feature of CSS since Grid). However, at the time of writing, the browser support for :has()
is not good enough to be used in production environments.
Let me add a pseudo-class to my BEM...
<code>/* ❌ 特异性分数:0,2,0 */ .something:not(.something--special) { /* 所有 something 的样式,除了特殊的 something */ }</code>
Oh no! Have you seen that specific score? Remember, with BEM, we ideally expect our selectors to have specific scores of 0,1,0. Why 0,2,0 is not good? Consider a similar extension example:
<code>.something:not(a) { color: red; } .something--special { color: blue; }</code>
Even if the second selector finally appears in the source code sequence, the first selector's higher specificity (0,1,1) will win, and the .something--special
element's color will be set to red. That is, assume that your BEM is written correctly and the selected element applies both the .something
base class and the .something--special
modifier class in HTML.
If used improperly, these pseudo-classes may affect the cascade in unexpected ways. It is these inconsistencies that can cause trouble later, especially in larger and more complex code bases.
Remember what I just said :where()
and its specificity is zero? We can take advantage of this:
<code>/* ✅ 特异性分数:0,1,0 */ .something:where(:not(.something--special)) { /* 等 */ }</code>
The first part of this selector (.something
) obtains its usual specificity score 0,1,0. But the specificity of :where()
—and everything therein—is 0, and does not further increase the selector specificity.
Those who don't care as much about specificity as I do (and to be fair, probably a lot of people) have been doing well in nesting. With some random keyboard typing we might get CSS like this (note that I use Sass for simplicity):
<code>.card { ... } .card--featured { /* 等 */ .card__title { ... } .card__title { ... } } .card__title { ... } .card__img { ... }</code>
In this example, we have a .card
component. When it is a "featured" card (using the .card--featured
class), the title and image of the card need to be different styles. However, as we now know, the specificity scores caused by the above code are inconsistent with the rest of our system.
A stubborn specific fanatic might do this:
It's not bad, right? Frankly, this is a pretty CSS.
<code>.card { ... } .card--featured { ... } .card__title { ... } .card__title--featured { ... } .card__img { ... } .card__img--featured { ... }</code>
However, HTML also has its drawbacks. Experienced BEM authors may be painfully aware that complex template logic is needed to conditionally apply modifier classes to multiple elements. In this example, the HTML template needs to conditionally add the
modifier class to three elements (, --featured
, and .card
), but it may be more in real-world examples. There are many if statements. .card__title
.card__img
:where()
<code>.card { ... } .card--featured { ... } .card__title { ... } :where(.card--featured) .card__title { ... } .card__img { ... } :where(.card--featured) .card__img { ... }</code>
Whether you should choose this method instead of applying the modifier class to various child elements depends on personal preference. But at least
<code>.card { ... } .card--featured { ... } .card__title { /* 等 */ :where(.card--featured) & { ... } } .card__img { /* 等 */ :where(.card--featured) & { ... } }</code>Now we have a choice!
We live in an imperfect world. Sometimes you need to deal with HTML that is beyond your control. For example, inject a third-party script that you need to style HTML. These tags are usually not written using BEM class names, and in some cases, these styles do not use classes at all, but IDs!
Similarly, :where()
can also help us solve this problem. This solution is a bit clumsy because we need to refer to a class in the DOM tree that we know exists.
<code>/* ❌ 特异性分数:0,2,0 */ .something:not(.something--special) { /* 所有 something 的样式,除了特殊的 something */ }</code>
Cite the parent element feels a bit risky and restrictive. What if the parent class changes or does not exist for some reason? A better (but perhaps equally clumsy) solution is to use :is()
instead. Remember that the specificity of :is()
is equal to the most specific selector in its selector list.
So we can use :is()
to refer to a class that we know (or hope!) exists, rather than using :where()
as in the example above.
<code>.something:not(a) { color: red; } .something--special { color: blue; }</code>
The always-existing body
will help us select the #widget
element, and the presence of the :is()
class in the same .dummy-class
will make the body
selector have the same specificity score (0,1,0) as the class… and use :where()
to make sure the selector is not more specific than this.
This is how we take advantage of the modern specific management capabilities of the :is()
and :where()
pseudo-classes and the specific conflict prevention capabilities obtained when writing CSS in BEM format. In the near future, once :has()
is supported by Firefox (it is currently supported behind the logo at the time of writing), we may want to pair it with :where()
to undo its specificity.
Whether you are fully naming with BEM or not, I hope we can agree that selector specificity is a good thing!
The above is the detailed content of Taming the Cascade With BEM and Modern CSS Selectors. For more information, please follow other related articles on the PHP Chinese website!