Tämja kaskaden med BEM och moderna CSS-väljare PlatoBlockchain Data Intelligence. Vertikal sökning. Ai.

Tämja kaskaden med BEM och moderna CSS-väljare

BEM. Som till synes alla tekniker i världen av frontend-utveckling, skriva CSS i ett BEM-format kan vara polariserande. Men det är – åtminstone i min Twitter-bubbla – en av de mer omtyckta CSS-metoderna.

Personligen tycker jag att BEM är bra, och jag tycker att du ska använda det. Men jag förstår också varför du kanske inte.

Oavsett din åsikt om BEM erbjuder den flera fördelar, den största är att den hjälper till att undvika specificitetskrockar i CSS Cascade. Det beror på att, om de används på rätt sätt, alla väljare skrivna i ett BEM-format bör ha samma specificitetspoäng (0,1,0). Jag har skapat CSS för massor av storskaliga webbplatser genom åren (tänk på myndigheter, universitet och banker), och det är på dessa större projekt där jag har funnit att BEM verkligen lyser. Att skriva CSS är mycket roligare när du är säker på att stilarna du skriver eller redigerar inte påverkar någon annan del av webbplatsen.

Det finns faktiskt undantag där det anses helt acceptabelt att lägga till specificitet. Till exempel: :hover och :focus pseudoklasser. De har ett specificitetspoäng på 0,2,0. En annan är pseudoelement - som ::before och ::after — som har en specificitetspoäng på 0,1,1. För resten av den här artikeln, låt oss dock anta att vi inte vill ha någon annan specificitetskrypning. 🤓

Men jag är inte riktigt här för att sälja dig på BEM. Istället vill jag prata om hur vi kan använda det tillsammans med moderna CSS-väljare — tänk :is(), :has(), :where(), etc. — to gain even mer kontroll av kaskaden.

Vad är det här med moderna CSS-väljare?

Smakämnen CSS Selectors Level 4 spec ger oss några kraftfulla nya (ish) sätt att välja element. Några av mina favoriter inkluderar :is(), :where()och :not(), som var och en stöds av alla moderna webbläsare och är säkra att använda på nästan alla projekt nuförtiden.

:is() och :where() är i princip samma sak förutom hur de påverkar specificiteten. Specifikt, :where() har alltid en specificitetspoäng på 0,0,0. Japp, till och med :where(button#widget.some-class) har ingen specificitet. Samtidigt specificiteten av :is() är det element i dess argumentlista med högst specificitet. Så vi har redan en kaskad-tråkig skillnad mellan två moderna väljare som vi kan arbeta med.

Den otroligt kraftfulla :has() relationell pseudoklass är också snabbt få webbläsarstöd (och är den största nya funktionen i CSS sedan dess Rutnät, enligt min ödmjuka åsikt). Men i skrivande stund, webbläsarstöd för :has() är inte tillräckligt bra för användning i produktionen ännu.

Låt mig hålla en av dessa pseudoklasser i min BEM och...

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

Hoppsan! Ser du det specificitetspoänget? Kom ihåg att med BEM vill vi helst att våra väljare ska ha ett specificitetspoäng på 0,1,0. Varför är 0,2,0 dålig? Betrakta samma exempel, utökat:

.something:not(.something--special) {
  color: red;
}
.something--special {
  color: blue;
}

Även om den andra väljaren är sist i källordningen, är den första väljarens högre specificitet (0,2,0) vinner och färgen på .something--special element kommer att ställas in på red. Det vill säga, förutsatt att din BEM är korrekt skriven och att det valda elementet har både .something grundklass och .something--special modifieringsklass tillämpas på den i HTML.

Om de används slarvigt kan dessa pseudoklasser påverka Cascade på oväntade sätt. Och det är den här sortens inkonsekvenser som kan skapa huvudvärk längre fram, särskilt på större och mer komplexa kodbaser.

Dang. Så vad nu?

Kom ihåg vad jag sa om :where() och det faktum att dess specificitet är noll? Vi kan använda det till vår fördel:

/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
  /* etc. */
}

Den första delen av denna väljare (.something) får sitt vanliga specificitetspoäng på 0,1,0. Men :where() — och allt inuti det — har en specificitet av 0, vilket inte ökar specificiteten för väljaren ytterligare.

:where() låter oss bygga bo

Folk som inte bryr sig lika mycket som jag om specificitet (och det är nog många, för att vara rättvis) har haft det ganska bra när det kommer till häckning. Med några bekymmerslösa tangentbordstryck kan vi sluta med CSS så här (observera att jag använder Sass för korthetens skull):

.card { ... }

.card--featured {
  /* etc. */  
  .card__title { ... }
  .card__title { ... }
}

.card__title { ... }
.card__img { ... }

I det här exemplet har vi en .card komponent. När det är ett "utvalt" kort (med hjälp av .card--featured klass), måste kortets titel och bild utformas på ett annat sätt. Men som vi nu vet, koden ovan resulterar i en specificitetspoäng som inte är förenlig med resten av vårt system.

En inbiten specificitetsnörd kan ha gjort så här istället:

.card { ... }
.card--featured { ... }
.card__title { ... }
.card__title--featured { ... }
.card__img { ... }
.card__img--featured { ... }

Det är väl inte så illa? Ärligt talat, detta är vacker CSS.

Det finns dock en nackdel med HTML. Erfarna BEM-författare är förmodligen smärtsamt medvetna om den klumpiga malllogik som krävs för att villkorligt tillämpa modifieringsklasser på flera element. I det här exemplet måste HTML-mallen lägga till villkorligt --featured modifieringsklass till tre element (.card, .card__titleoch .card__img) men förmodligen ännu mer i ett verkligt exempel. Det är mycket if uttalanden.

Smakämnen :where() selector kan hjälpa oss att skriva mycket mindre malllogik – och färre BEM-klasser att starta upp – utan att öka specificitetsnivån.

.card { ... }
.card--featured { ... }

.card__title { ... }
:where(.card--featured) .card__title { ... }

.card__img { ... }
:where(.card--featured) .card__img { ... }

Här är samma sak men i Sass (observera det efterföljande et-tecken):

.card { ... }
.card--featured { ... }
.card__title { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}
.card__img { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}

Huruvida du ska välja detta tillvägagångssätt framför att tillämpa modifieringsklasser på de olika underordnade elementen är en fråga om personlig preferens. Men åtminstone :where() ger oss valet nu!

Vad sägs om icke-BEM HTML?

Vi lever inte i en perfekt värld. Ibland behöver du hantera HTML som ligger utanför din kontroll. Till exempel ett tredjepartsskript som injicerar HTML som du behöver stil. Den uppmärkningen skrivs ofta inte med BEM-klassnamn. I vissa fall använder dessa stilar inte klasser alls utan ID:n!

Än en gång, :where() har vår rygg. Denna lösning är lite hackig, eftersom vi behöver referera till klassen för ett element någonstans längre upp i DOM-trädet som vi vet finns.

/* ❌ specificity score: 1,0,0 */
#widget {
  /* etc. */
}

/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
  /* etc. */
}

Att hänvisa till ett föräldraelement känns dock lite riskabelt och begränsande. Tänk om den föräldraklassen ändras eller inte är där av någon anledning? En bättre (men kanske lika hackig) lösning skulle vara att använda :is() istället. Kom ihåg, specificiteten av :is() är lika med den mest specifika väljaren i dess väljarlista.

Så istället för att referera till en klass som vi vet (eller hoppas!) finns med :where(), som i exemplet ovan, kan vi referera till en påhittad klass och märka.

/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  /* etc. */
}

Den ständigt närvarande body hjälper oss att välja vår #widget element och närvaron av .dummy-class klass i samma :is() ger body väljare samma specificitetspoäng som en klass (0,1,0)... och användningen av :where() ser till att väljaren inte blir mer specifik än så.

Det är allt!

Det är så vi kan dra nytta av de moderna specificitetshanterande funktionerna i :is() och :where() pseudoklasser vid sidan av specificitetskollisionsskyddet som vi får när vi skriver CSS i ett BEM-format. Och inom en inte alltför avlägsen framtid, gång :has() får stöd för Firefox (det stöds för närvarande bakom en flagga i skrivande stund) vi kommer förmodligen att vilja para ihop den med :where() för att ångra dess specificitet.

Oavsett om du går all-in på BEM-namngivning eller inte, hoppas jag att vi kan komma överens om att det är bra att ha konsekvens i väljarspecificitet!

Tidsstämpel:

Mer från CSS-tricks