Domare la cascata con BEM e moderni selettori CSS PlatoBlockchain Data Intelligence. Ricerca verticale. Ai.

Addomesticare la cascata con BEM e moderni selettori CSS

BEM. Come apparentemente tutte le tecniche nel mondo dello sviluppo front-end, scrivere CSS in un formato BEM può essere polarizzante. Ma è, almeno nella mia bolla di Twitter, una delle metodologie CSS più apprezzate.

Personalmente, penso che BEM sia buono e penso che dovresti usarlo. Ma capisco anche perché potresti non farlo.

Indipendentemente dalla tua opinione su BEM, offre diversi vantaggi, il più grande dei quali è che aiuta a evitare conflitti di specificità nel CSS Cascade. Questo perché, se usato correttamente, qualsiasi selettore scritto in un formato BEM dovrebbe avere lo stesso punteggio di specificità (0,1,0). Nel corso degli anni ho progettato il CSS per molti siti Web su larga scala (si pensi al governo, alle università e alle banche), ed è su questi progetti più grandi che ho scoperto che il BEM brilla davvero. Scrivere CSS è molto più divertente quando hai la certezza che gli stili che stai scrivendo o modificando non influiscano su qualche altra parte del sito.

In realtà ci sono eccezioni in cui si ritiene del tutto accettabile aggiungere specificità. Ad esempio: il :hover ed :focus pseudo classi. Quelli hanno un punteggio di specificità di 0,2,0. Un altro sono gli pseudo elementi - come ::before ed ::after - che hanno un punteggio di specificità di 0,1,1. Per il resto di questo articolo, però, supponiamo di non volere nessun altro errore di specificità. 🤓

Ma non sono davvero qui per venderti su BEM. Invece, voglio parlare di come possiamo usarlo insieme ai moderni selettori CSS: pensa :is(), :has(), :where(), ecc. - per guadagnare pari Scopri di più controllo di la Cascata.

Cos'è questa storia dei selettori CSS moderni?

I Selettori CSS Livello 4 spec ci offre alcuni nuovi modi potenti (ish) per selezionare gli elementi. Alcuni dei miei preferiti includono :is(), :where()e :not(), ognuno dei quali è supportato da tutti i browser moderni ed è sicuro da usare su quasi tutti i progetti al giorno d'oggi.

:is() ed :where() sono fondamentalmente la stessa cosa tranne per il modo in cui incidono sulla specificità. Nello specifico, :where() ha sempre un punteggio di specificità di 0,0,0. Sì, anche :where(button#widget.some-class) non ha specificità. Nel frattempo, la specificità di :is() è l'elemento nella sua lista di argomenti con la massima specificità. Quindi, abbiamo già una distinzione Cascade tra due selettori moderni con cui possiamo lavorare.

L'incredibilmente potente :has() lo è anche la pseudo-classe relazionale guadagnando rapidamente il supporto del browser (ed è la più grande nuova funzionalità dei CSS da allora Griglia, a mio modesto parere). Tuttavia, al momento della scrittura, il supporto del browser per :has() non è ancora abbastanza buono per l'uso in produzione.

Fammi inserire una di quelle pseudo-classi nel mio BEM e...

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

Ops! Vedi quel punteggio di specificità? Ricorda, con BEM idealmente vogliamo che tutti i nostri selezionatori abbiano un punteggio di specificità di 0,1,0. Perché è 0,2,0 cattivo? Considera questo stesso esempio, espanso:

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

Anche se il secondo selettore è l'ultimo nell'ordine di origine, la maggiore specificità del primo selettore (0,2,0) vince e il colore di .something--special gli elementi saranno impostati su red. Cioè, supponendo che il tuo BEM sia scritto correttamente e che l'elemento selezionato abbia sia il .something classe base e .something--special modifier class ad essa applicata nell'HTML.

Usate con noncuranza, queste pseudo-classi possono avere un impatto su Cascade in modi inaspettati. Ed è questo tipo di incongruenze che possono creare mal di testa lungo la linea, specialmente su basi di codice più grandi e complesse.

Dannazione. E ora?

Ricorda cosa stavo dicendo :where() e il fatto che la sua specificità sia zero? Possiamo usarlo a nostro vantaggio:

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

La prima parte di questo selettore (.something) ottiene il consueto punteggio di specificità di 0,1,0. Ma :where() - e tutto ciò che contiene - ha una specificità di 0, che non aumenta ulteriormente la specificità del selettore.

:where() ci permette di nidificare

Le persone a cui non importa quanto me della specificità (e probabilmente sono molte persone, per essere onesti) se la sono cavata abbastanza bene quando si tratta di nidificare. Con alcuni colpi di tastiera spensierati, potremmo finire con CSS in questo modo (nota che sto usando Sass per brevità):

.card { ... }

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

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

In questo esempio, abbiamo a .card componente. Quando si tratta di una carta "in primo piano" (usando il .card--featured class), il titolo e l'immagine della carta devono avere uno stile diverso. Ma, come noi adesso sapere, il codice sopra si traduce in un punteggio di specificità che è incoerente con il resto del nostro sistema.

Un nerd della specificità irriducibile avrebbe potuto invece fare questo:

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

Non è poi così male, vero? Francamente, questo è un bellissimo CSS.

Tuttavia, c'è un aspetto negativo nell'HTML. Gli autori BEM stagionati sono probabilmente dolorosamente consapevoli della goffa logica del modello richiesta per applicare condizionalmente le classi modificatrici a più elementi. In questo esempio, il modello HTML deve aggiungere in modo condizionale il file --featured classe modificatore a tre elementi (.card, .card__titlee .card__img) anche se probabilmente ancora di più in un esempio reale. Questo è molto if dichiarazioni.

I :where() selector può aiutarci a scrivere molta meno logica del modello e meno classi BEM da avviare, senza aumentare il livello di specificità.

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

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

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

Ecco la stessa cosa ma in Sass (nota il trailing e commerciale):

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

Indipendentemente dal fatto che tu debba o meno optare per questo approccio rispetto all'applicazione di classi modificatrici ai vari elementi figlio è una questione di preferenze personali. Ma almeno :where() ci dà la scelta ora!

Che dire dell'HTML non BEM?

Non viviamo in un mondo perfetto. A volte devi avere a che fare con HTML che è al di fuori del tuo controllo. Ad esempio, uno script di terze parti che inserisce l'HTML di cui hai bisogno per definire lo stile. Quel markup spesso non è scritto con i nomi delle classi BEM. In alcuni casi quegli stili non usano affatto le classi ma gli ID!

Ancora una volta, :where() ha le nostre spalle. Questa soluzione è leggermente complicata, poiché dobbiamo fare riferimento alla classe di un elemento da qualche parte più in alto nell'albero DOM che sappiamo esistere.

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

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

Tuttavia, fare riferimento a un elemento genitore sembra un po' rischioso e restrittivo. Cosa succede se quella classe genitore cambia o non c'è per qualche motivo? Una soluzione migliore (ma forse altrettanto confusa) sarebbe quella di utilizzare :is() invece. Ricorda, la specificità di :is() è uguale al selettore più specifico nel suo elenco di selettori.

Quindi, invece di fare riferimento a una classe che sappiamo (o speriamo!) esista :where(), come nell'esempio precedente, potremmo fare riferimento a una classe inventata e the etichetta.

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

Il sempre presente body ci aiuterà a selezionare il nostro #widget elemento e la presenza dell' .dummy-class classe all'interno della stessa :is()body selettore lo stesso punteggio di specificità di una classe (0,1,0)… e l'uso di :where() assicura che il selettore non diventi più specifico di così.

Questo è tutto!

È così che possiamo sfruttare le moderne funzionalità di gestione della specificità del :is() ed :where() pseudo-classi insieme alla prevenzione delle collisioni di specificità che otteniamo quando scriviamo CSS in un formato BEM. E in un futuro non troppo lontano, una volta :has() ottiene il supporto per Firefox (è attualmente supportato dietro un flag al momento della scrittura) probabilmente vorremo accoppiarlo con :where() per annullare la sua specificità.

Che tu vada all-in sulla denominazione BEM o meno, spero che possiamo concordare sul fatto che avere coerenza nella specificità del selettore è una buona cosa!

Timestamp:

Di più da Trucchi CSS