css og layoutsanimationer

Brug af CSS Houdini: Opret Tilpassede Effekter Der Ikke Er Tilgængelige i Almindelig CSS

Brug af CSS Houdini: Opret Tilpassede Effekter Der Ikke Er Tilgængelige i Almindelig CSS

Prøv at gøre noget tilsyneladende simpelt: animer en vinkel i conic-gradient() jævnt ved hover. Du definerer en custom property --kat: 0deg, tilføjer transition: --kat 0.4s, fører musen hen over elementet – og der sker ingenting. Gradienten skifter springvis, som om transition slet ikke fandtes. Det er ikke en fejl i din kode. For browseren er --kat blot en ugennemsigtig streng – den ved ikke, at det er en vinkel, og har derfor ingen måde at interpolere den mellem 0deg og 180deg på. Den kan interpolere tal, farver, længder – fordi den kender deres typer. Custom properties har, i modsætning til indbyggede CSS-egenskaber, slet ingen type. Det er præcis i dette hul, at CSS Houdini hører hjemme.

Hvad Houdini egentlig er

Det er en udbredt misforståelse: Houdini er hverken én teknologi eller blot én specifikation. Det er en paraply over flere uafhængige forslag i W3C, som forbindes af én idé – at eksponere fragmenter af rendering-motoren (parsing af værdier, layout, tegning, animation), som tidligere var fuldstændig skjult for udvikleren. I stedet for at vente på, at der dukker en færdig CSS-egenskab op, der gør præcis det, du har brug for, får du en lavniveau-krog ind i et konkret trin i denne proces.

Disse kroge realiseres som worklets – små JavaScript-moduler, konceptuelt beslægtet med Web Workers, men designet specifikt til rendering-pipelinen. En worklet kører uden for hovedtråden, har ikke adgang til window, document eller DOM, og kommunikerer udelukkende gennem en strengt defineret kontrakt (f.eks. paint(ctx, size, properties)). Det er ikke en begrænsning født af dovenskab hos specifikationens forfattere – det er en bevidst arkitektonisk beslutning. Takket være isolationen kan motoren køre worklets parallelt, på separate tråde, og cache resultatet uden risiko for, at din kode rører ved noget, den ikke burde. Den samme isolation er grunden til, at du inde i en worklet ikke finder Date, performance.now() eller fetch – den begrænsede adgang til præcis tid og netværk er en bevidst beskyttelse mod timing- og fingerprinting-angreb fra sidens rendering-kode.

De enkelte dele af denne paraply befinder sig i dag på helt forskellige stadier: én har længe været en del af Baseline, en anden virker kun i Chrome, og endnu en er efter et årti stadig eksperimentel. Den skelnen er afgørende – og de fleste introduktionsartikler om Houdini springer den over og behandler det hele som én færdig, brugsklar teknologi.

Den eneste del af Houdini, det virkelig kan betale sig at bruge i dag: @property

CSS Properties and Values API løser præcis det problem, denne artikel åbnede med. Du registrerer en custom property med en konkret type (syntax), en startværdi og information om, hvorvidt den skal nedarves – og fra det øjeblik behandler browseren den som en fuldgyldig, animerbar værdi i stedet for en streng.

html

<div class="ring"></div>

css

@property --kat {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.ring {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(from var(--kat), #ff4d4d, #4d79ff, #4dff88, #ff4d4d);
transition: --kat 0.6s ease;
}
.ring:hover {
--kat: 180deg;
}

Dette eksempel gør reelt noget, som almindelig CSS ikke kan – uden @property ville ovenstående transition simpelthen ikke virke, præcis som i scenariet i indledningen. syntax: '<angle>' fortæller browseren, at --kat er en vinkel, så den kan interpolere den jævnt; inherits: false forhindrer, at underliggende elementer utilsigtet arver værdien, hvilket ved custom properties ofte er kilde til svært sporbare fejl.

Samme effekt kan opnås fra JavaScript, hvilket giver mening, når du registrerer egenskaber dynamisk (f.eks. genereret ud fra data) i stedet for hårdkodet i stylesheetet:

javascript

if ('registerProperty' in CSS) {
CSS.registerProperty({
name: '--kat',
syntax: '<angle>',
inherits: false,
initialValue: '0deg',
});
}

Dette er den eneste del af Houdini, der i dag har Baseline-status – den virker native i Chrome, Firefox (fra version 128) og Safari (fra 16.4), uden polyfills og uden @supports. Hvis du kun skulle huske og indføre én ting fra hele Houdini, er det netop denne.

CSS Paint API: en worklet, der virkelig tegner

Paint API lader dig erstatte background med funktionen paint(), som tegner direkte på elementet ved hjælp af en beskåret grænseflade, der minder om Canvas 2D (PaintRenderingContext2D) – uden fillText, drawImage eller aflæsning af pixels. Det, der adskiller denne tilgang fra almindelig tegning på et <canvas>, er inputProperties: worklet'en erklærer, hvilke custom properties den vil observere, og gentegner automatisk kun, når en af dem ændrer sig – uden en eneste linje JavaScript dedikeret til at lytte efter events.

Nedenfor ses et mønster af advarende, skrå striber, hvis vinkel styres af den samme, tidligere registrerede egenskab --kat (mere præcist dens lokale variant --stripe-angle) – hvilket viser, hvordan @property og Paint API naturligt supplerer hinanden.

html

<div class="hazard"></div>

css

@property --stripe-angle {
syntax: '<angle>';
inherits: false;
initial-value: 45deg;
}
.hazard {
width: 100%;
height: 120px;
--stripe-angle: 45deg;
background: paint(hazardStripes);
transition: --stripe-angle 0.4s ease;
}
.hazard:hover {
--stripe-angle: 135deg;
}

javascript

// main.js
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('hazard-stripes.js');
}

javascript

// hazard-stripes.js
class HazardStripesPainter {
static get inputProperties() {
return ['--stripe-angle'];
}
paint(ctx, size, properties) {
const angle = properties.get('--stripe-angle').to('deg').value;
const stripeWidth = 24;
const { width, height } = size;
const diagonal = Math.sqrt(width ** 2 + height ** 2) * 2;
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.rotate((angle * Math.PI) / 180);
ctx.translate(-diagonal, -diagonal);
let stripeIndex = 0;
for (let x = 0; x < diagonal * 2; x += stripeWidth) {
ctx.fillStyle = stripeIndex % 2 === 0 ? '#111111' : '#f5c400';
ctx.fillRect(x, 0, stripeWidth, diagonal * 2);
stripeIndex++;
}
ctx.restore();
}
}
registerPaint('hazardStripes', HazardStripesPainter);

Det er værd at lægge mærke til properties.get('--stripe-angle').to('deg').value – det er CSS Typed OM, endnu et element fra samme familie af API'er. I stedet for at parse en streng manuelt (parseFloat, beskæring af "deg"), får du en typet værdi, CSSUnitValue, som du eksplicit konverterer til den enhed, du har brug for. Fordi vinklen på de tegnede striber udelukkende afhænger af den custom property, der er erklæret i inputProperties, ved motoren præcis, hvornår elementet skal gentegnes – en ændring af en urelateret CSS-egenskab et andet sted på siden udløser ikke paint() igen.

Layout API og Animation Worklet: virkeligheden i 2026

Her begynder den del, som de fleste introduktionsartikler om Houdini forbigår i tavshed. CSS Layout API – den tredje søjle i samme specifikation, der gør det muligt at definere sin egen layout-algoritme (svarende til sin egen display: grid) – har eksisteret i udkastet siden 2015 og er efter et årti stadig eksperimentel. Ingen stabil browser har implementeret den native; den er kun tilgængelig bag flags eller i Chromes origin trials. Støder du på en artikel, der viser display: layout(navn) som en brugsklar løsning, så tjek udgivelsesdatoen – det er sandsynligvis en demo fra en origin trial for flere år siden, ikke noget du kan køre i en brugers browser i dag.

Animation Worklet skulle give fuld kontrol over scroll-drevne animationer, der kører uden for hovedtråden. I praksis understøttes den kun af Chromium. Vigtigere er det, at dens primære use case ikke længere er en grund til at ty til Houdini, fordi CSS har fået en native løsning på det samme problem: animation-timeline: scroll() gør det i dag muligt at koble en animation til scroll uden en eneste linje JavaScript og uden worklets, med voksende browserunderstøttelse. Det er en god illustration af et bredere mønster: en del af de grunde, Houdini overhovedet blev skabt for, er med tiden endt direkte i CSS-specifikationen i stedet for at forblive et lavniveau-API for udviklere.

Hvornår giver det mening i praksis

  • Brug @property frit, uden sikkerhedsforanstaltninger – den har Baseline-status og behøver hverken @supports eller polyfill.
  • Betragt Paint API som progressiv forbedring – pak den ind i @supports(background: paint(x)) med et fornuftigt fallback i ren CSS (f.eks. en almindelig gradient) til Firefox, eller brug polyfillen css-paint-polyfill, hvis effekten skal virke overalt.
  • Planlæg ikke en produktionsfunktion omkring Layout API – det er stadig et eksperiment uden reel browserunderstøttelse, godt til leg, ikke til roadmappet.
  • Husk, at paint() ikke er "gratis" CSS – det er din egen JavaScript-kode, der køres igen, hver gang en erklæret inputProperties-værdi ændrer sig. Kostbare beregninger (generering af støj, komplekse proceduremæssige mønstre) skal optimeres på nøjagtig samme måde som en render-loop på et <canvas> – det er let uforvarende at bygge en animation, der gentegner hele elementet billede for billede på hovedtrådens layout.

Konklusion

Houdini er ikke én beslutning om "bruge eller ej" – det er tre, fire separate beslutninger med forskellig risiko. For det første løser @property et reelt, konkret problem (animering af custom properties) og er i dag lige så sikker at bruge som enhver anden CSS-egenskab – det er den eneste del af dette økosystem, det er værd at indføre uden tøven. For det andet giver Paint API reel kraft (en worklet, der kun gentegnes, når de erklærede afhængigheder ændrer sig, uden manuel lytning efter events), men kræver en bevidst plan for browsere uden understøttelse – det er et værktøj til progressiv forbedring, ikke til kritiske funktioner. For det tredje er Layout API og Animation Worklet et godt eksempel på, at en specifikation i sig selv ikke garanterer udbredelse – efter flere år venter den ene stadig på implementering, mens den anden delvist er blevet erstattet af enklere, native CSS. Før du griber til Houdini, bør du undersøge, i hvilken af disse tre kurve den konkrete del, du har brug for, hører hjemme.