css og layouts

CSS @layer: hvordan cascade layers afslutter krigen om specificitet

CSS @layer: hvordan cascade layers afslutter krigen om specificitet

Du har en kort-komponent fra et designsystem-bibliotek: .card .card__header .card__title { font-size: 18px; } – tre klasser i selektoren, fordi komponentens forfatter indlejrede den sådan. Du vil overtrumfe den med én hjælpeklasse, .text-lg { font-size: 20px; }, tilføjet i HTML'en efter kortets klasser. Det virker ikke. Titlen holder stædigt fast ved 18px, selvom .text-lg står senere i koden, hvilket intuitivt burde betyde, at "den nyeste burde vinde". Det er ikke en browserfejl – tre klasser i selektoren slår én klasse, uanset rækkefølgen i filen. Sådan har specificitet virket siden 1996, og sådan skal det virke.

Den klassiske løsning er at hæve sin egen regels specificitet – .card .text-lg, eller, hvis det ikke rækker, !important. Begge løsninger virker punktvis og går galt systemisk: hver gang nogen i teamet tilføjer en regel med endnu højere specificitet (eller endnu et !important), bliver resten af arket uforudsigeligt. Det er netop det problem – ikke æstetisk, men arkitektonisk – CSS Cascade Layers løser.

En ny akse til at afgøre konflikter, ikke endnu en måde at hæve prioritet på

Det centrale at forstå: @layer er ikke endnu et værktøj til at øge en regels prioritet, sådan som !important eller at tilføje endnu en klasse til selektoren. Det er et separat trin i cascade-algoritmen, afgjort før specificiteten, ikke ved siden af den. Den fulde rækkefølge, browseren bruger til at afgøre en konflikt mellem to CSS-regler, ser (forenklet ved at udelade origin) sådan ud:

  1. Lag (@layer) – en regel fra et lag erklæret senere vinder over en regel fra et lag erklæret tidligere, uanset selektorernes specificitet indeni.
  2. Specificitet – afgør først, når begge regler er i det samme lag (eller begge slet ikke er i noget lag).
  3. Kildeorden – den sidste dommer, når de to foregående kriterier giver uafgjort.

Det gør, at én klasse .text-lg i et senere lag kan slå en selektor med tre klasser i et tidligere lag – for opgøret når overhovedet aldrig frem til sammenligning af specificitet. Laget afgør sagen tidligere.

css

/* Du erklærer lagrækkefølgen én gang, på forhånd – den behøver ikke
stemme overens med den rækkefølge, indholdet faktisk optræder i filerne */
@layer reset, base, components, utilities;
@layer components {
.card .card__header .card__title {
font-size: 18px;
}
}
@layer utilities {
.text-lg {
font-size: 20px;
}
}

Den tomme erklæring @layer reset, base, components, utilities; øverst i filen er ikke en formalitet – det er en eksplicit fastlæggelse af prioritetsrækkefølgen, før nogen af de lag overhovedet får reelt indhold. Dermed er lagrækkefølgen uafhængig af rækkefølgen af imports eller filer – components kan fysisk stå i koden før utilities, og alligevel vinder utilities, fordi den rækkefølge blev erklæret fra starten.

Praktisk eksempel: reset, komponenter og hjælpeklasser, der aldrig kæmper mod hinanden

I praksis er dette mønster – reset lavere end komponenter, komponenter lavere end hjælpeklasser – præcis, hvordan lagsystemet i Tailwind fungerer fra version 3.1: en hjælpeklasse skal altid vinde over en komponentregel, uanset hvor mange klasser den komponents selektor har, for det er netop derfor, hjælpeklasser overhovedet findes.

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æg mærke til, at !important ved .text-lg i laget utilities her er overflødigt – laget sikrer allerede prioriteten, uden at man behøver ty til CSS' tungeste våben. Det er en god illustration af, hvorfor cascade layers reelt reducerer mængden af !important i koden: du holder op med at have brug for det som en omgåelse af specificitet, fordi du har et værktøj, der er designet præcis til den opgave.

Den samme mekanisme løser et andet, lige så hyppigt problem – et eksternt CSS-bibliotek, hvis selektorer du ikke kontrollerer, og hvis prioritet du altid vil have lavere end dine egne stilarter, uden at hæve specificiteten af dine egne regler:

css

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

Hele biblioteket havner i laget vendor, placeret allerførst i rækkefølgen – så dine base, components og utilities slår det altid, uanset hvor aggressivt det bibliotek bygger sine selektorer.

Faldgruben, der bryder intuitionen: stilarter uden lag slår alle lag

Det er den mest oversete, og samtidig mest konsekvensrige, detalje ved hele mekanismen: en regel, der ikke er i noget lag, vinder over en regel i et vilkårligt lag – for almindelige, ikke-!important-deklarationer. Ikke "taber, fordi den er i et usynligt lag til sidst" – den vinder altid, betingelsesløst, over enhver lagregel, selv hvis den sidste er i et lag erklæret som den højeste prioritet.

css

@layer utilities {
.text-lg {
font-size: 20px;
}
}
/* Denne regel, selvom den ikke er i noget lag og ligner en
tilfældig, enkeltstående override – VINDER over .text-lg ovenfor,
uanset hvor i filen den befinder sig */
.title {
font-size: 16px;
}

Den adfærd er bevidst – takket være den kan gradvis indførelse af cascade layers i et eksisterende, stort projekt foregå sikkert: al den "gamle" CSS, der endnu ikke er havnet i noget lag, bevarer som standard den højeste prioritet, så den ikke pludselig begynder at tabe til nyligt tilføjede lag. Men den samme adfærd er kilden til de mest forvirrende bugs i teams, der indfører @layer delvist: en enkelt regel, der ved et uheld er efterladt uden for et lag – en tastefejl, en glemt import, en <style>-blok injiceret af et tredjepartsbibliotek – vil slå hele det omhyggeligt designede prioritetssystem, uden nogen advarsel. Den praktiske konsekvens: hvis du beslutter dig for cascade layers, så put konsekvent alt i et eller andet lag, inklusive reset og basisstile – lad ingen regel stå "udenfor", for det er den, ikke laget utilities, der vinder enhver konflikt.

Den anden faldgrube: !important vender lagrækkefølgen om

Hvis du griber til !important inde i et lag, vender prioritetsrækkefølgen mellem lagene om. For almindelige deklarationer vinder laget erklæret senest. For deklarationer med !important vinder laget erklæret tidligst.

css

@layer reset, components;
@layer reset {
.card { padding: 0 !important; }
}
@layer components {
.card { padding: 16px !important; }
}
/* Reset vinder (0px), fordi ved !important har det lag,
der er erklæret tidligst, forrang – præcis det omvendte af
reglen, der gælder uden !important */

Det er ikke et lune i specifikationen – det er en konsekvent udvidelse af en regel, der allerede fandtes for origins (brugerens !important-ark har altid slået siteforfatterens !important, modsat hvad der gælder for almindelige deklarationer). Den praktiske konklusion: !important inde i et lagsystem kan overraske selv nogen, der forstår den almindelige @layer-rækkefølge godt – endnu en god grund til at behandle !important som noget, du griber til bevidst og sjældent, ikke som en standardmetode til at vinde et opgør.

Faldgruber og god praksis

  • Ingen regler uden for lagene, hvis systemet skal være forudsigeligt. Som vist ovenfor slår selv én enkelt regel uden @layer hele lagsystemet for almindelige deklarationer – konsekvens vejer tungere end bekvemmeligheden ved at tilføje "bare én lille regel" i farten.
  • Specificitet gælder stadig inden for et enkelt lag. @layer eliminerer specificitetskrige mellem lag, men inden i ét lag afgør to regler stadig konflikten som altid – fornuftig klassenavngivning og undgåelse af dybt indlejrede selektorer betaler sig stadig.
  • Erklær lagrækkefølgen eksplicit, helt øverst, med en tom @layer navn1, navn2, ...;. Det gør dig uafhængig af rækkefølgen af filimports – prioritetsrækkefølgen fastlægges én gang, overskueligt, ét sted, uanset i hvilken rækkefølge de enkelte CSS-filer rent faktisk senere indlæses.
  • Browserunderstøttelse er ikke længere en grund til forsigtighed. Cascade Layers opnåede Baseline-status (bredt tilgængelig) allerede i 2022 – Chrome 99, Firefox 97, Safari 15.4 – så de kan indføres uden @supports i ethvert nyt projekt.

Opsummering

@layer konkurrerer ikke med specificitet – den virker på et helt andet trin i cascade-algoritmen, tidligere, og gør det derved muligt for én hjælpeklasse bevidst og forudsigeligt at slå en selektor med tre indlejrede klasser, uden at hæve prioriteten og uden !important. Det er et reelt arkitektonisk værktøj til at opdele reset, basisstile, komponenter og hjælpeklasser i lag med eksplicit erklæret vigtighedsrækkefølge – men to af dens regler bryder intuitionen så kraftigt, at de er værd at huske hver for sig: en stil uden for et lag slår ethvert lag, og !important inde i lag vender deres prioritetsrækkefølge om. At ignorere en af de to kendsgerninger ender ikke med en fejl i konsollen – det ender med CSS, der opfører sig anderledes, end den tilsyneladende logiske lagrækkefølge i filen skulle tilsige.