Limita la copertura dei selettori con l'attributo CSS @scope at-rule

Scopri come utilizzare @scope per selezionare elementi solo all'interno di una sottostruttura limitata del DOM.

Supporto dei browser

  • 118
  • 118
  • x
  • x

L'arte delicata di scrivere i selettori CSS

Quando scrivi i selettori, potresti trovarti diviso tra due mondi. Da un lato l'obiettivo è specificare con precisione gli elementi selezionati. D'altra parte, vuoi che i selettori siano facili da sostituire e non siano strettamente associati alla struttura DOM.

Ad esempio, se vuoi selezionare "l'immagine hero nell'area dei contenuti del componente della scheda", che è una selezione di elementi piuttosto specifica, molto probabilmente non vorrai scrivere un selettore come .card > .content > img.hero.

  • Questo selettore ha una specificità piuttosto elevata di (0,3,1) che lo rende difficile da sostituire man mano che il codice cresce.
  • Utilizzando il combinatore diretto figlio è strettamente associato alla struttura DOM. Se il markup dovesse cambiare, dovrai cambiare anche il CSS.

Tuttavia, non è consigliabile scrivere solo img come selettore per quell'elemento, in quanto verrebbero selezionati tutti gli elementi immagine della pagina.

Trovare il giusto equilibrio è spesso piuttosto difficile. Nel corso degli anni, alcuni sviluppatori hanno trovato soluzioni e soluzioni alternative per aiutarti in situazioni come questa. Ad esempio:

  • Metodologie come il BEM richiedono di assegnare a questo elemento una classe card__img card__img--hero per mantenere bassa la specificità, consentendoti al contempo di essere specifico in ciò che selezioni.
  • 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, ai selettori per impedire loro di scegliere come target elementi sul lato opposto della pagina.
  • Alcune librerie aboliscono del tutto i selettori e richiedono di inserire gli attivatori di stile direttamente nel markup.

E se non ne avessi bisogno? Cosa accadrebbe se CSS ti offrisse un modo per indicare con precisione gli elementi selezionati, senza che tu debba scrivere selettori ad alta specificità o strettamente associati al tuo DOM? È qui che entra in gioco @scope, offrendoti la possibilità di selezionare elementi solo all'interno di una sottostruttura del DOM.

Ti presentiamo @scope

Con @scope puoi limitare la copertura dei selettori. Per farlo, imposta la principale ambito che determina il limite superiore della struttura ad albero secondaria che vuoi scegliere come target. Con un set radice di ambito, le regole di stile contenute, denominate regole di stile con ambito, possono effettuare una selezione solo da quel sottoalbero limitato del DOM.

Ad esempio, per scegliere come target solo gli elementi <img> nel componente .card, devi impostare .card come radice di ambito della regola at @scope.

@scope (.card) {
    img {
        border-color: green;
    }
}

La regola per lo stile basata su ambito img { … } può selezionare in modo efficace solo gli elementi <img> che rientrano nell'ambito dell'elemento .card corrispondente.

Per impedire che gli elementi <img> all'interno dell'area dei contenuti della scheda (.card__content) vengano selezionati, puoi rendere più specifico il selettore img. Un altro modo per farlo è utilizzare il fatto che la regola at di @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 ha come target solo gli elementi <img> posizionati tra gli elementi .card e .card__content nell'albero predecessore. Questo tipo di ambito, con un limite superiore e un limite inferiore, viene spesso definito donut scope

Selettore :scope

Per impostazione predefinita, tutte le regole di stile con ambito sono relative alla radice di ambito. È anche possibile scegliere come target l'elemento principale dell'ambito. A questo scopo, 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 vengono implicitamente anteposti a :scope. Se vuoi, puoi esprimerti in modo esplicito in merito, anteponendo a te :scope. In alternativa, puoi anteporre il selettore & da Nesting CSS.

@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 di 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 al di fuori della loro radice di ambito utilizzando :scope. Ad esempio:

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

Tieni presente che le regole di stile con ambito non possono eseguire l'escape della struttura ad albero secondario. Le selezioni come :scope + p non sono valide perché si tenta di selezionare elementi che non rientrano nell'ambito.

@scope e specificità

I selettori che utilizzi nel preludio di @scope non influiscono sulla specificità dei selettori contenuti. Nell'esempio seguente, la specificità del selettore img è ancora (0,0,1).

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        …
    }
}

La specificità di :scope è quella di una pseudo-classe normale, ovvero (0,1,0).

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}

Nell'esempio seguente, internamente, & viene riscritta 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. Questo processo è noto come desugaring.

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        …
    }
}

Poiché & viene deprecato utilizzando :is(), la specificità di & viene calcolata seguendo le regole di specificità :is(): la specificità di & è quella dell'argomento più specifico.

Applicata a questo esempio, la specificità di :is(#sidebar, .card) è quella del suo argomento più specifico, ossia #sidebar, e pertanto diventa (1,0,0). Combinando questo aspetto 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) */
        …
    }
}

Differenza tra :scope e & in @scope

Oltre alle differenze nel modo in cui viene calcolata la specificità, un'altra differenza tra :scope e & è che :scope rappresenta la radice di ambito con corrispondenza, mentre & rappresenta il selettore utilizzato per corrispondere alla radice di ambito.

Per questo motivo è possibile utilizzare & più volte. A differenza di :scope, che puoi utilizzare una sola volta, poiché non puoi far corrispondere una radice di ambito all'interno di una radice di ambito.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :root :root { /* ❌ Does not work */
    …
  }
}

Ambito senza preludi

Quando scrivi stili incorporati con l'elemento <style>, puoi limitare l'ambito delle regole di stile all'elemento principale che include l'elemento <style> senza specificare alcuna radice di ambito. Per farlo, 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 hanno come target solo gli elementi all'interno di div con nome della classe card__header, perché div è l'elemento principale dell'elemento <style>.

@scope nella cascata

All'interno della Cascade CSS, @scope aggiunge anche un nuovo criterio: vicinanza ambito. Il passaggio viene dopo la specificità, ma prima dell'ordine di apparizione.

Visualizzazione di Cascade CSS.

Come in base alla specifica:

Quando confronti le dichiarazioni che appaiono nelle regole di stile con radici di ambito diverse, prevale la dichiarazione con il minor numero di hop di elementi generazionali o di pari livello tra la radice di ambito e il soggetto della regola di stile con ambito.

Questo nuovo passaggio è utile quando si nidificano diverse varianti di un componente. Prendiamo ad esempio 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 quel piccolo markup, il terzo link sarà white anziché black, anche se è un elemento secondario di un elemento div con la classe .light applicata. Ciò è dovuto al criterio dell'ordine di apparizione che la cascata utilizza qui per determinare il vincitore. Vede che .dark a è stato dichiarato l'ultimo, quindi vincerà dalla regola .light a

Con il criterio di prossimità 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 con ambito hanno la stessa specificità, viene attivato il criterio di prossimità dell'ambito. Pesa entrambi i selettori in base alla vicinanza alla radice dell'ambito. Per quel terzo elemento a, si tratta solo di un hop alla radice di ambito .light, ma di due a quello .dark. Di conseguenza, il selettore a in .light vincerà.

Nota di chiusura: isolamento del selettore, non dell'isolamento dello stile

È importante tenere presente che @scope limita la copertura dei selettori, non offre l'isolamento dello stile. Le proprietà che ereditano fino a elementi secondari verranno comunque ereditate oltre il limite inferiore di @scope. Una di queste proprietà è color. Quando lo dichiari all'interno di un ambito ad anello, l'elemento color verrà comunque ereditato dai figli all'interno del foro dell'anello.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

Nell'esempio precedente, l'elemento .card__content e i relativi elementi secondari hanno un colore hotpink perché ereditano il valore da .card.

(Foto di copertina di rustam burkhanov su Unsplash)