css och layouter

CSS @layer: hur cascade layers avslutar kriget om specificitet

Du har en kortkomponent från ett designsystembibliotek: .card .card__header .card__title { font-size: 18px; } – tre klasser i selektorn, eftersom komponentens författare nästlade den så. Du vill överskrida den med en enda hjälpklass, .text-lg { font-size: 20px; }, tillagd i HTML:en efter kortets klasser. Det fungerar inte. Titeln stannar envist på 18px, trots att .text-lg står senare i koden, så intuitivt "borde den nyare vinna". Det är inget webbläsarfel – tre klasser i en selektor slår en klass, oavsett ordningen i filen. Så har specificitet fungerat sedan 1996, och så ska den fungera.

Den klassiska lösningen är att höja den egna regelns specificitet – .card .text-lg, eller, när det inte räcker, !important. Båda lösningarna fungerar punktvis och går sönder systematiskt: varje gång någon i teamet lägger till en regel med ännu högre specificitet (eller ännu en !important) blir resten av stilmallen oförutsägbar. Det är exakt det problemet – inte estetiskt, utan arkitektoniskt – som CSS Cascade Layers löser.

En ny axel för att avgöra konflikter, inte ännu ett sätt att höja prioritet

Den avgörande insikten: @layer är inte ännu ett verktyg för att öka en regels prioritet, som !important eller att lägga till en klass till i selektorn. Det är ett separat steg i kaskad-algoritmen, avgjort innan specificiteten, inte bredvid den. Den fullständiga ordning i vilken webbläsaren avgör en konflikt mellan två CSS-regler ser (förenklat, om vi bortser från origin) ut så här:

  1. Lager (@layer) – en regel från ett senare deklarerat lager vinner över en regel från ett tidigare deklarerat lager, oavsett selektorernas specificitet.
  2. Specificitet – avgör först när båda reglerna är i samma lager (eller båda helt utanför något lager).
  3. Ordning i källkoden – den sista domaren, när de föregående två kriterierna ger oavgjort.

Det gör att en enda klass .text-lg i ett senare lager kan besegra en selektor med tre klasser i ett tidigare lager – för konfrontationen når aldrig ens fram till steget där specificitet jämförs. Lagret avgör saken innan dess.

css

/* Lagerordningen deklarerar du en gång, i förväg – den behöver inte
matcha den ordning innehållet faktiskt dyker upp i filerna */
@layer reset, base, components, utilities;
@layer components {
.card .card__header .card__title {
font-size: 18px;
}
}
@layer utilities {
.text-lg {
font-size: 20px;
}
}

Den tomma deklarationen @layer reset, base, components, utilities; högst upp i filen är ingen formalitet – det är ett uttryckligt fastställande av prioritetsordningen, innan något av dessa lager får något faktiskt innehåll. Tack vare det är lagerordningen oberoende av importordning eller filordning – components kan fysiskt hamna i koden före utilities, och utilities vinner ändå, eftersom den ordningen deklarerades från början.

Praktiskt exempel: reset, komponenter och hjälpklasser som aldrig krigar med varandra

I praktiken är det här mönstret – reset lägre än komponenter, komponenter lägre än hjälpklasser – exakt hur t.ex. lagersystemet i Tailwind fungerar sedan version 3.1: en hjälpklass ska alltid vinna över en komponentregel, oavsett hur många klasser komponentens selektor har, för det är precis därför hjälpklasser överhuvudtaget finns.

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

Lägg märke till att !important vid .text-lg i lagret utilities här är överflödigt – lagret ger redan prioritet, utan att behöva ta till CSS tyngsta vapen. Det är en bra illustration av varför cascade layers verkligen minskar antalet !important i koden: du slutar behöva det som en genväg runt specificiteten, eftersom du har ett verktyg som är designat exakt för den uppgiften.

Samma mekanism löser ett andra, lika vanligt problem – ett externt CSS-bibliotek vars selektorer du inte kontrollerar, och vars prioritet du vill ha alltid lägre än dina egna stilar, utan att höja specificiteten på dina egna regler:

css

@import url("some-ui-library.css") layer(vendor);
@layer vendor, base, components, utilities;

Hela biblioteket hamnar i lagret vendor, placerat allra först i ordningen – så dina base, components och utilities slår det alltid, oavsett hur aggressivt biblioteket bygger sina selektorer.

Fallgropen som bryter mot intuitionen: stilar utanför lager slår alla lager

Det här är den oftast förbisedda, men samtidigt mest konsekvensrika detaljen i hela mekanismen: en regel som inte tillhör något lager vinner över en regel i vilket lager som helst – för vanliga, icke-!important-deklarationer. Den "förlorar inte för att den är i ett osynligt lager längst bak" – den vinner alltid, ovillkorligt, mot varje lagerregel, även om den senare tillhör ett lager deklarerat som högsta prioritet.

css

@layer utilities {
.text-lg {
font-size: 20px;
}
}
/* Den här regeln, trots att den inte tillhör något lager och ser ut
som en slumpmässig, enstaka override – VINNER över .text-lg ovan,
oavsett var i filen den befinner sig */
.title {
font-size: 16px;
}

Det beteendet är avsiktligt – tack vare det är det säkert att gradvis införa cascade layers i ett befintligt, stort projekt: all "gammal" CSS som ännu inte flyttats in i något lager behåller som standard högsta prioritet, så den börjar inte plötsligt förlora mot nyligen tillagda lager. Men samma beteende är också källan till de mest förvirrande buggarna i team som inför @layer delvis: en enstaka regel som av misstag lämnas utanför ett lager – ett stavfel, en glömd import, ett <style>-block injicerat av ett tredjepartsbibliotek – slår hela det noggrant designade prioritetssystemet, utan någon varning. Den praktiska konsekvensen: om du bestämmer dig för cascade layers, lägg konsekvent allt i något lager, inklusive reset och basstilar – lämna inte en enda regel "utanför", för det är den, inte lagret utilities, som vinner varje konflikt.

Den andra fallgropen: !important vänder på lagerordningen

Om du använder !important inuti ett lager vänds prioritetsordningen mellan lagren om. För vanliga deklarationer vinner det senare deklarerade lagret. För deklarationer med !important vinner det tidigare deklarerade lagret.

css

@layer reset, components;
@layer reset {
.card { padding: 0 !important; }
}
@layer components {
.card { padding: 16px !important; }
}
/* reset (0px) vinner, eftersom det vid !important är det tidigare
deklarerade lagret som har företräde – exakt tvärtom mot regeln
som gäller utan !important */

Det är ingen nyck i specifikationen – det är en konsekvent förlängning av en regel som redan fanns för origins (användarens !important-stilmallar har alltid slagit sidförfattarens !important, tvärtom mot vanliga deklarationer). Den praktiska slutsatsen: !important inuti ett lagersystem kan överraska även den som förstår den vanliga @layer-ordningen väl – ännu ett gott skäl att behandla !important som något du tar till medvetet och sällan, inte som ett standardsätt att vinna en konfrontation.

Fallgropar och god praxis

  • Noll regler utanför lager, om systemet ska vara förutsägbart. Som visat ovan slår redan en enda regel utan @layer hela lagersystemet för vanliga deklarationer – konsekvens väger tyngre än bekvämligheten att i farten lägga till "bara en liten regel".
  • Specificitet gäller fortfarande inom ett enskilt lager. @layer eliminerar specificitetskrig mellan lager, men inuti ett lager avgör två regler fortfarande konflikten som vanligt – förnuftig klassnamngivning och att undvika djupt nästlade selektorer lönar sig fortfarande.
  • Deklarera lagerordningen explicit, längst upp, i en tom @layer namn1, namn2, ...;. Det gör dig oberoende av importordningen på filer – prioritetsordningen fastställs en gång, tydligt, på ett ställe, oavsett i vilken ordning de enskilda CSS-filerna faktiskt senare laddas.
  • Webbläsarstöd är inte längre ett skäl till försiktighet. Cascade Layers nådde Baseline-status (brett tillgängligt) redan 2022 – Chrome 99, Firefox 97, Safari 15.4 – så de går att införa utan @supports i alla nya projekt.

Sammanfattning

@layer konkurrerar inte med specificiteten – den verkar i ett helt annat steg av kaskad-algoritmen, tidigare, och låter därför en enda hjälpklass medvetet och förutsägbart besegra en selektor med tre nästlade klasser, utan att höja prioritet och utan !important. Det är ett verkligt arkitektoniskt verktyg för att dela upp reset, basstilar, komponenter och hjälpklasser i lager med en uttryckligen deklarerad viktighetsordning – men två av dess regler bryter så pass mot intuitionen att de förtjänar att memoreras separat: en stil utanför alla lager slår varje lager, och !important inuti lager vänder på deras prioritetsordning. Att ignorera någon av dessa två fakta slutar inte med ett fel i konsolen – det slutar med CSS som beter sig annorlunda än vad den till synes logiska lagerordningen i filen skulle antyda.