Modern CSS 2026: Funktionerna som förändrade hur jag skriver stilar
Container queries, CSS-lager, :has(), color-mix(), nesting, scrolldrivna animationer och anchor positioning. CSS-funktionerna som fick mig att sluta ta till JavaScript.
Jag raderade min sista Sass-fil för sex månader sedan. Inte för att jag ville göra ett uttalande. Utan för att jag genuint inte behövde den längre.
I över ett decennium var CSS det språk vi bad om ursäkt för. Vi behövde preprocessorer för nesting och variabler. Vi behövde JavaScript för containerbaserad dimensionering, scrolllänkade animationer, föräldraselektion och hälften av de layoutmönster designers gav oss. Vi byggde hela runtimesystem — CSS-in-JS-bibliotek, utility-ramverk, PostCSS-pluginkedjor — för att kompensera för vad språket inte kunde göra nativt.
Den eran är över. Inte "nästan över." Inte "på väg." Över.
Funktionerna som landade i webbläsare mellan 2024 och 2026 lade inte bara till bekvämlighet. De förändrade den mentala modellen. CSS är inte längre ett stilspråk du kämpar mot. Det är ett stilspråk som faktiskt tänker på komponenter, specificitetshantering, responsiv design på elementnivå och animation utan en runtime.
Här är vad som förändrades, varför det spelar roll och vad du kan sluta göra tack vare det.
Container Queries: Slutet på "Hur bred är viewporten?"#
Det här är den som fundamentalt förändrade hur jag tänker på responsiv design. I tjugo år hade vi media queries. Media queries frågar: "hur bred är viewporten?" Container queries frågar: "hur bred är containern som denna komponent lever i?"
Den distinktionen låter subtil. Det är den inte. Det är skillnaden mellan komponenter som bara fungerar i en layoutkontext och komponenter som fungerar överallt.
Problemet som funnits i två decennier#
Tänk dig en kortkomponent. I en sidopanel ska den staplas vertikalt med en liten bild. I ett huvudinnehållsområde ska den bli horisontell med en större bild. I en fullbreddshjältesektion ska den vara något helt annat.
Med media queries skulle du skriva något liknande:
/* Det gamla sättet: koppla komponentstiler till sidlayout */
.card {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.sidebar .card {
/* Fortfarande vertikal i sidopanelen */
}
.main-content .card {
flex-direction: row;
/* Horisontell i huvudområdet */
}
}
@media (min-width: 1200px) {
.hero .card {
/* Ännu en layout */
}
}Kortet känner till .sidebar, .main-content och .hero. Det känner till sidan. Det är inte en komponent längre — det är ett sidmedvetet fragment. Flytta det till en annan sida och allt går sönder.
Container Queries löser detta helt#
/* Container query-sättet: komponenten vet bara om sig själv */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@container card (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
}
}
@container card (min-width: 700px) {
.card {
grid-template-columns: 300px 1fr;
gap: 2rem;
font-size: 1.125rem;
}
}Kortet vet inte var det bor. Det bryr sig inte. Placera det i en 300px sidopanel och det är vertikalt. Placera det i ett 700px huvudområde och det är horisontellt. Släpp det i en fullbreddssektion och det anpassar sig. Noll kunskap om sidlayouten krävs.
inline-size vs size#
Du vill nästan alltid använda container-type: inline-size. Detta möjliggör frågor på inline-axeln (bredd i horisontella skrivlägen). Att använda container-type: size möjliggör frågor på både inline- och blockaxeln, men det kräver att containern har explicit dimensionering i båda dimensionerna, vilket bryter normalt dokumentflöde i de flesta fall.
/* Detta är vad du vill 99% av gångerna */
.wrapper {
container-type: inline-size;
}
/* Detta kräver explicit höjd — sällan vad du vill */
.wrapper-both {
container-type: size;
height: 500px; /* krävs, annars kollapsar den */
}Namngivna containrar för nästlade kontexter#
När du nästlar containrar blir namngivning väsentlig:
.page-layout {
container-type: inline-size;
container-name: layout;
}
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
/* Sikta på sidopanelen specifikt, inte närmaste förfader */
@container sidebar (max-width: 250px) {
.nav-item {
font-size: 0.875rem;
padding: 0.25rem;
}
}
/* Sikta på sidlayouten */
@container layout (min-width: 1200px) {
.page-header {
font-size: 2.5rem;
}
}Utan namn frågar @container den närmaste förfadern med containment. Med nästling är det ofta inte den container du vill ha. Namnge dem. Alltid.
Container Query-enheter#
Den här är underskattad. Container query-enheter (cqi, cqb, cqw, cqh) låter dig dimensionera saker relativt till containern, inte viewporten:
@container (min-width: 400px) {
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
}4cqi är 4% av containerns inline-storlek. Titeln skalas med containern, inte fönstret. Detta är vad flytande typografi borde ha varit från början.
CSS-lager: Specificitetskriget är över#
Om container queries förändrade hur jag tänker på responsiv design, så förändrade @layer hur jag tänker på CSS-arkitektur. För första gången har vi ett vettigt, deklarativt sätt att hantera specificitet över ett helt projekt.
Problemet#
CSS-specificitet är ett poängsystem som inte bryr sig om dina intentioner. En utility-klass med .text-red förlorar mot .card .title eftersom den senare har högre specificitet. Lösningen var alltid densamma: gör dina selektorer mer specifika, lägg till !important eller strukturera om allt.
Vi byggde hela metodologier (BEM, SMACSS, ITCSS) och verktygskedjor bara för att undvika specificitetskonflikter. Allt det var en kringgång av en saknad språkfunktion.
Lagerordning#
@layer låter dig deklarera ordningen i vilken grupper av stilar beaktas, oavsett specificitet inom dessa grupper:
/* Deklarera lagerordning — denna enda rad kontrollerar allt */
@layer reset, base, components, utilities, overrides;
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: oklch(20% 0 0);
}
h1, h2, h3 {
line-height: 1.2;
text-wrap: balance;
}
}
@layer components {
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px oklch(0% 0 0 / 0.12);
}
.card .title {
font-size: 1.25rem;
font-weight: 600;
color: oklch(25% 0.05 260);
}
}
@layer utilities {
.text-red {
color: oklch(55% 0.25 25);
}
.hidden {
display: none;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
}Även om .card .title har högre specificitet än .text-red vinner utilityn eftersom utilities-lagret deklareras efter components. Inget !important. Inga specificitetshack. Lagerordningen är det sista ordet.
Hur Tailwind CSS v4 använder lager#
Tailwind v4 lutar sig tungt mot @layer. När du skriver @import "tailwindcss" får du:
@layer theme, base, components, utilities;Varje Tailwind-utility lever i utilities-lagret. Dina anpassade komponentstilar hamnar i components. Det är därför en text-red-500-klass kan åsidosätta en komponents färg utan att behöva !important — den är i ett senare lager.
Om du bygger ditt eget designsystem utan Tailwind, stjäl denna arkitektur. Den är rätt:
@layer reset, tokens, base, layouts, components, utilities, overrides;Sju lager räcker gott. Jag har aldrig behövt fler.
Olagerlagda stilar slår allt#
En hake: stilar som inte är i något lager har högst prioritet. Detta är faktiskt användbart — det innebär att dina engångsöverrides för specifika sidor automatiskt vinner:
@layer components {
.modal {
background: white;
}
}
/* Inte i något lager — vinner över allt i lager */
.special-page .modal {
background: oklch(95% 0.02 260);
}Men det innebär också att tredjeparts-CSS som inte är lagermedveten kan åsidosätta hela ditt system. Wrappa tredjepartsstilar i ett lager för att kontrollera dem:
@layer third-party {
@import url("some-library.css");
}:has()-selektorn: Förälderselektorn vi alltid velat ha#
I bokstavligen decennier bad utvecklare om en förälderselektorn. "Jag vill stila en förälder baserat på dess barn." Svaret var alltid "CSS kan inte göra det" följt av en JavaScript-kringgång. :has() förändrar detta helt, och det visar sig att den är ännu kraftfullare än det vi bad om.
Grundläggande föräldraselektion#
/* Stila en formulärgrupp när dess input har fokus */
.form-group:has(input:focus) {
border-color: oklch(55% 0.2 260);
box-shadow: 0 0 0 3px oklch(55% 0.2 260 / 0.15);
}
/* Stila ett kort annorlunda när det innehåller en bild */
.card:has(img) {
padding-top: 0;
}
.card:has(img) .card-content {
padding: 1.5rem;
}
/* Ett kort utan bild får annan behandling */
.card:not(:has(img)) {
border-left: 4px solid oklch(55% 0.2 260);
}Formulärvalideringsstatus utan JavaScript#
Det är här :has() blir genuint spännande. Kombinerat med HTML-valideringspseudoklasser kan du bygga formulärgränssnitt som reagerar på giltighetsstatus med noll JavaScript:
/* Fältwrappern reagerar på sin inputs giltighet */
.field:has(input:invalid:not(:placeholder-shown)) {
--field-color: oklch(55% 0.25 25);
}
.field:has(input:valid:not(:placeholder-shown)) {
--field-color: oklch(55% 0.2 145);
}
.field {
--field-color: oklch(70% 0 0);
border: 2px solid var(--field-color);
border-radius: 0.5rem;
padding: 0.75rem;
transition: border-color 0.2s;
}
.field label {
color: var(--field-color);
font-size: 0.875rem;
font-weight: 500;
}
.field .error-message {
display: none;
color: oklch(55% 0.25 25);
font-size: 0.8rem;
margin-top: 0.25rem;
}
.field:has(input:invalid:not(:placeholder-shown)) .error-message {
display: block;
}Delen med :not(:placeholder-shown) är avgörande — den förhindrar att valideringsstilar visas på tomma fält som inte har berörts ännu.
Layout baserad på antal barn#
Det här mönstret är absurt användbart och var genuint omöjligt före :has():
/* Anpassa gridkolumner baserat på hur många element som finns */
.grid-auto:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid-auto:has(> :nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
.grid-auto:has(> :nth-child(13)) {
grid-template-columns: repeat(4, 1fr);
}Gridet ändrar sitt kolumnantal baserat på hur många barn det har. Inget JavaScript. Ingen ResizeObserver. Ingen klassväxling.
Sidopaneldetektering#
Ett av mina favoritmönster — ändra layouten baserat på om en sidopanel finns:
.page-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
/* Om layouten innehåller en sidopanel, byt till två kolumner */
.page-layout:has(.sidebar) {
grid-template-columns: 1fr 300px;
}
/* Anpassa huvudinnehållets bredd när sidopanel finns */
.page-layout:has(.sidebar) .main-content {
max-width: 65ch;
}Lägg till en sidopanelkomponent i DOM:en, layouten anpassar sig. Ta bort den, den anpassar sig tillbaka. CSS:en är sanningens källa, inte en tillståndsvariabel.
Kombinera :has() med andra selektorer#
:has() komponerar vackert med allt annat:
/* Stila en artikel bara när den har en specifik klass av figur */
article:has(figure.full-bleed) {
overflow: visible;
}
/* En navigation som ändras när den innehåller en sökinput */
nav:has(input[type="search"]:focus) {
background: oklch(98% 0 0);
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.08);
}
/* Aktivera mörkt läge på komponentnivå */
.theme-switch:has(input:checked) ~ main {
color-scheme: dark;
background: oklch(15% 0 0);
color: oklch(90% 0 0);
}Webbläsarstödet är utmärkt nu. Alla moderna webbläsare har stött :has() sedan tidigt 2024. Det finns ingen anledning att inte använda det.
CSS-nesting: Nativt, äntligen stabilt#
Jag tänker inte överdriva den här. CSS-nesting är trevligt. Det är inte revolutionerande på det sätt som container queries eller :has() är. Men det tar bort en av de sista anledningarna att använda Sass, och det spelar roll.
Syntaxen#
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
.title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.description {
color: oklch(40% 0 0);
line-height: 1.7;
}
&:hover {
box-shadow: 0 4px 12px oklch(0% 0 0 / 0.1);
}
&.featured {
border: 2px solid oklch(60% 0.2 260);
}
}Detta är giltig CSS. Inget byggsteg. Ingen preprocessor. Ingen PostCSS-plugin. Webbläsaren hanterar det nativt.
Skillnader från Sass#
Det finns några syntaxskillnader värda att känna till:
/* CSS-nesting — fungerar nu i alla webbläsare */
.parent {
/* Direkt klass/elementnesting fungerar utan & */
.child {
color: red;
}
/* & krävs för pseudoklasser och sammansatta selektorer */
&:hover {
background: blue;
}
&.active {
font-weight: bold;
}
/* Nästlade media queries — detta är bra */
@media (width >= 768px) {
flex-direction: row;
}
}I tidiga implementationer behövde du & före elementselektorer (som & p istället för bara p). Den begränsningen lättades. Sedan 2025 stödjer alla stora webbläsare ren elementnesting: .parent { p { ... } } fungerar fint.
Nästlade media queries#
Det här är nesting-killer-funktionen i CSS, enligt min åsikt. Inte selektornestingen — möjligheten att placera media queries inuti ett regelblock:
.hero {
padding: 2rem;
font-size: 1rem;
@media (width >= 768px) {
padding: 4rem;
font-size: 1.25rem;
}
@media (width >= 1200px) {
padding: 6rem;
font-size: 1.5rem;
}
}Jämför detta med det gamla sättet där dina .hero-stilar var utspridda över tre olika @media-block, möjligen hundratals rader ifrån varandra. Nesting håller det responsiva beteendet samlokaliserat med komponenten. Läsbarheten förbättras dramatiskt.
Nästla inte för djupt#
En varning: bara för att du kan nästla sex nivåer djupt betyder det inte att du bör. Samma råd från Sass gäller här. Om din nesting skapar selektorer som .page .section .card .content .text .highlight har du skapat ett specificitetsmonster och en underhållsmardröm. Två eller tre nivåer är sweet spot.
/* Bra — två nivåer */
.nav {
.link {
color: inherit;
&:hover {
color: oklch(55% 0.2 260);
}
}
}
/* Dåligt — specificitetsmardröm */
.page {
.layout {
.sidebar {
.nav {
.list {
.item {
.link {
color: red; /* lycka till att åsidosätta detta */
}
}
}
}
}
}
}Färgfunktioner: oklch() och color-mix() förändrar allt#
hsl() hade en bra period. Den var mer intuitiv än rgb(). Men den har ett fundamentalt fel: den är inte perceptuellt enhetlig. En hsl(60, 100%, 50%) (gul) ser dramatiskt ljusare ut för det mänskliga ögat än hsl(240, 100%, 50%) (blå), trots att de har samma ljushetsvärde.
Varför oklch() vinner#
oklch() är perceptuellt enhetlig. Lika ljushetsvärden ser lika ljusa ut. Detta spelar enorm roll när man genererar färgpaletter, skapar teman och säkerställer tillgänglig kontrast:
:root {
/* oklch(ljushet kroma nyans) */
--color-primary: oklch(55% 0.2 260); /* blå */
--color-secondary: oklch(55% 0.2 330); /* lila */
--color-success: oklch(55% 0.2 145); /* grön */
--color-danger: oklch(55% 0.25 25); /* röd */
--color-warning: oklch(75% 0.18 85); /* gul — högre L för jämn perception */
/* Alla dessa ser lika "medelstarka" ut för det mänskliga ögat */
/* Med HSL skulle du behöva olika ljushetsvärden för varje nyans */
}De tre värdena är intuitiva när du förstår dem:
- Ljushet (0% till 100%): Hur ljust eller mörkt. 0% är svart, 100% är vitt.
- Kroma (0 till ~0,37): Hur livfullt. 0 är grått, högre är mer mättat.
- Nyans (0 till 360): Färgvinkeln. 0/360 är rosarött, 145 är grönt, 260 är blått.
color-mix() för härledda färger#
color-mix() låter dig skapa färger från andra färger vid körningstid. Ingen Sass darken()-funktion. Inget JavaScript. Bara CSS:
:root {
--brand: oklch(55% 0.2 260);
/* Ljusare genom att blanda med vit */
--brand-light: color-mix(in oklch, var(--brand) 30%, white);
/* Mörkare genom att blanda med svart */
--brand-dark: color-mix(in oklch, var(--brand) 70%, black);
/* Skapa en subtil bakgrund */
--brand-bg: color-mix(in oklch, var(--brand) 8%, white);
/* Halvtransparent version */
--brand-overlay: color-mix(in oklch, var(--brand) 50%, transparent);
}
.button {
background: var(--brand);
color: white;
&:hover {
background: var(--brand-dark);
}
&:active {
background: color-mix(in oklch, var(--brand) 60%, black);
}
}
.button.secondary {
background: var(--brand-bg);
color: var(--brand-dark);
&:hover {
background: color-mix(in oklch, var(--brand) 15%, white);
}
}Delen in oklch spelar roll. Blandning i srgb ger grumliga mellanfärger. Blandning i oklch ger perceptuellt jämna resultat. Blanda alltid i oklch.
Bygga en komplett palett med oklch()#
Så här genererar jag en hel nyanstrappa från en enda nyans:
:root {
--hue: 260;
--chroma: 0.2;
--color-50: oklch(97% calc(var(--chroma) * 0.1) var(--hue));
--color-100: oklch(93% calc(var(--chroma) * 0.2) var(--hue));
--color-200: oklch(85% calc(var(--chroma) * 0.4) var(--hue));
--color-300: oklch(75% calc(var(--chroma) * 0.6) var(--hue));
--color-400: oklch(65% calc(var(--chroma) * 0.8) var(--hue));
--color-500: oklch(55% var(--chroma) var(--hue));
--color-600: oklch(48% var(--chroma) var(--hue));
--color-700: oklch(40% calc(var(--chroma) * 0.9) var(--hue));
--color-800: oklch(32% calc(var(--chroma) * 0.8) var(--hue));
--color-900: oklch(25% calc(var(--chroma) * 0.7) var(--hue));
--color-950: oklch(18% calc(var(--chroma) * 0.5) var(--hue));
}Ändra --hue till 145 och du har en grön palett. Ändra till 25 och du har röd. Ljusstegen är perceptuellt jämna. Kroman avtar vid extremerna så de ljusaste och mörkaste nyanserna inte är övermättade. Det här är den typen av sak som tidigare krävde ett designverktyg eller en Sass-funktion. Nu är det åtta rader CSS.
Scrolldrivna animationer: Inget JavaScript behövs#
Det här är funktionen som fick mig att radera mest JavaScript. Scrolllänkade animationer — förloppsindikatorer, parallaxeffekter, visningsanimationer, klibbiga headers med övergångar — krävde tidigare IntersectionObserver, scroll-eventlyssnare eller ett bibliotek som GSAP. Nu är det CSS.
Scrollförloppsindikator#
Den klassiska "läsförloppsindikatorn" längst upp på en artikelsida:
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: oklch(55% 0.2 260);
transform-origin: left;
z-index: 1000;
animation: grow-progress linear both;
animation-timeline: scroll();
}
@keyframes grow-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}Det var allt. Hela läsförloppsindikatorn. Inget JavaScript. Ingen scroll-eventlyssnare. Ingen requestAnimationFrame. Ingen "procent scrollad"-beräkning. Bindningen animation-timeline: scroll() gör allt.
Visning vid scroll#
Element som tonas in när de kommer in i viewporten:
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}animation-timeline: view() kopplar animationen till elementets synlighet i viewporten. animation-range: entry 0% entry 100% innebär att animationen körs från det ögonblick elementet börjar komma in i viewporten tills det är helt synligt.
Parallax utan bibliotek#
.parallax-bg {
animation: parallax linear both;
animation-timeline: scroll();
}
@keyframes parallax {
from {
transform: translateY(-20%);
}
to {
transform: translateY(20%);
}
}
.parallax-section {
overflow: hidden;
position: relative;
}Bakgrundsbilden rör sig i en annan hastighet än scrollningen, vilket skapar en parallaxeffekt. Smidigt, prestandastarkt (webbläsaren kan komposita på GPU:n) och noll JavaScript.
Namngivna scroll-tidslinjor#
För mer kontroll kan du namnge scroll-tidslinjor och referera dem från andra element:
.scroller {
overflow-y: auto;
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: block;
}
/* Ett element var som helst i DOM:en kan referera denna tidslinje */
.indicator {
animation: progress linear both;
animation-timeline: --my-scroller;
}
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}Detta fungerar även när indikatorn och scrollern inte är förälder/barn. Vilket element som helst kan länka till vilken namngiven scroll-tidslinje som helst. Detta är kraftfullt för dashboard-gränssnitt där en scrollbar panel driver en indikator i en fast header.
Anchor Positioning: Tooltips och popovers rätt gjort#
Före anchor positioning krävde det JavaScript att koppla en tooltip till dess triggerelement. Du behövde beräkna positioner med getBoundingClientRect(), hantera scrolloffsetter, hantera viewportkollisioner och omberäkna vid resize. Bibliotek som Popper.js (nu Floating UI) existerade specifikt för att detta var så svårt att få rätt.
CSS anchor positioning gör detta deklarativt:
.trigger {
anchor-name: --my-trigger;
}
.tooltip {
position: fixed;
position-anchor: --my-trigger;
/* Placera tooltipens topp-center vid triggerns botten-center */
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
/* Reservpositionering om den svämmar över viewporten */
position-try-fallbacks: flip-block, flip-inline;
}Automatisk viewport-kollisionshantering#
Egenskapen position-try-fallbacks är delen som skulle ha krävt 200 rader JavaScript. Den säger till webbläsaren: "om tooltipen svämmar över viewporten nedtill, vänd den uppåt. Om den svämmar över till höger, vänd den till vänster."
.dropdown-menu {
position: fixed;
position-anchor: --menu-button;
/* Standard: under knappen, justerad till vänsterkanten */
top: anchor(bottom);
left: anchor(left);
/* Om den inte får plats under, prova ovan. Om den inte får plats vänsterjusterad, prova högerjusterad */
position-try-fallbacks: flip-block, flip-inline;
/* Lägg till ett mellanrum mellan ankaret och dropdown:en */
margin-top: 4px;
}Namngivna position-fallbacks#
För mer kontroll över reservpositioner kan du definiera anpassade try-alternativ:
@position-try --above-right {
bottom: anchor(top);
right: anchor(right);
top: auto;
left: auto;
margin-top: 0;
margin-bottom: 4px;
}
@position-try --left-center {
right: anchor(left);
top: anchor(center);
left: auto;
bottom: auto;
translate: 0 -50%;
margin-top: 0;
margin-right: 4px;
}
.tooltip {
position: fixed;
position-anchor: --trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
margin-top: 4px;
position-try-fallbacks: --above-right, --left-center;
}Webbläsaren provar varje fallback i ordning tills den hittar en som håller elementet inom viewporten. Det här är den typen av rumsligt resonemang som var genuint smärtsamt i JavaScript.
Ankring med Popover API:et#
Anchor positioning parar perfekt med det nya Popover API:et:
<button popovertarget="my-popover" style="anchor-name: --btn">Inställningar</button>
<div popover id="my-popover" style="
position-anchor: --btn;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
margin-top: 8px;
">
Popover-innehåll här
</div>Inget JavaScript för att visa/dölja (Popover API:et hanterar det). Inget JavaScript för positionering (anchor positioning hanterar det). Inget JavaScript för light-dismiss-beteende (Popover API:et hanterar det också). Hela tooltip/popover/dropdown-mönstret — ett mönster som drev hela npm-paket — är nu HTML och CSS.
CSS Grid Subgrid: Nästlad justering som faktiskt fungerar#
Grid är kraftfullt, men det hade en frustrerande begränsning: ett barngrid kunde inte justera sina element efter föräldergridet. Om du hade en rad med kort och ville att titeln, innehållet och sidfoten på varje kort skulle riktas in över korten var du utan tur. Varje korts interna grid var oberoende.
Subgrid fixar detta.
Problemet med kortjustering#
/* Föräldergrid */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
/* Varje kort blir ett subgrid som justerar rader mot föräldern */
.card {
display: grid;
grid-row: span 3; /* Kortet spänner 3 rader i föräldern */
grid-template-rows: subgrid;
gap: 0; /* Kortet kontrollerar sitt eget interna mellanrum */
}
.card-title {
font-weight: 600;
padding: 1rem 1rem 0.5rem;
}
.card-body {
padding: 0 1rem;
color: oklch(40% 0 0);
}
.card-footer {
padding: 0.5rem 1rem 1rem;
margin-top: auto;
border-top: 1px solid oklch(90% 0 0);
}Nu justeras titlarna över alla tre korten på samma rad. Innehållen justeras. Sidfötterna justeras. Även när ett kort har en tvåradig titel och ett annat har en enradig titel upprätthålls justeringen av föräldergridet.
Före Subgrid: Hacken#
Utan subgrid krävde denna justering antingen:
- Fasta höjder (bräckligt, går sönder med dynamiskt innehåll)
- JavaScript-mätning (långsamt, blixt av feljustering)
- Ge upp och acceptera feljustering (vanligt, fult)
/* Det gamla hacket — bräckligt och går sönder med dynamiskt innehåll */
.card-title {
min-height: 3rem; /* be att ingen titel överstiger detta */
}
.card-body {
min-height: 8rem; /* ännu mer bön */
}Subgrid gör hacket onödigt. Föräldergridet fördelar radhöjder baserat på det högsta innehållet i varje rad över alla kort.
Kolumn-subgrid för formulärlayouter#
Subgrid fungerar på kolumner också, vilket är perfekt för formulärlayouter:
.form {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.75rem 1rem;
}
.form-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
}
.form-row label {
grid-column: 1;
text-align: right;
font-weight: 500;
}
.form-row input {
grid-column: 2;
padding: 0.5rem;
border: 1px solid oklch(80% 0 0);
border-radius: 0.375rem;
}Alla etiketter justeras. Alla inputs justeras. Etikettkolumnen anpassar sig automatiskt till den bredaste etiketten. Inga hårdkodade bredder.
View Transitions API: SPA-liknande navigering utan ramverk#
Den här är den mest ambitiösa funktionen på den här listan. View Transitions API:et låter dig animera mellan sidnavigeringar — inklusive cross-document-navigeringar (vanliga länkklick i en flersidig app). Din statiska HTML-sajt kan nu ha smidiga, animerade sidövergångar.
Cross-document-övergångar#
För att aktivera cross-document view-transitions lägger du till en enda CSS-regel på både den gamla och nya sidan:
@view-transition {
navigation: auto;
}Det var allt. Webbläsaren kommer nu att cross-fada mellan sidor vid navigering. Standardövergången är en smidig opacitetsfade. Inget JavaScript. Inget ramverk. Bara två rader CSS på varje sida.
Anpassa övergången#
Du kan anpassa vilka övergångar som sker genom att namnge specifika element:
/* På båda sidorna */
.page-header {
view-transition-name: header;
}
.main-content {
view-transition-name: content;
}
.hero-image {
view-transition-name: hero;
}
/* Övergångsanimationer */
::view-transition-old(content) {
animation: slide-out 0.3s ease-in both;
}
::view-transition-new(content) {
animation: slide-in 0.3s ease-out both;
}
::view-transition-old(hero) {
animation: fade-out 0.2s ease-in both;
}
::view-transition-new(hero) {
animation: none; /* Den nya hjältebilden bara visas */
}
@keyframes slide-out {
to {
transform: translateX(-100%);
opacity: 0;
}
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}Vid navigering mellan två sidor som båda har en .hero-image med view-transition-name: hero animerar webbläsaren automatiskt bilden från sin position på den gamla sidan till sin position på den nya sidan. Det är mönstret "shared element transition" från mobilutveckling, nu i webbläsaren.
SPA View Transitions#
För single-page-appar (React, Vue, Svelte etc.) är JavaScript-API:et rättframt:
/* CSS-sidan — definiera dina övergångsanimationer */
::view-transition-old(root) {
animation: fade-out 0.2s ease-in;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease-out;
}// JS-sidan — wrappa din DOM-uppdatering i startViewTransition
document.startViewTransition(() => {
// Uppdatera DOM:en här — React-render, innerHTML-byte etc.
updateContent(newPageData);
});Webbläsaren tar en ögonblicksbild av det gamla tillståndet, kör din uppdatering, tar en ögonblicksbild av det nya tillståndet och animerar mellan dem. Namngivna element får individuella övergångar; allt annat får en standardcrossfade.
Respektera användarens preferenser#
Respektera alltid prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}Vad du kan sluta använda#
Dessa funktioner eliminerar tillsammans en hel del verktyg, bibliotek och mönster som vi förlitat oss på i åratal. Här är vad jag tagit bort eller slutat ta till:
Sass/SCSS för nesting och variabler#
CSS har nativ nesting. CSS har custom properties (och har haft det i år). De två huvudanledningarna till att folk tog till Sass finns nu i språket. Om du fortfarande använder Sass bara för $variables och nesting kan du sluta.
/* Före: Sass */
$primary: #3b82f6;
.card {
background: white;
border: 1px solid lighten($primary, 40%);
&:hover {
border-color: $primary;
}
.title {
color: darken($primary, 15%);
}
}
/* Efter: Nativ CSS */
.card {
--primary: oklch(55% 0.2 260);
background: white;
border: 1px solid color-mix(in oklch, var(--primary) 20%, white);
&:hover {
border-color: var(--primary);
}
.title {
color: color-mix(in oklch, var(--primary) 70%, black);
}
}CSS-versionen är kraftfullare — oklch ger perceptuellt enhetlig färgmanipulation, color-mix fungerar vid körningstid (Sass lighten är bara kompileringstid), och custom properties kan ändras dynamiskt från JavaScript eller media queries.
JavaScript för scrollanimationer#
Radera din IntersectionObserver visa-vid-scroll-kod. Radera ditt JavaScript för scrollförloppsindikatorn. Radera din parallaxscrollhanterare. animation-timeline: scroll() och animation-timeline: view() hanterar allt detta med bättre prestanda (compositor-tråden, inte huvudtråden).
// Före: JavaScript du kan radera
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));/* Efter: CSS som ersätter allt ovan */
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}Popper.js / Floating UI för positionering#
Om du använder Floating UI (tidigare Popper.js) bara för grundläggande tooltip/popover-positionering ersätter CSS anchor positioning det. Du förlorar några av de avancerade funktionerna (virtuella element, anpassad middleware), men för 90%-fallet — "placera denna popover nära den knappen och håll den i viewporten" — gör CSS det nativt nu.
margin: auto-centreringsknep#
Det här är inte nytt, men jag ser fortfarande margin: 0 auto överallt. Moderna layoutverktyg gör det onödigt i de flesta fall:
/* Gamla sättet */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
/* Bättre: CSS Grid eller Flexbox */
.page {
display: grid;
place-items: center;
}
/* Eller: container queries gör containern själv responsiv */
.content {
container-type: inline-size;
width: min(100% - 2rem, 1200px);
margin-inline: auto; /* OK, fortfarande auto, men margin-inline är tydligare */
}margin: 0 auto fungerar fortfarande fint. Men om du använder det för vertikal centrering eller komplex justering, ta till flexbox eller grid istället.
Media queries för responsivitet på komponentnivå#
Det här är den stora. Om du skriver @media-queries för att göra en komponent responsiv gör du troligen fel nu. Container queries bör vara ditt standardval för responsivitet på komponentnivå. Reservera @media-queries för beslut på sidnivå: layoutändringar, navigationsmönster, utskriftsstilar.
/* Före: Media query för komponentstil (fel scope) */
@media (min-width: 768px) {
.product-card {
flex-direction: row;
}
}
/* Efter: Container query (rätt scope) */
.product-card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.product-card {
flex-direction: row;
}
}Den större bilden#
Dessa funktioner existerar inte isolerat. De komponerar. Container queries + :has() + nesting + oklch() + lager — använda tillsammans — ger dig en CSS-skapandeupplevelse som hade varit oigenkännlig för fem år sedan:
@layer components {
.card-wrapper {
container-type: inline-size;
}
.card {
--accent: oklch(55% 0.2 260);
display: grid;
gap: 1rem;
padding: 1.5rem;
border-radius: 0.5rem;
background: white;
border: 1px solid oklch(90% 0 0);
&:has(img) {
padding-top: 0;
img {
border-radius: 0.5rem 0.5rem 0 0;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
}
&:has(.badge.new) {
border-color: var(--accent);
}
.title {
font-size: 1.125rem;
font-weight: 600;
color: oklch(20% 0 0);
text-wrap: balance;
}
.meta {
font-size: 0.875rem;
color: oklch(50% 0 0);
}
@container (min-width: 500px) {
grid-template-columns: 200px 1fr;
&:has(img) {
padding: 0;
img {
border-radius: 0.5rem 0 0 0.5rem;
height: 100%;
aspect-ratio: auto;
}
}
.content {
padding: 1.5rem;
}
}
}
}Det enda blocket hanterar:
- Responsiv layout på komponentnivå (container queries)
- Villkorlig stilsättning baserad på innehåll (
:has()) - Ren selektororganisering (nesting)
- Perceptuellt enhetliga färger (
oklch) - Förutsägbar specificitetshantering (
@layer)
Ingen preprocessor. Ingen CSS-in-JS-runtime. Inga utility-klasser för detta. Bara CSS, som gör vad CSS borde ha gjort hela tiden.
Plattformen hann ikapp. Äntligen. Och det var värt väntan.