Beperk het bereik van uw selectors met de CSS @scope at-regel

Leer hoe u @scope kunt gebruiken om alleen elementen binnen een beperkte substructuur van uw DOM te selecteren.

Browser Support

  • Chroom: 118.
  • Rand: 118.
  • Firefox: achter een vlag.
  • Safari: 17.4.

Source

De delicate kunst van het schrijven van CSS-selectors

Bij het schrijven van selectors wordt u misschien heen en weer geslingerd tussen twee werelden. Aan de ene kant wil je behoorlijk specifiek zijn over welke elementen je selecteert. Aan de andere kant wilt u dat uw selectors gemakkelijk te overschrijven zijn en niet nauw gekoppeld zijn aan de DOM-structuur.

Wanneer u bijvoorbeeld “de heldafbeelding in het inhoudsgebied van de kaartcomponent” wilt selecteren – wat een nogal specifieke elementselectie is – wilt u waarschijnlijk geen selector schrijven zoals .card > .content > img.hero .

  • Deze selector heeft een vrij hoge specificiteit van (0,3,1) waardoor deze moeilijk te overschrijven is naarmate uw code groeit.
  • Door te vertrouwen op de directe kindcombinator is deze nauw gekoppeld aan de DOM-structuur. Mocht de opmaak ooit veranderen, dan moet u ook uw CSS wijzigen.

Maar u wilt ook niet alleen img als selector voor dat element schrijven, omdat daarmee alle afbeeldingselementen op uw pagina zouden worden geselecteerd.

Hierin de juiste balans vinden is vaak een hele uitdaging. Door de jaren heen hebben sommige ontwikkelaars oplossingen en oplossingen bedacht om u in situaties als deze te helpen. Bijvoorbeeld:

  • Methodologieën zoals BEM schrijven voor dat je dat element een klasse card__img card__img--hero geeft om de specificiteit laag te houden, terwijl je toch specifiek kunt zijn in wat je selecteert.
  • Op JavaScript gebaseerde oplossingen zoals Scoped CSS of Styled Components herschrijven al uw selectors door willekeurig gegenereerde tekenreeksen (zoals sc-596d7e0e-4 ) toe te voegen aan uw selectors om te voorkomen dat ze zich richten op elementen aan de andere kant van uw pagina.
  • Sommige bibliotheken schrappen selectors zelfs helemaal en vereisen dat je de stylingtriggers rechtstreeks in de markup zelf plaatst.

Maar wat als je die allemaal niet nodig had? Wat als CSS je een manier zou bieden om behoorlijk specifiek te zijn over welke elementen je selecteert, zonder dat je selectors met een hoge specificiteit hoeft te schrijven of selectors die nauw gekoppeld zijn aan je DOM? Welnu, dat is waar @scope in het spel komt en je een manier biedt om alleen elementen binnen een subboom van je DOM te selecteren.

Maak kennis met @scope

Met @scope kunt u het bereik van uw selectors beperken. U doet dit door de scoping root in te stellen die de bovengrens bepaalt van de subboom die u wilt targeten. Met een scoping root set kunnen de ingesloten stijlregels – de zogenaamde scoped stijlregels – alleen selecteren uit die beperkte subboom van de DOM.

Als u bijvoorbeeld alleen de <img> -elementen in de component .card wilt targeten, stelt u .card in als de scoping root van de @scope at-regel.

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

De bereikstijlregel img { … } kan effectief alleen <img> -elementen selecteren die binnen het bereik van het overeenkomende .card element vallen.

Om te voorkomen dat de <img> -elementen in het inhoudsgebied 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 bereik richt zich alleen op <img> -elementen die tussen de elementen .card en .card__content in de stamboom zijn geplaatst. Dit type scoping – met een boven- en ondergrens – wordt vaak een donut-scope genoemd

De :scope selector

Standaard zijn alle stijlregels met bereik relatief ten opzichte van de scoping root. Het is ook mogelijk om het scoping root-element zelf te targeten. Gebruik hiervoor de :scope -selector.

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

Selectors binnen bereikstijlregels krijgen impliciet :scope voorafgegaan. Als je wilt, kun je er expliciet over zijn, door zelf :scope ervoor te zetten. Als alternatief kunt u de & -selector vooraf laten gaan, vanuit 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 scopinglimiet kan de :scope pseudo-klasse gebruiken om een ​​specifieke relatie met de scopingroot te vereisen:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

Een scopinglimiet kan ook verwijzen naar elementen buiten hun scopingroot door :scope te gebruiken. Bijvoorbeeld:

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

Merk op dat de scoped-stijlregels zelf niet aan de subboom kunnen ontsnappen. Selecties zoals :scope + p zijn ongeldig omdat daarmee elementen worden geselecteerd die niet binnen het bereik vallen.

@scope en specificiteit

De selectors die u in de inleiding voor @scope gebruikt, hebben geen invloed op de specificiteit van de opgenomen selectors. In het onderstaande voorbeeld is de specificiteit van de img selector nog steeds (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 & intern herschreven naar de selector die wordt gebruikt voor de scoping root, verpakt in een :is() selector. Uiteindelijk zal de browser :is(#sidebar, .card) img gebruiken als selector om de matching uit te voeren. Dit proces staat bekend als ontsuikeren .

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

Omdat & wordt ontsuikerd met behulp van :is() , wordt de specificiteit van & berekend volgens de :is() specificiteitsregels : de specificiteit van & is die van zijn meest specifieke argument.

Toegepast op dit voorbeeld is de specificiteit van :is(#sidebar, .card) die van zijn meest specifieke argument, namelijk #sidebar , en wordt daarom (1,0,0) . Combineer dat met de specificiteit van img – namelijk (0,0,1) – en je krijgt (1,0,1) als specificiteit voor de hele complexe selector.

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        
    }
}

Het verschil tussen :scope en & binnen @scope

Naast verschillen in de manier waarop specificiteit wordt berekend, is een ander verschil tussen :scope en & dat :scope de overeenkomende scopingwortel vertegenwoordigt, terwijl & de selector vertegenwoordigt die wordt gebruikt om de scopingwortel te matchen.

Hierdoor is het mogelijk om & meerdere keren te gebruiken. Dit is in tegenstelling tot :scope dat u slechts één keer kunt gebruiken, omdat u een scoping root niet binnen een scoping root kunt matchen.

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

Prelude-loze reikwijdte

Wanneer u inline stijlen schrijft met het <style> -element, kunt u de stijlregels beperken tot het omsluitende bovenliggende element van het <style> -element, door geen bereikbasis op te geven. Dit doe je door de prelude van @scope 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 richten de scoped-regels zich alleen op elementen binnen de div met de klassenaam card__header , omdat die div het bovenliggende element van het <style> -element is.

@scope in de cascade

Binnen de CSS Cascade voegt @scope ook een nieuw criterium toe: scoping proximity . De stap komt na specificiteit, maar vóór de volgorde van verschijnen.

Visualisatie van de CSS-cascade.

Volgens specificatie :

Bij het vergelijken van declaraties die voorkomen in stijlregels met verschillende bereikwortels, wint de declaratie met het minste generatie- of zusterelement tussen de scopingwortel en het onderwerp van de bereikstijlregel.

Deze nieuwe stap is handig bij het nesten van verschillende varianten van een component. Neem dit voorbeeld, dat @scope nog niet gebruikt:

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

Als je dat kleine stukje markup bekijkt, zal de derde link white zijn in plaats van black , ook al is het een kind van een div waarop de klasse .light is toegepast. Dit komt door het volgorde-van-verschijningscriterium dat de cascade hier gebruikt om de winnaar te bepalen. Het ziet dat .dark a als laatste is verklaard, dus het wint van de .light a -regel

Met het scoping nabijheidscriterium is dit nu opgelost:

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

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

Omdat beide scoped a dezelfde specificiteit hebben, treedt het scoping-nabijheidscriterium in werking. Het weegt beide selectors op basis van de nabijheid van hun bereikwortel. Voor dat derde a is het slechts één sprong naar de .light scopingwortel, maar twee naar de .dark wortel. Daarom zal de a selector in .light winnen.

Slotopmerking: Selectorisolatie, niet stijlisolatie

Een belangrijke opmerking om te maken is dat @scope het bereik van de selectors beperkt, het biedt geen stijlisolatie. Eigenschappen die overerven van kinderen, zullen nog steeds erven, voorbij de ondergrens van de @scope . Eén zo'n eigenschap is de color . Als je aangeeft dat er één in een donut-scope zit, zal de color nog steeds worden overgenomen van kinderen in het gat van de donut.

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

In het bovenstaande voorbeeld hebben het element .card__content en zijn onderliggende elementen een hotpink kleur omdat ze de waarde van .card overnemen.

(Omslagfoto door rustam burkhanov op Unsplash )