css og layouts

CSS @layer: hvordan cascade layers avslutter krigen om spesifisitet

Du har en kortkomponent fra et designsystem-bibliotek: .card .card__header .card__title { font-size: 18px; } – tre klasser i selektoren, fordi komponentens forfatter nøstet den slik. Du vil overstyre den med én verktøyklasse, .text-lg { font-size: 20px; }, lagt til i HTML-en etter kortets klasser. Det fungerer ikke. Tittelen holder seg stivnakket på 18px, selv om .text-lg står senere i koden, så intuitivt «burde den nyeste vinne». Dette er ikke en nettleserfeil – tre klasser i en selektor slår én klasse, uansett rekkefølge i filen. Slik har spesifisitet fungert siden 1996, og slik er den ment å fungere.

Den klassiske løsningen er å øke spesifisiteten til sin egen regel – .card .text-lg, eller, når det ikke er nok, !important. Begge løsningene fungerer punktvis og bryter sammen systemisk: hver gang noen i teamet legger til en regel med enda høyere spesifisitet (eller enda en !important), blir resten av arket uforutsigbart. Det er nettopp dette problemet – ikke estetisk, men arkitektonisk – CSS Cascade Layers løser.

En ny akse for konfliktløsning, ikke enda en måte å øke prioritet på

Det avgjørende å forstå: @layer er ikke enda et verktøy for å øke en regels prioritet, slik som !important eller å legge til enda en klasse i selektoren. Det er et eget trinn i kaskade-algoritmen, avgjort før spesifisiteten, ikke ved siden av den. Den fullstendige rekkefølgen nettleseren avgjør en konflikt mellom to CSS-regler i, ser (forenklet ved å utelate origin) slik ut:

  1. Lag (@layer) – en regel fra et lag deklarert senere vinner over en regel fra et lag deklarert tidligere, uansett spesifisiteten til selektorene inni.
  2. Spesifisitet – avgjør først når begge reglene er i samme lag (eller ingen av dem er i noe lag i det hele tatt).
  3. Kildeorden – siste dommer, når de to foregående kriteriene ender uavgjort.

Dette gjør at én klasse .text-lg i et senere lag kan slå en selektor med tre klasser i et tidligere lag – fordi sammenstøtet aldri i det hele tatt når frem til trinnet der spesifisiteten sammenlignes. Laget avgjør saken tidligere.

css

/* Lagrekkefølgen erklærer du én gang, på forhånd – den trenger ikke
samsvare med rekkefølgen innholdet deres faktisk vises i filene */
@layer reset, base, components, utilities;
@layer components {
.card .card__header .card__title {
font-size: 18px;
}
}
@layer utilities {
.text-lg {
font-size: 20px;
}
}

Denne tomme deklarasjonen, @layer reset, base, components, utilities;, øverst i filen er ikke en formalitet – det er en eksplisitt fastsettelse av prioritetsrekkefølgen, før noen av lagene får noe reelt innhold. Takket være dette er lagrekkefølgen uavhengig av rekkefølgen på importer eller filer – components kan fysisk befinne seg i koden før utilities, og likevel vinner utilities alltid, fordi den rekkefølgen ble erklært fra starten.

Praktisk eksempel: reset, komponenter og verktøyklasser som aldri kjemper mot hverandre

I praksis er dette mønsteret – reset lavere enn komponenter, komponenter lavere enn verktøyklasser – akkurat slik lagsystemet i Tailwind har fungert siden versjon 3.1: en verktøyklasse skal alltid vinne over en komponentregel, uansett hvor mange klasser komponentens selektor har, fordi det er nettopp derfor verktøyklasser i det hele tatt eksisterer.

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

Legg merke til at !important på .text-lg i laget utilities er unødvendig her – laget sikrer allerede prioriteten, uten at du trenger å gripe til CSS-ens tyngste våpen. Dette illustrerer godt hvorfor cascade layers reelt reduserer antallet !important i koden: du slutter å trenge det som en omvei rundt spesifisitet, fordi du har et verktøy designet nøyaktig for denne oppgaven.

Den samme mekanismen løser et annet, like vanlig problem – et eksternt CSS-bibliotek hvis selektorer du ikke kontrollerer, og hvis prioritet du alltid vil ha lavere enn dine egne stiler, uten å øke spesifisiteten til dine egne regler:

css

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

Hele biblioteket havner i laget vendor, plassert helt først i rekkefølgen – så dine base, components og utilities overstyrer den alltid, uansett hvor aggressivt biblioteket bygger sine selektorer.

Fellen som bryter intuisjonen: stiler uten lag slår alle lag

Dette er den oftest oversette, og samtidig mest konsekvensrike detaljen ved hele mekanismen: en regel som ikke er i noe lag, vinner over en regel i et hvilket som helst lag – for vanlige, ikke-!important-deklarasjoner. Den «taper ikke fordi den er i et usynlig lag på slutten» – den vinner alltid, betingelsesløst, over enhver lag-regel, selv om sistnevnte er i laget erklært som høyeste prioritet.

css

@layer utilities {
.text-lg {
font-size: 20px;
}
}
/* Denne regelen, selv om den ikke er i noe lag og ser ut som en
tilfeldig, enkeltstående override – VINNER over .text-lg ovenfor,
uansett hvor i filen den befinner seg */
.title {
font-size: 16px;
}

Denne oppførselen er bevisst – takket være den er det trygt å gradvis innføre cascade layers i et eksisterende, stort prosjekt: all «gammel» CSS som ennå ikke har havnet i noe lag, beholder som standard høyeste prioritet, så den begynner ikke plutselig å tape mot nylig lagt-til lag. Men den samme oppførselen er kilden til de mest forvirrende feilene i team som innfører @layer delvis: én enkelt regel som ved et uhell blir liggende utenfor et lag – en skrivefeil, en glemt import, en <style>-blokk injisert av et tredjepartsbibliotek – overstyrer hele det nøye designede prioritetssystemet, uten noen advarsel. Praktisk konsekvens: hvis du bestemmer deg for cascade layers, legg konsekvent alt i et eller annet lag, inkludert reset og grunnleggende stiler – ikke la noen regel bli liggende «utenfor», for det er den, ikke laget utilities, som vinner enhver konflikt.

Den andre fellen: !important snur lagrekkefølgen

Hvis du griper til !important inni et lag, snus prioritetsrekkefølgen mellom lagene. For vanlige deklarasjoner vinner laget deklarert senere. For deklarasjoner med !important – vinner laget deklarert tidligere.

css

@layer reset, components;
@layer reset {
.card { padding: 0 !important; }
}
@layer components {
.card { padding: 16px !important; }
}
/* Reset (0px) vinner, fordi ved !important har laget deklarert
tidligere fortrinnsrett – nøyaktig omvendt av regelen
som gjelder uten !important */

Dette er ikke et innfall i spesifikasjonen – det er en konsekvent utvidelse av et prinsipp som allerede fantes for origin (brukerens !important-ark har alltid overstyrt sideforfatterens !important, motsatt av med vanlige deklarasjoner). Praktisk konklusjon: !important inni lagsystemet kan overraske selv noen som forstår den vanlige @layer-rekkefølgen godt – enda en god grunn til å behandle !important som noe du griper til bevisst og sjelden, ikke som standardmåten å vinne et sammenstøt på.

Fallgruver og gode praksiser

  • Null regler utenfor lag, hvis systemet skal være forutsigbart. Som vist ovenfor slår selv én eneste regel uten @layer hele lagsystemet for vanlige deklarasjoner – konsekvens er viktigere enn bekvemmeligheten ved å skrive til «bare én liten regel» i farten.
  • Spesifisitet gjelder fortsatt inni ett enkelt lag. @layer fjerner spesifisitetskriger mellom lag, men inni ett lag avgjør to regler fortsatt en konflikt som alltid – fornuftig klassenavngivning og å unngå dyp nøsting av selektorer lønner seg fortsatt.
  • Erklær lagrekkefølgen eksplisitt, helt øverst, med en tom @layer navn1, navn2, ...;. Dette fjerner avhengigheten av rekkefølgen filene importeres i – prioritetsrekkefølgen er fastsatt én gang, leselig, på ett sted, uansett i hvilken rekkefølge de enkelte CSS-filene faktisk lastes senere.
  • Nettleserstøtte er ikke lenger en grunn til forsiktighet. Cascade Layers oppnådde Baseline-status (bredt tilgjengelig) allerede i 2022 – Chrome 99, Firefox 97, Safari 15.4 – så de kan innføres uten @supports i ethvert nytt prosjekt.

Oppsummering

@layer konkurrerer ikke med spesifisitet – den virker på et helt annet trinn i kaskade-algoritmen, tidligere, og lar dermed én verktøyklasse bevisst og forutsigbart slå en selektor med tre nøstede klasser, uten å øke prioriteten og uten !important. Dette er et reelt arkitektonisk verktøy for å dele reset, grunnleggende stiler, komponenter og verktøyklasser inn i lag med eksplisitt erklært viktighetsrekkefølge – men to av dens regler bryter intuisjonen så kraftig at de er verdt å huske separat: en stil utenfor ethvert lag slår hvert eneste lag, og !important inni lag snur deres prioritetsrekkefølge. Å ignorere noen av disse to faktaene ender ikke med en feil i konsollen – det ender med CSS som oppfører seg annerledes enn den tilsynelatende logiske lagrekkefølgen i filen skulle tilsi.