CSS @layer: hoe cascade layers de oorlog om specificiteit beëindigen
CSS @layer: hoe cascade layers de oorlog om specificiteit beëindigen
Je hebt een kaartcomponent uit een designsysteem-library: .card .card__header .card__title { font-size: 18px; } – drie klassen in de selector, omdat de auteur van het component het zo genest heeft. Je wilt dit overschrijven met één utility-klasse, .text-lg { font-size: 20px; }, toegevoegd in de HTML na de klassen van de kaart. Het werkt niet. De titel blijft koppig op 18px staan, ondanks dat .text-lg later in de code voorkomt, waardoor je intuïtief zou verwachten dat "de nieuwere wint". Dat is geen browserbug – drie klassen in een selector verslaan één klasse, ongeacht de volgorde in het bestand. Zo werkt specificiteit al sinds 1996, en zo hoort het te werken.
De klassieke workaround is het opvoeren van de specificiteit van je eigen regel – .card .text-lg, of, als dat niet genoeg is, !important. Beide oplossingen werken lokaal en falen systemisch: elke keer dat iemand in het team een regel toevoegt met een nóg hogere specificiteit (of nóg een !important), wordt de rest van het stylesheet onvoorspelbaar. Precies dit probleem – geen esthetisch probleem, maar een architectonisch – lossen CSS Cascade Layers op.
Een nieuwe as voor het oplossen van conflicten, geen zoveelste manier om prioriteit op te voeren
Het cruciale om te begrijpen: @layer is geen extra instrument om de prioriteit van een regel te verhogen, zoals !important of het toevoegen van nóg een klasse aan de selector. Het is een aparte fase in het cascade-algoritme, die vóór specificiteit wordt beslecht, niet ernaast. De volledige volgorde waarin de browser een conflict tussen twee CSS-regels oplost, ziet er (vereenvoudigd, zonder origin) zo uit:
- Laag (
@layer) – een regel uit een later gedeclareerde laag wint van een regel uit een eerder gedeclareerde laag, ongeacht de specificiteit van de selectors erbinnen. - Specificiteit – wordt pas beslecht als beide regels in dezelfde laag zitten (of allebei in helemaal geen laag).
- Volgorde in de bron – de laatste scheidsrechter, wanneer de vorige twee criteria gelijkspel opleveren.
Dat maakt dat één klasse .text-lg in een latere laag een selector met drie geneste klassen in een eerdere laag kan verslaan – omdat het gevecht nooit de fase van het vergelijken van specificiteit bereikt. De laag beslecht de zaak al eerder.
css
/* Je declareert de volgorde van lagen één keer, van tevoren – die hoeft nietovereen te komen met de volgorde waarin hun inhoud daadwerkelijk in bestanden voorkomt */@layer reset, base, components, utilities;@layer components {.card .card__header .card__title {font-size: 18px;}}@layer utilities {.text-lg {font-size: 20px;}}
Die lege declaratie @layer reset, base, components, utilities; bovenaan het bestand is geen formaliteit – het is het expliciet vastleggen van de prioriteitsvolgorde, voordat een van deze lagen daadwerkelijke inhoud krijgt. Daardoor is de volgorde van lagen onafhankelijk van de volgorde van imports of bestanden – components kan fysiek eerder in de code staan dan utilities, en toch wint utilities, omdat die volgorde bij aanvang zo is vastgelegd.
Praktisch voorbeeld: reset, componenten en utilities die nooit met elkaar vechten
In de praktijk is dit patroon – reset lager dan componenten, componenten lager dan utility-klassen – precies hoe bijvoorbeeld het lagensysteem van Tailwind sinds versie 3.1 werkt: een utility-klasse hoort altijd te winnen van een componentregel, ongeacht hoeveel klassen de selector van dat component heeft, want daarvoor bestaan utility-klassen eigenlijk.
css
@layer reset, base, components, utilities;@layer reset {* {margin: 0;padding: 0;box-sizing: border-box;}}@layer base {body {font-family: system-ui, sans-serif;color: #1a1a1a;}}@layer components {.card {border: 1px solid #e0e0e0;border-radius: 12px;padding: 16px;}.card .card__header .card__title {font-size: 18px;font-weight: 600;}}@layer utilities {.text-lg {font-size: 20px !important;}.mt-4 {margin-top: 16px;}}
Merk op dat !important bij .text-lg in de laag utilities hier overbodig is – de laag zorgt al voor prioriteit, zonder dat je naar het zwaarste wapen van CSS hoeft te grijpen. Dat is een goede illustratie van waarom cascade layers het aantal !important's in code écht terugdringen: je hebt het niet meer nodig als workaround voor specificiteit, omdat je een instrument tot je beschikking hebt dat exact voor deze taak is ontworpen.
Hetzelfde mechanisme lost een tweede, even veelvoorkomend probleem op – een externe CSS-library waarvan je de selectors niet beheert en waarvan je de prioriteit altijd lager wilt houden dan je eigen stijlen, zonder de specificiteit van je eigen regels op te voeren:
css
@import url("some-ui-library.css") layer(vendor);@layer vendor, base, components, utilities;
De hele library belandt in de laag vendor, geplaatst helemaal aan het begin van de volgorde – dus je eigen base, components en utilities verslaan hem altijd, ongeacht hoe agressief die library zijn eigen selectors opbouwt.
Een valkuil die je intuïtie doorbreekt: stijlen zonder laag verslaan alle lagen
Dit is het meest over het hoofd geziene, en tegelijk het meest verstrekkende detail van het hele mechanisme: een regel die in geen enkele laag zit, wint van een regel in om het even welke laag – voor gewone, niet-!important declaraties. Niet "verliest, omdat hij in een onzichtbare laag helemaal achteraan zit" – hij wint altijd, onvoorwaardelijk, van elke regel in een laag, zelfs als die laatste in de laag zit die als hoogste prioriteit is gedeclareerd.
css
@layer utilities {.text-lg {font-size: 20px;}}/* Deze regel, ondanks dat hij in geen enkele laag zit en eruitziet alseen toevallige, geïsoleerde override – WINT van .text-lg hierboven,ongeacht waar in het bestand hij staat */.title {font-size: 16px;}
Dat gedrag is bewust – dankzij dit gedrag is geleidelijke invoering van cascade layers in een bestaand, groot project veilig: alle "oude" CSS die nog in geen enkele laag zit, behoudt standaard de hoogste prioriteit, dus begint hij niet plotseling te verliezen van nieuw toegevoegde lagen. Maar datzelfde gedrag is de bron van de meest verwarrende bugs bij teams die @layer gedeeltelijk invoeren: één enkele regel die per ongeluk buiten een laag is gebleven – een typefout, een vergeten import, een <style>-blok geïnjecteerd door een externe library – verslaat het hele zorgvuldig ontworpen prioriteitssysteem, zonder enige waarschuwing. Praktisch gevolg: als je voor cascade layers kiest, plaats dan consequent alles in een laag, inclusief de reset en basisstijlen – laat geen enkele regel "buiten" liggen, want die zal, niet de laag utilities, elk conflict winnen.
Tweede valkuil: !important keert de volgorde van lagen om
Als je binnen een laag naar !important grijpt, keert de prioriteitsvolgorde tussen lagen om. Voor gewone declaraties wint de later gedeclareerde laag. Voor declaraties met !important wint de eerder gedeclareerde laag.
css
@layer reset, components;@layer reset {.card { padding: 0 !important; }}@layer components {.card { padding: 16px !important; }}/* reset wint (0px), omdat bij !important voorrang gaat naarde eerder gedeclareerde laag – exact het omgekeerde van de regeldie zonder !important geldt */
Dit is geen gril van de specificatie – het is een consequente uitbreiding van een regel die al bestond voor origins (!important-stylesheets van de gebruiker versloegen altijd al !important van de auteur van de pagina, omgekeerd aan gewone declaraties). Praktische conclusie: !important binnen een lagensysteem kan zelfs iemand verrassen die de gewone volgorde van @layer goed begrijpt – nóg een goede reden om !important te behandelen als iets waar je bewust en zelden naar grijpt, niet als de standaardmanier om een conflict te winnen.
Valkuilen en goede praktijken
- Nul regels buiten lagen, als het systeem voorspelbaar moet blijven. Zoals hierboven getoond, verslaat zelfs één regel zonder
@layerhet hele lagensysteem voor gewone declaraties – consequentheid weegt zwaarder dan het gemak van "nog even snel één kleine regel" toevoegen. - Specificiteit blijft gelden binnen één enkele laag.
@layerelimineert specificiteitsoorlogen tussen lagen, maar binnen één laag beslechten twee regels het conflict nog steeds zoals altijd – zinvolle klassennamen en het vermijden van diep geneste selectors blijven de moeite waard. - Declareer de volgorde van lagen expliciet, helemaal bovenaan, met een lege
@layer naam1, naam2, ...;. Dat maakt je onafhankelijk van de volgorde waarin bestanden worden geïmporteerd – de prioriteitsvolgorde staat één keer, overzichtelijk, op één plek vast, ongeacht in welke volgorde de afzonderlijke CSS-bestanden later daadwerkelijk laden. - Browserondersteuning is geen reden meer tot voorzichtigheid. Cascade Layers bereikten Baseline-status (breed beschikbaar) al in 2022 – Chrome 99, Firefox 97, Safari 15.4 – dus je kunt ze zonder
@supportsinvoeren in elk nieuw project.
Samenvatting
@layer concurreert niet met specificiteit – het werkt op een compleet andere, eerdere fase van het cascade-algoritme, en stelt daardoor één utility-klasse in staat om bewust en voorspelbaar een selector met drie geneste klassen te verslaan, zonder prioriteit op te voeren en zonder !important. Het is een echt architectonisch instrument om reset, basisstijlen, componenten en utilities te verdelen over lagen met een expliciet vastgelegde volgorde van belang – maar twee van zijn regels doorbreken de intuïtie zo sterk dat ze de moeite waard zijn om apart te onthouden: een stijl buiten elke laag verslaat elke laag, en !important binnen lagen keert de volgorde van hun prioriteit om. Een van deze twee feiten negeren leidt niet tot een foutmelding in de console – het leidt tot CSS dat anders werkt dan de schijnbaar logische volgorde van lagen in het bestand zou doen vermoeden.