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

Scopri come utilizzare @scope per selezionare elementi solo all'interno di un sottoalbero limitato del tuo DOM.

Supporto dei browser

  • 118
  • 118
  • x
  • 17,4

Origine

La delicata arte di scrivere selettori CSS

Quando scrivi i selettori, potresti ritrovarti divisa tra due mondi. Da un lato, vuoi essere abbastanza specifico sugli elementi che selezioni. D'altra parte, i selettori devono poter eseguire facilmente l'override e non sono 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 ((0,3,1)), che rende difficile eseguire l'override man mano che il codice cresce.
  • Basandosi sul combinatore figlio diretto, è strettamente associato alla struttura DOM. Se il markup dovesse cambiare, dovrai modificare anche il CSS.

Inoltre, non è opportuno scrivere solo img come selettore per quell'elemento, perché verrebbero selezionati tutti gli elementi dell'immagine della pagina.

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

  • Metodologie come il BEM impongono di assegnare all'elemento una classe card__img card__img--hero per mantenere bassa la specificità e, al contempo, essere specifica in ciò che viene selezionato.
  • Le soluzioni basate su JavaScript come CSS con ambito o componenti con stile riscrivono tutti i tuoi selettori aggiungendo stringhe generate in modo casuale, ad esempio sc-596d7e0e-4, per impedire che eseguano il targeting degli elementi sul lato opposto della pagina.
  • Alcune librerie eliminano del tutto i selettori e richiedono di inserire gli attivatori di stile direttamente nel markup.

E se non ne avessi bisogno? Immagina se il CSS ti offra un modo per essere abbastanza specifico riguardo agli elementi selezionati, senza che tu debba scrivere selettori ad alta specificità o strettamente associati al tuo DOM? È qui che entra in gioco @scope, offrendoti un modo per selezionare elementi solo all'interno di un sottoalbero del tuo DOM.

Introduzione a @scope

Con @scope puoi limitare la copertura dei tuoi selettori. Per farlo, devi impostare la radice dell'ambito che determina il limite superiore del sottoalbero che vuoi scegliere come target. Con un set principale di definizione dell'ambito, le regole di stile contenute, denominate regole di stile con ambito, possono selezionare solo da quel sottoalbero limitato del DOM.

Ad esempio, per scegliere come target solo gli elementi <img> nel componente .card, imposti .card come radice dell'ambito della regola at-rule di @scope.

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

La regola di stile con ambito img { … } può selezionare a tutti gli effetti solo gli elementi <img> che rientrano nell'ambito dell'elemento .card corrispondente.

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

Selettore :scope

Per impostazione predefinita, tutte le regole di stile con ambito sono relative alla radice di definizione dell'ambito. È anche possibile scegliere come target l'elemento principale di definizione dell'ambito stesso. 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 vengono anteposti implicitamente a :scope. Se vuoi, puoi anche mostrarlo in modo esplicito anteponendo tu :scope. In alternativa, puoi anteporre il selettore & a 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 definizione dell'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ò anche fare riferimento a elementi al di fuori della radice di definizione dell'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 stesse non possono eseguire l'escape del sottoalbero. Selezioni come :scope + p non sono valide perché 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 riportato di seguito, 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 regolare, 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 dell'ambito, aggregato all'interno di un selettore :is(). Alla fine, il browser utilizzerà :is(#sidebar, .card) img come selettore per la corrispondenza. Questa procedura è nota come desugaring.

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

Poiché & viene desugato usando :is(), la specificità di & viene calcolata seguendo le regole di specificità :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 quindi diventa (1,0,0). Combina questo risultato con la specificità di img, che è (0,0,1), e 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 di definizione dell'ambito corrispondente, mentre & rappresenta il selettore utilizzato per corrispondere alla radice di valutazione.

Per questo motivo, è possibile utilizzare & più volte. A differenza di :scope, che puoi utilizzare solo una volta, in quanto non è possibile far corrispondere una radice di ambito all'interno di una radice di definizione dell'ambito.

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

Ambito senza premessa

Quando scrivi stili incorporati con l'elemento <style>, puoi limitare le regole di stile all'elemento principale contenente 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 dell'div con il nome della classe card__header, perché questo div è l'elemento principale dell'elemento <style>.

@scope nella cascata

All'interno di Cascade CSS, @scope aggiunge anche un nuovo criterio: prossimità dell'ambito. Questo passaggio avviene dopo la specificità, ma prima dell'ordine di apparizione.

Visualizzazione della Cascade CSS.

Come secondo specifiche:

Quando confronti le dichiarazioni che compaiono nelle regole di stile con radici di ambito diverse, vince la dichiarazione con il minor numero di elementi generazionali o di pari livello tra la radice di definizione dell'ambito e il soggetto della regola di stile con ambito.

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

Con il criterio di prossimità dell'ambito, il seguente problema è 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 di definizione dell'ambito. Per il terzo elemento a, è necessario un solo hop alla radice di ambito .light, ma due a quello .dark. Di conseguenza, il selettore a in .light vincerà.

Nota di chiusura: isolamento tramite selettore, non isolamento dello stile

Una nota importante da tenere presente è che @scope limita la copertura dei selettori e non offre isolamento dello stile. Le proprietà che ereditano l'eredità verso il basso negli elementi secondari continueranno a ereditare, oltre il limite inferiore di @scope. Una di queste proprietà è color. Quando dichiari che un elemento è all'interno di un ambito ad anello, color continuerà comunque a ereditare i valori secondari 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)