Leer hoe je @scope kunt gebruiken om alleen elementen binnen een beperkte substructuur van je DOM te selecteren.
Gepubliceerd: 4 oktober 2023
Bij het schrijven van selectors kun je je in een spagaat bevinden. Enerzijds wil je heel specifiek zijn over welke elementen je selecteert. Anderzijds wil je dat je selectors gemakkelijk te overschrijven blijven en niet te sterk gekoppeld zijn aan de DOM-structuur.
Als je bijvoorbeeld de hero-afbeelding in het contentgebied van de kaartcomponent wilt selecteren – wat een vrij specifieke elementselectie is – wil je waarschijnlijk geen selector schrijven zoals .card > .content > img.hero .
- Deze selector heeft een vrij hoge specificiteit van
(0,3,1)waardoor het lastig is om deze te overschrijven naarmate je code groeit. - Doordat het gebruikmaakt van de directe child combinator is het sterk gekoppeld aan de DOM-structuur. Als de markup ooit verandert, moet je ook je CSS aanpassen.
Maar je wilt ook niet alleen img als selector voor dat element gebruiken, want dat zou alle afbeeldingselementen op je pagina selecteren.
Het vinden van de juiste balans hierin is vaak een hele uitdaging. In de loop der jaren hebben sommige ontwikkelaars oplossingen en omwegen bedacht om je in dergelijke situaties te helpen. Bijvoorbeeld:
- Methodologieën zoals BEM schrijven voor dat je dat element een klasse geeft zoals
card__img card__img--heroom de specificiteit laag te houden, terwijl je toch specifiek kunt zijn in wat je selecteert. - JavaScript-gebaseerde oplossingen zoals Scoped CSS of Styled Components herschrijven al je selectors door willekeurig gegenereerde tekenreeksen – zoals
sc-596d7e0e-4– aan je selectors toe te voegen om te voorkomen dat ze elementen aan de andere kant van je pagina targeten. - Sommige libraries schaffen selectors zelfs helemaal af en vereisen dat je de stylingtriggers direct in de markup zelf plaatst.
Maar wat als je dat allemaal niet nodig had? Wat als CSS je een manier bood om heel specifiek te zijn over welke elementen je selecteert, zonder dat je selectors met een hoge specificiteit of selectors die sterk aan je DOM zijn gekoppeld hoeft te schrijven? Dat is waar @scope om de hoek komt kijken. Hiermee kun je elementen selecteren die zich alleen binnen een subboom van je DOM bevinden.
Introductie van @scope
Met @scope kun je het bereik van je selectors beperken. Dit doe je door de scoping root in te stellen, die de bovengrens bepaalt van de subboom waarop je je wilt richten. Met een ingestelde scoping root kunnen de opgenomen stijlregels – genaamd scoped style rules – alleen selecteren uit die beperkte subboom van de DOM.
Om bijvoorbeeld alleen de <img> -elementen in het .card component te selecteren, stelt u .card in als de scoping root van de @scope at-rule.
@scope (.card) {
img {
border-color: green;
}
}
De stijlregel img { … } met beperkte reikwijdte kan in feite alleen <img> `-elementen selecteren die zich binnen het bereik van het overeenkomende .card element bevinden.
Om te voorkomen dat de <img> -elementen binnen het contentgebied van de kaart ( .card__content ) worden geselecteerd, kunt u de img selector specifieker maken. Een andere manier om dit te doen is door gebruik te maken van het feit dat de @scope at-regel ook een scopinglimiet accepteert, die de ondergrens bepaalt.
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
Deze stijlregel met een beperkt bereik is alleen van toepassing op <img> -elementen die zich tussen .card en .card__content elementen in de bovenliggende boomstructuur bevinden. Dit type bereik – met een boven- en ondergrens – wordt vaak een donutbereik genoemd.
De :scope -selector
Standaard zijn alle stijlregels met een bereik relatief ten opzichte van het root-element. Het is ook mogelijk om het root-element zelf als doelwit te kiezen. Gebruik hiervoor de selector :scope .
@scope (.card) {
:scope {
/* Selects the matched .card itself */
}
img {
/* Selects img elements that are a child of .card */
}
}
Selectoren binnen scoped stijlregels krijgen impliciet :scope ervoor geplaatst. Je kunt dit desgewenst expliciet aangeven door zelf :scope toe te voegen. Als alternatief kun je de ` & selector gebruiken, zoals beschreven in 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 */
}
}
Een bereiklimiet kan de pseudo-klasse :scope gebruiken om een specifieke relatie tot de bereikbasis te vereisen:
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
Een bereiklimiet kan ook verwijzen naar elementen buiten het bereik waartoe het behoort door gebruik te maken van :scope . Bijvoorbeeld:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
De stijlregels binnen een bepaald bereik kunnen de subboom zelf niet verlaten. Selecties zoals :scope + p zijn ongeldig omdat hiermee elementen worden geselecteerd die zich niet binnen het bereik bevinden.
@scope en specificiteit
De selectors die je in de prelude voor @scope gebruikt, hebben geen invloed op de specificiteit van de daarin opgenomen selectors. In ons voorbeeld blijft de specificiteit van de img selector (0,0,1) .
@scope (#sidebar) {
img { /* Specificity = (0,0,1) */
...
}
}
De specificiteit van :scope is die van een reguliere pseudo-klasse, namelijk (0,1,0) .
@scope (#sidebar) {
:scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
...
}
}
In het volgende voorbeeld wordt de & intern herschreven naar de selector die wordt gebruikt voor de scoping root, ingekapseld in een :is() selector. Uiteindelijk gebruikt de browser :is(#sidebar, .card) img als selector om de overeenkomst te vinden. Dit proces staat bekend als desugaring .
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
...
}
}
Omdat & wordt ontdaan van suikers met behulp van :is() , wordt de specificiteit van & berekend volgens de specificiteitsregels van :is() : de specificiteit van & is gelijk aan die van het meest specifieke argument.
Toegepast op dit voorbeeld is de specificiteit van :is(#sidebar, .card) gelijk aan die van het meest specifieke argument, namelijk #sidebar , en wordt daarom (1,0,0) . Combineer dat met de specificiteit van img – die (0,0,1) is – en je krijgt (1,0,1) als specificiteit voor de gehele complexe selector.
@scope (#sidebar, .card) {
& img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
...
}
}
Het verschil tussen :scope en & binnen @scope
Naast de verschillen in de manier waarop specificiteit wordt berekend, is een ander verschil tussen :scope en & dat :scope de overeenkomende scoping root vertegenwoordigt, terwijl & de selector vertegenwoordigt die wordt gebruikt om de scoping root te matchen.
Hierdoor is het mogelijk om & meerdere keren te gebruiken. Dit in tegenstelling tot :scope , dat je slechts één keer kunt gebruiken, omdat je geen scoping root binnen een scoping root kunt matchen.
@scope (.card) {
& & { /* Selects a `.card` in the matched root .card */
}
:scope :scope { /* ❌ Does not work */
…
}
}
Inleidingloze reikwijdte
Bij het schrijven van inline stijlen met het <style> -element kun je de stijlregels beperken tot het omringende ouderelement van het <style> -element door geen scoping root op te geven. Dit doe je door de @scope -prelude weg te laten.
<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>
In het bovenstaande voorbeeld zijn de scoped rules alleen gericht op elementen binnen de div met de klassenaam card__header , omdat die div het ouderelement is van het <style> -element.
@scope in de cascade
Binnen de CSS Cascade voegt @scope ook een nieuw criterium toe: scoping proximity . Deze stap komt na specificity, maar vóór order of appearance.
Volgens specificatie :
Bij het vergelijken van declaraties die voorkomen in stijlregels met verschillende bereikswortels, wint de declaratie met het minste aantal generatie- of sibling-element-sprongen tussen de bereikswortel en het onderwerp van de stijlregel.
Deze nieuwe stap komt goed van pas wanneer je meerdere varianten van een component in elkaar nestelt. Neem bijvoorbeeld dit voorbeeld, dat nog geen gebruikmaakt @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>
Bij het bekijken van dat stukje code zal de derde link white zijn in plaats van black , ook al is het een kindelement van een div met de klasse .light . Dit komt door het volgordecriterium dat de cascade hier gebruikt om de winnaar te bepalen. Het ziet dat .dark a als laatste is gedeclareerd, dus wint die van de .light a regel.
Met het nabijheidscriterium voor de scope is dit nu opgelost:
@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}
@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}
Omdat beide scoped a selectors dezelfde specificiteit hebben, treedt het scoping-nabijheidscriterium in werking. Het weegt beide selectors op basis van hun nabijheid tot de scoping root. Voor dat derde a element is het slechts één stap naar de .light scoping root, maar twee naar de .dark scoping root. Daarom wint de a selector in .light .
Selectorisolatie, niet stijlisolatie.
Houd er rekening mee dat @scope het bereik van de selectors beperkt. Het biedt geen stijlisolatie. Eigenschappen die naar onderliggende elementen worden overgeërfd, blijven overerven, ook buiten de ondergrens van @scope . Een voorbeeld van zo'n eigenschap is de color . Wanneer je die binnen een `donut` scope declareert, wordt de color nog steeds overgeërfd naar onderliggende elementen binnen het gat van de donut.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
In het voorbeeld hebben het element .card__content en de bijbehorende kinderen een hotpink kleur omdat ze de waarde van .card overnemen.