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

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

Supporto dei browser

  • Chrome: 118.
  • Edge: 118.
  • Firefox: dietro un flag.
  • Safari: 17.4.

Origine

L'arte delicata della scrittura di selettori CSS

Quando scrivi i selettori, potresti trovarti indeciso tra due mondi. Da un lato, devi essere abbastanza specifico sugli elementi da selezionare. 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 hero nell'area dei contenuti del componente della 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 la sua sostituzione man mano che il codice cresce.
  • Poiché si basa sul combinatore di elementi secondari diretti, è strettamente accoppiato alla struttura del DOM. Se il markup dovesse cambiare, devi modificare anche il CSS.

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

Trovare il giusto equilibrio in questo caso è spesso una sfida. Nel corso degli anni, alcuni sviluppatori hanno trovato soluzioni e soluzioni alternative per aiutarti in situazioni come queste. Ad esempio:

  • Metodologie come BEM richiedono di assegnare all'elemento una classe card__img card__img--hero per mantenere bassa la specificità, pur consentendoti di essere specifico in ciò che selezioni.
  • Le soluzioni basate su JavaScript, come il CSS basato su ambito o i componenti stilizzati, riscrivono tutti i selettori aggiungendo stringhe generate in modo casuale, come sc-596d7e0e-4, per impedire che abbiano come target elementi all'altro lato della pagina.
  • Alcune librerie aboliscono addirittura i selettori e richiedono di inserire gli attivatori degli stili direttamente nel markup stesso.

Ma cosa succede se non ne hai bisogno? E se CSS ti offrisse un modo per essere abbastanza specifico sugli elementi che selezioni, senza dover scrivere selettori di elevata specificità o strettamente accoppiati al tuo 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 ad @scope

Con @scope puoi limitare la copertura dei selettori. A tal fine, imposta l'elemento scoping root, che determina il limite superiore del sottoalbero che vuoi scegliere come target. Con un insieme di regole di ambito impostato, le regole di stile contenute, denominate regole di stile basate su ambito, possono selezionare solo il sottoalbero limitato del DOM.

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

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

La regola di stile basata sugli ambiti 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 basata sugli ambiti ha come target solo gli elementi <img> posizionati tra gli elementi .card e .card__content nell'albero degli antenati. Questo tipo di ambito, con un limite superiore e uno inferiore, è spesso indicato come ambito a forma di ciambella.

Il selettore :scope

Per impostazione predefinita, tutte le regole di stile basate sugli ambiti sono relative all'elemento radice dell'ambito. È anche possibile scegliere come target l'elemento principale di ambito stesso. 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 */
    }
}

Ai selettori all'interno delle regole di stile basate sugli ambiti viene anteposto :scope in modo implicito. Se vuoi, puoi essere esplicito anteponendo :scope. In alternativa, puoi anteporre il selettore & dal nidificazione 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 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) { ... }

Tieni presente che le regole di stile basate sugli ambiti non possono uscire dal sottoalbero. Selezioni come :scope + p non sono valide perché tentano di selezionare elementi non inclusi nell'ambito.

@scope e specificità

I selettori utilizzati nel preludio per @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 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 l'elemento radice dell'ambito, racchiuso in un selettore :is(). Alla fine, il browser utilizzerà :is(#sidebar, .card) img come selettore per eseguire la corrispondenza. Questo processo è noto come rimozione del codice non necessario.

@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.

Applicato a questo esempio, la specificità di :is(#sidebar, .card) è quella del suo argomento più specifico, ovvero #sidebar, e quindi diventa (1,0,0). Se combini questa operazione con la specificità di img, ovvero (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 calcolo della specificità, un'altra differenza tra :scope e & è che :scope rappresenta l'elemento radice dell'ambito corrispondente, mentre & rappresenta il selettore utilizzato per trovare una corrispondenza con l'elemento radice dell'ambito.

Per questo motivo, è possibile utilizzare & più volte. A differenza di :scope, che puoi utilizzare una sola volta, non puoi associare una radice di ambito all'interno di un'altra 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 applicare l'ambito delle regole di stile all'elemento principale che contiene l'elemento <style> non specificando alcuna radice di ambito. 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 basate sugli ambiti hanno come target solo gli elementi all'interno di div con il nome della classe card__header, perché div è l'elemento principale di <style>.

@scope nella struttura a cascata

All'interno della cascata CSS, @scope aggiunge anche un nuovo criterio: vicinanza allo scopo. Il passaggio viene visualizzato dopo la specificità, ma prima dell'ordine di visualizzazione.

Visualizzazione della struttura a cascata del CSS.

Secondo le specifiche:

Quando si confrontano le dichiarazioni visualizzate nelle 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 basata sugli ambiti.

Questo nuovo passaggio è utile quando si nidificano diverse varianti di un componente. Prendi 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 frammento di markup, il terzo link sarà white anziché black, anche se è un elemento secondario di un div a cui è applicata la classe .light. Questo accade a causa del criterio di ordine di visualizzazione utilizzato dalla struttura a cascata per determinare il vincitore. Vede che .dark a è stato dichiarato per ultimo, quindi vincerà in base alla regola .light a

Con il criterio di prossimità dell'ambito, il problema è stato risolto:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

Poiché entrambi i selettori a basati su ambito hanno la stessa specificità, viene attivato il criterio di prossimità basato su ambito. Pondera entrambi i selettori in base alla vicinanza alla radice dell'ambito. Per il terzo elemento a, è necessario un solo hop per raggiungere la radice di ambito .light, ma due per quella .dark. Pertanto, il selettore a in .light avrà la precedenza.

Nota finale: isolamento del selettore, non isolamento dello stile

È importante notare che @scope limita la portata dei selettori, ma non offre l'isolamento degli stili. Le proprietà che vengono ereditate dalle proprietà secondarie continueranno a essere ereditate, oltre il limite inferiore di @scope. Una di queste proprietà è color. Se lo dichiari all'interno di un ambito donut, color verrà comunque ereditato dagli elementi secondari all'interno del foro del donut.

@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)