Scopri come utilizzare @scope per selezionare elementi solo all'interno di un sottoalbero limitato del DOM.
Data di pubblicazione: 4 ottobre 2023
Quando scrivi i selettori, potresti trovarti combattuto tra due mondi. Da un lato, devi essere molto specifico sugli elementi che selezioni. D'altra parte, vuoi che i selettori rimangano facili da sostituire e non siano strettamente accoppiati alla struttura DOM.
Ad esempio, quando vuoi selezionare "l'immagine principale nell'area dei contenuti del componente scheda", che è una selezione di elementi piuttosto specifica, molto probabilmente non vuoi scrivere un selettore come .card > .content > img.hero.
- Questo selettore ha una specificità piuttosto elevata di
(0,3,1), il che rende difficile l'override man mano che il codice cresce. - Poiché si basa sul combinatore di elementi secondari diretti, è strettamente accoppiato alla struttura DOM. Se il markup cambia, devi modificare anche il CSS.
Tuttavia, non devi scrivere solo img come selettore per quell'elemento, perché selezionerebbe tutti gli elementi immagine della pagina.
Trovare il giusto equilibrio in questo senso è spesso una sfida. Nel corso degli anni, alcuni sviluppatori hanno ideato soluzioni e soluzioni alternative per aiutarti in situazioni come queste. Ad esempio:
- Metodologie come BEM stabiliscono che a questo elemento deve essere assegnata una classe
card__img card__img--heroper mantenere bassa la specificità, consentendoti al contempo di essere specifico nella selezione. - Le soluzioni basate su JavaScript, come CSS con ambito o Componenti con stile, riscrivono tutti i selettori aggiungendo stringhe generate in modo casuale, ad esempio
sc-596d7e0e-4, per impedire che abbiano come target elementi dall'altro lato della pagina. - Alcune librerie eliminano completamente i selettori e richiedono di inserire i trigger di stile direttamente nel markup.
Ma cosa succede se non ti serve nessuno di questi? Cosa succederebbe se il CSS ti offrisse un modo per selezionare elementi specifici senza richiedere di scrivere selettori con un'alta specificità o strettamente accoppiati al DOM? È qui che entra in gioco @scope, che ti offre un modo per selezionare gli elementi solo all'interno di un sottoalbero del DOM.
Introduzione di @scope
Con @scope puoi limitare la copertura dei selettori. A questo scopo, imposta la radice dell'ambito, che determina il limite superiore del sottoalbero che vuoi scegliere come target. Con una radice di ambito impostata, le regole di stile contenute, denominate regole di stile con ambito, possono selezionare solo da questo sottoalbero limitato del DOM.
Ad esempio, per scegliere come target solo gli elementi <img> nel componente .card, imposta .card come radice di ambito della regola @ @scope.
@scope (.card) {
img {
border-color: green;
}
}
La regola di stile con ambito img { … } può selezionare in modo efficace solo gli elementi <img> che rientrano nell'ambito dell'elemento .card corrispondente.
Per impedire la selezione degli elementi <img> all'interno dell'area dei contenuti della scheda (.card__content), puoi rendere più specifico il selettore img. Un altro modo per farlo è utilizzare il fatto che la regola at @scope accetta anche un limite di ambito che determina il limite inferiore.
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
Questa regola di stile con ambito specifico ha come target solo gli elementi <img> posizionati tra gli elementi .card e .card__content nell'albero degli elementi principali. Questo tipo di ambito, con un limite superiore e uno inferiore, viene spesso chiamato ambito a ciambella.
Selettore :scope
Per impostazione predefinita, tutte le regole di stile con ambito sono relative alla radice dell'ambito. È anche possibile scegliere come target l'elemento principale dell'ambito. Per farlo, utilizza il selettore :scope.
@scope (.card) {
:scope {
/* Selects the matched .card itself */
}
img {
/* Selects img elements that are a child of .card */
}
}
I selettori all'interno delle regole di stile con ambito ricevono implicitamente il prefisso :scope. Se
vuoi, puoi specificarlo aggiungendo :scope.
In alternativa, puoi anteporre il selettore & da
CSS Nesting.
@scope (.card) {
img {
/* Selects img elements that are a child of .card */
}
:scope img {
/* Also selects img elements that are a child of .card */
}
& img {
/* Also selects img elements that are a child of .card */
}
}
Un limite di ambito può utilizzare la pseudo-classe :scope per richiedere una relazione specifica con la radice dell'ambito:
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
Un limite di ambito può fare riferimento anche a elementi esterni alla radice dell'ambito utilizzando :scope. Ad esempio:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
Le regole di stile con ambito non possono uscire dal sottoalbero. Selezioni come :scope + p non sono valide perché tentano di selezionare elementi che non rientrano nell'ambito.
@scope e specificità
I selettori utilizzati nel preludio per @scope non influiscono sulla specificità dei selettori contenuti. Nel nostro esempio, la specificità del selettore img è ancora (0,0,1).
@scope (#sidebar) {
img { /* Specificity = (0,0,1) */
...
}
}
La specificità di :scope è quella di una normale pseudo-classe, ovvero (0,1,0).
@scope (#sidebar) {
:scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
...
}
}
Nell'esempio seguente, internamente, & viene riscritto nel selettore utilizzato per la radice di ambito, racchiuso all'interno di un selettore :is(). Alla fine, il browser utilizzerà :is(#sidebar, .card) img come selettore per eseguire la corrispondenza. Questa procedura è nota come desugaring.
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
...
}
}
Poiché & viene desugarizzato utilizzando :is(), la specificità di & viene calcolata seguendo le regole di specificità di :is(): la specificità di & è quella del suo argomento più specifico.
Applicata a questo esempio, la specificità di :is(#sidebar, .card) è quella del suo argomento più specifico, ovvero #sidebar, e pertanto diventa (1,0,0). Se combini questo valore con la specificità di img, che è (0,0,1), ottieni (1,0,1) come specificità per l'intero selettore complesso.
@scope (#sidebar, .card) {
& img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
...
}
}
La differenza tra :scope e & all'interno di @scope
Oltre alle differenze nel modo in cui viene calcolata la specificità, un'altra differenza tra :scope e & è che :scope rappresenta la radice dell'ambito corrispondente, mentre & rappresenta il selettore utilizzato per trovare la corrispondenza con la radice dell'ambito.
Per questo motivo, è possibile utilizzare & più volte. Ciò è in contrasto con :scope, che puoi utilizzare una sola volta, in quanto non puoi trovare una corrispondenza per una radice di ambito all'interno di una radice di ambito.
@scope (.card) {
& & { /* Selects a `.card` in the matched root .card */
}
:scope :scope { /* ❌ Does not work */
…
}
}
Ambito senza preludio
Quando scrivi stili in linea con l'elemento <style>, puoi limitare le regole di stile all'elemento principale che contiene l'elemento <style> non specificando alcuna radice di ambito. A tale scopo, ometti il preludio di @scope.
<div class="card">
<div class="card__header">
<style>
@scope {
img {
border-color: green;
}
}
</style>
<h1>Card Title</h1>
<img src="…" height="32" class="hero">
</div>
<div class="card__content">
<p><img src="…" height="32"></p>
</div>
</div>
Nell'esempio precedente, le regole con ambito specifico hanno come target solo gli elementi all'interno di div con il nome della classe card__header, perché div è l'elemento principale dell'elemento <style>.
@scope nella cascata
All'interno della cascata CSS, @scope aggiunge anche un nuovo criterio: prossimità di ambito. Questo passaggio viene dopo la specificità, ma prima dell'ordine di visualizzazione.
Quando si confrontano dichiarazioni che appaiono in regole di stile con radici di ambito diverse, vince la dichiarazione con il minor numero di salti generazionali o tra elementi fratelli tra la radice di ambito e l'oggetto della regola di stile con ambito.
Questo nuovo passaggio è utile quando nidifichi diverse varianti di un componente. Prendiamo questo esempio, che non utilizza ancora @scope:
<style>
.light { background: #ccc; }
.dark { background: #333; }
.light a { color: black; }
.dark a { color: white; }
</style>
<div class="light">
<p><a href="#">What color am I?</a></p>
<div class="dark">
<p><a href="#">What about me?</a></p>
<div class="light">
<p><a href="#">Am I the same as the first?</a></p>
</div>
</div>
</div>
Quando visualizzi questo piccolo markup, il terzo link sarà white anziché black, anche se è un elemento secondario di un div a cui è applicata la classe .light. Ciò è dovuto al criterio dell'ordine di apparizione utilizzato dalla cascata per determinare il vincitore. Viene visualizzato che .dark a è stato dichiarato ultimo, quindi vincerà in base alla regola .light a
Con il criterio di prossimità di definizione dell'ambito, questo problema è stato risolto:
@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}
@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}
Poiché entrambi i selettori a hanno la stessa specificità, entra in azione il criterio di prossimità di ambito. Pesa entrambi i selettori in base alla loro prossimità alla radice di ambito. Per il terzo elemento a, c'è un solo hop alla radice di ambito .light, ma due a quella .dark. Pertanto, vincerà il selettore a in .light.
Isolamento del selettore, non dello stile
Tieni presente che @scope limita la copertura dei selettori. Non offre l'isolamento
dello stile. Le proprietà che vengono ereditate dai figli continuano a essere ereditate oltre il limite inferiore di @scope. Una di queste proprietà è color. Quando
dichiariamo che uno si trova all'interno di un ambito a ciambella, color viene comunque ereditato
dai figli all'interno del foro della ciambella.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
Nell'esempio, l'elemento .card__content e i relativi elementi secondari hanno un colore hotpink perché ereditano il valore da .card.