De cascade temmen met BEM en moderne CSS-selectors PlatoBlockchain Data Intelligence. Verticaal zoeken. Ai.

De cascade temmen met BEM en moderne CSS-kiezers

BEM. Zoals schijnbaar alle technieken in de wereld van front-end development, CSS schrijven in een BEM-formaat kan polariserend werken. Maar het is – althans in mijn Twitter-bubbel – een van de meest geliefde CSS-methodologieën.

Persoonlijk denk ik dat BEM goed is, en ik denk dat je het zou moeten gebruiken. Maar ik snap ook waarom je dat misschien niet doet.

Ongeacht uw mening over BEM, het biedt verschillende voordelen, waarvan de grootste is dat het helpt om specificiteitsconflicten in de CSS Cascade te voorkomen. Dat komt omdat, indien correct gebruikt, alle selectors die in een BEM-indeling zijn geschreven, dezelfde specificiteitsscore zouden moeten hebben (0,1,0). Ik heb in de loop der jaren de CSS ontworpen voor tal van grootschalige websites (denk aan de overheid, universiteiten en banken), en het is bij deze grotere projecten dat BEM echt uitblinkt. Het schrijven van CSS is veel leuker als u erop kunt vertrouwen dat de stijlen die u schrijft of bewerkt geen invloed hebben op een ander deel van de site.

Er zijn eigenlijk uitzonderingen waarbij het volkomen acceptabel wordt geacht om specificiteit toe te voegen. Bijvoorbeeld: de :hover en :focus pseudo klassen. Die hebben een specificiteitsscore van 0,2,0. Een andere is pseudo-elementen - zoals ::before en ::after - die een specificiteitsscore hebben van 0,1,1. Laten we voor de rest van dit artikel echter aannemen dat we geen andere specificiteitsgriep willen. 🤓

Maar ik ben hier niet echt om je op BEM te verkopen. In plaats daarvan wil ik het hebben over hoe we het naast moderne CSS-selectors kunnen gebruiken - denk na :is(), :has(), :where(), enz. - om gelijk te krijgen meer controle de cascade.

Wat is dit met moderne CSS-kiezers?

De CSS-kiezers Niveau 4 spec geeft ons een aantal krachtige nieuwe (achtige) manieren om elementen te selecteren. Enkele van mijn favorieten zijn :is(), :where() en :not(), die allemaal worden ondersteund door alle moderne browsers en tegenwoordig veilig kunnen worden gebruikt voor bijna elk project.

:is() en :where() zijn in wezen hetzelfde, behalve hoe ze de specificiteit beïnvloeden. specifiek, :where() heeft altijd een specificiteitsscore van 0,0,0. Ja, zelfs :where(button#widget.some-class) heeft geen specificiteit. Ondertussen, de specificiteit van :is() is het element in zijn lijst met argumenten met de hoogste specificiteit. We hebben dus al een Cascade-worstelend onderscheid tussen twee moderne selectors waarmee we kunnen werken.

De ongelooflijk krachtige :has() relationele pseudo-klasse is ook wint snel aan browserondersteuning (en is sindsdien de grootste nieuwe functie van CSS Raster, naar mijn bescheiden mening). Echter, op het moment van schrijven, browserondersteuning voor :has() is nog niet goed genoeg voor gebruik in de productie.

Laat me een van die pseudo-klassen in mijn BEM steken en ...

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

Oeps! Zie je die specificiteitsscore? Onthoud dat we met BEM idealiter willen dat onze selectors allemaal een specificiteitsscore van hebben 0,1,0. Waarom is 0,2,0 slechte? Beschouw hetzelfde voorbeeld, uitgebreid:

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

Ook al is de tweede selector de laatste in de bronvolgorde, de hogere specificiteit van de eerste selector (0,2,0) wint, en de kleur van .something--special elementen worden ingesteld red. Dat wil zeggen, ervan uitgaande dat uw BEM correct is geschreven en het geselecteerde element zowel de .something basisklasse en .something--special modifier-klasse die erop is toegepast in de HTML.

Onzorgvuldig gebruikt, kunnen deze pseudo-klassen de Cascade op onverwachte manieren beïnvloeden. En het zijn dit soort inconsistenties die later voor hoofdpijn kunnen zorgen, vooral bij grotere en complexere codebases.

Dang. Dus wat nu?

Onthoud waar ik het over had :where() en het feit dat de specificiteit nul is? Daar kunnen we ons voordeel mee doen:

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

Het eerste deel van deze selector (.something) krijgt zijn gebruikelijke specificiteitsscore van 0,1,0. Maar :where() - en alles erin - heeft een specificiteit van 0, wat de specificiteit van de selector niet verder verhoogt.

:where() stelt ons in staat om te nestelen

Mensen die niet zoveel geven om specificiteit als ik (en dat zijn waarschijnlijk veel mensen, om eerlijk te zijn) hebben het redelijk goed gehad als het op nesten aankomt. Met wat zorgeloze toetsaanslagen kunnen we eindigen met CSS zoals deze (merk op dat ik Sass gebruik voor beknoptheid):

.card { ... }

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

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

In dit voorbeeld hebben we een .card bestanddeel. Als het een "uitgelichte" kaart is (met behulp van de .card--featured class), moeten de titel en afbeelding van de kaart anders worden vormgegeven. Maar zoals wij nu weet, de bovenstaande code resulteert in een specificiteitsscore die niet consistent is met de rest van ons systeem.

Een die-hard specificiteitsnerd had dit in plaats daarvan misschien gedaan:

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

Dat is niet zo erg, toch? Eerlijk gezegd is dit prachtige CSS.

Er is echter een keerzijde in de HTML. Doorgewinterde BEM-auteurs zijn zich waarschijnlijk pijnlijk bewust van de onhandige sjabloonlogica die nodig is om modificatieklassen voorwaardelijk toe te passen op meerdere elementen. In dit voorbeeld moet de HTML-sjabloon voorwaardelijk de --featured modificatieklasse naar drie elementen (.card, .card__title en .card__img) hoewel waarschijnlijk nog meer in een realistisch voorbeeld. Dat is veel if statements.

De :where() selector kan ons helpen veel minder sjabloonlogica te schrijven - en minder BEM-klassen om op te starten - zonder toe te voegen aan het specificiteitsniveau.

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

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

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

Hier is hetzelfde, maar dan in Sass (let op de trailing ampersand):

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

Of u al dan niet voor deze aanpak moet kiezen boven het toepassen van modificatieklassen op de verschillende onderliggende elementen, is een kwestie van persoonlijke voorkeur. Maar tenminste :where() geeft ons nu de keuze!

Hoe zit het met niet-BEM HTML?

We leven niet in een perfecte wereld. Soms heb je te maken met HTML waar je geen controle over hebt. Bijvoorbeeld een script van derden dat HTML injecteert die u nodig heeft om te stylen. Die opmaak wordt vaak niet geschreven met BEM-klassenamen. In sommige gevallen gebruiken die stijlen helemaal geen klassen maar ID's!

Nogmaals, :where() heeft onze rug. Deze oplossing is enigszins gehackt, omdat we moeten verwijzen naar de klasse van een element ergens verderop in de DOM-boom waarvan we weten dat het bestaat.

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

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

Verwijzen naar een bovenliggend element voelt echter een beetje riskant en beperkend. Wat als die ouderklasse verandert of er om de een of andere reden niet is? Een betere (maar misschien even hacky) oplossing zou zijn om te gebruiken :is() in plaats van. Vergeet niet, de specificiteit van :is() is gelijk aan de meest specifieke selector in de selectorlijst.

Dus in plaats van te verwijzen naar een klasse waarvan we weten (of hopen!) dat die bestaat :where(), zoals in het bovenstaande voorbeeld, kunnen we verwijzen naar een verzonnen klasse en de label.

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

Het altijd aanwezige body zal ons helpen bij het selecteren van onze #widget element, en de aanwezigheid van de .dummy-class klasse binnen hetzelfde :is() geeft de body selector dezelfde specificiteitsscore als een klasse (0,1,0)… en het gebruik van :where() zorgt ervoor dat de selector niet specifieker wordt.

Dat is het!

Dat is hoe we de moderne specificiteitsbeheerfuncties van de kunnen benutten :is() en :where() pseudo-klassen naast de specificiteit van botsingspreventie die we krijgen bij het schrijven van CSS in een BEM-indeling. En in de niet al te verre toekomst, eens :has() krijgt Firefox-ondersteuning (het wordt momenteel ondersteund achter een vlag op het moment van schrijven) we zullen het waarschijnlijk willen koppelen aan :where() om zijn specificiteit ongedaan te maken.

Of je nu all-in gaat op BEM-naamgeving of niet, ik hoop dat we het erover eens zijn dat consistentie in selectorspecificiteit een goede zaak is!

Tijdstempel:

Meer van CSS-trucs