Używanie CSS Houdini: Twórz niestandardowe efekty niedostępne w zwykłym CSS
Używanie CSS Houdini: Twórz niestandardowe efekty niedostępne w zwykłym CSS
Spróbuj zrobić coś pozornie prostego: płynnie animować kąt w conic-gradient() na hover. Definiujesz custom property --kat: 0deg, dodajesz transition: --kat 0.4s, najeżdżasz myszką – i nic. Gradient zmienia się skokowo, jakby transition w ogóle nie istniał. To nie jest błąd w Twoim kodzie. Dla przeglądarki --kat to zwykły, nieprzezroczysty string – nie wie, że to kąt, więc nie ma jak go interpolować między 0deg a 180deg. Może interpolować liczby, kolory, długości – bo zna ich typy. Custom properties, w przeciwieństwie do wbudowanych właściwości CSS, żadnego typu nie mają. Właśnie w tej luce mieści się CSS Houdini.
Czym naprawdę jest Houdini
To częste nieporozumienie: Houdini nie jest jedną technologią ani nawet jedną specyfikacją. To parasol kilku niezależnych propozycji w W3C, które łączy jedna idea – odsłonić fragmenty silnika renderującego (parsowanie wartości, layout, malowanie, animację), które wcześniej były całkowicie ukryte przed programistą. Zamiast czekać, aż w CSS pojawi się gotowa właściwość robiąca dokładnie to, czego potrzebujesz, dostajesz niskopoziomowy hak w konkretny etap tego procesu.
Te haki są realizowane jako worklety – małe moduły JavaScript, koncepcyjnie bliskie Web Workerom, ale zaprojektowane specjalnie pod pipeline renderowania. Worklet działa poza głównym wątkiem, nie ma dostępu do window, document ani DOM, i komunikuje się wyłącznie przez ściśle zdefiniowany kontrakt (np. paint(ctx, size, properties)). To nie jest ograniczenie z lenistwa twórców specyfikacji – to świadoma decyzja architektoniczna. Dzięki izolacji silnik może uruchamiać worklety równolegle, na osobnych wątkach, i cache'ować wynik bez ryzyka, że Twój kod dotknie czegoś, czego nie powinien. Ta sama izolacja jest powodem, dla którego wewnątrz workleta nie znajdziesz Date, performance.now() ani fetch – ograniczenie dostępu do precyzyjnego czasu i sieci to świadoma ochrona przed atakami typu timing/fingerprinting z poziomu kodu renderującego stronę.
Poszczególne części tego parasola są dziś na zupełnie różnych etapach: jedna jest od dawna częścią Baseline, inna działa tylko w Chromie, a jeszcze inna po dekadzie wciąż jest eksperymentem. To rozróżnienie jest kluczowe – i większość wprowadzających artykułów o Houdini je pomija, traktując całość jako jedną, gotową do użycia technologię.
Jedyny Houdini, którego naprawdę warto używać dziś: @property
CSS Properties and Values API rozwiązuje dokładnie problem z otwarcia tego artykułu. Rejestrujesz custom property z konkretnym typem (syntax), wartością początkową i informacją, czy ma być dziedziczona – i od tej chwili przeglądarka traktuje ją jak pełnoprawną, animowalną wartość, a nie string.
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;}
Ten przykład realnie robi coś, czego zwykły CSS zrobić nie potrafi – bez @property powyższy transition po prostu by nie zadziałał, dokładnie tak jak w scenariuszu na początku artykułu. syntax: '<angle>' mówi przeglądarce, że --kat to kąt, więc może go płynnie interpolować; inherits: false zapobiega przypadkowemu dziedziczeniu przez elementy potomne, co przy custom properties bywa źródłem trudnych do namierzenia błędów.
Ten sam efekt można osiągnąć z poziomu JavaScriptu, co ma sens, gdy rejestrujesz właściwości dynamicznie (np. generowane na podstawie danych) zamiast na sztywno w arkuszu stylów:
javascript
if ('registerProperty' in CSS) {CSS.registerProperty({name: '--kat',syntax: '<angle>',inherits: false,initialValue: '0deg',});}
To jedyna część Houdini, która ma dziś status Baseline – działa natywnie w Chrome, Firefoksie (od wersji 128) i Safari (od 16.4), bez polyfilli i bez @supports. Jeśli z całego Houdini miałbyś zapamiętać i wdrożyć jedną rzecz, to właśnie tę.
CSS Paint API: worklet, który naprawdę maluje
Paint API pozwala zastąpić background funkcją paint(), która rysuje bezpośrednio na elemencie, korzystając z okrojonego interfejsu przypominającego Canvas 2D (PaintRenderingContext2D) – bez fillText, drawImage czy odczytu pikseli. To, co odróżnia to podejście od zwykłego rysowania na <canvas>, to inputProperties: worklet deklaruje, które custom properties chce obserwować, i przerysowuje się automatycznie tylko wtedy, gdy któraś z nich się zmieni – bez ani jednej linii JavaScriptu odpowiedzialnej za nasłuchiwanie zdarzeń.
Poniżej wzór ostrzegawczych, ukośnych pasów, których kąt sterowany jest tą samą, zarejestrowaną wcześniej właściwością --kat (a dokładniej jej lokalną odmianą --stripe-angle) – co pokazuje, jak @property i Paint API naturalnie się uzupełniają.
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.jsif ('paintWorklet' in CSS) {CSS.paintWorklet.addModule('hazard-stripes.js');}
javascript
// hazard-stripes.jsclass 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);
Warto zwrócić uwagę na properties.get('--stripe-angle').to('deg').value – to CSS Typed OM, kolejny element tej samej rodziny API. Zamiast parsować string ręcznie (parseFloat, przycinanie "deg"), dostajesz typowaną wartość CSSUnitValue, którą jawnie konwertujesz do jednostki, jakiej potrzebujesz. Dzięki temu, że kąt narysowanych pasów zależy wyłącznie od zadeklarowanej w inputProperties custom property, silnik wie dokładnie, kiedy trzeba przerysować element – zmiana niepowiązanej właściwości CSS gdzie indziej na stronie nie uruchomi ponownie paint().
Layout API i Animation Worklet: rzeczywistość 2026
Tu zaczyna się część, o której większość wprowadzających artykułów o Houdini milczy. CSS Layout API – trzeci filar tej samej specyfikacji, pozwalający zdefiniować własny algorytm układu (odpowiednik własnego display: grid) – istnieje w projekcie od 2015 roku i po dekadzie wciąż jest eksperymentalny. Żadna stabilna przeglądarka nie wdrożyła go natywnie; dostępny jest wyłącznie za flagami lub w originach testowych Chrome. Jeśli natrafisz na artykuł pokazujący display: layout(nazwa) jako gotowe do użycia rozwiązanie, sprawdź datę publikacji – to prawdopodobnie demo z origin trial sprzed lat, nie coś, co uruchomisz dziś w przeglądarce użytkownika.
Animation Worklet miał dać pełną kontrolę nad animacjami napędzanymi scrollem, wykonywanymi poza głównym wątkiem. Realnie wspiera go tylko Chromium. Co ważniejsze – jego główny use case przestał być powodem do sięgania po Houdini, bo CSS dostał natywne rozwiązanie tego samego problemu: animation-timeline: scroll() pozwala dziś powiązać animację ze scrollem bez jednej linijki JavaScriptu i bez workletów, z rosnącym wsparciem w przeglądarkach. To dobra ilustracja szerszego wzorca: część powodów, dla których Houdini w ogóle powstał, z czasem trafiła wprost do specyfikacji CSS, zamiast zostać niskopoziomowym API dla programistów.
Kiedy to ma sens w praktyce
@propertyużywaj śmiało, bez zabezpieczeń – ma status Baseline, nie potrzebuje@supportsani polyfilla.- Paint API traktuj jako progresywne wzbogacenie – opakuj go w
@supports(background: paint(x))z sensownym fallbackiem w czystym CSS (np. zwykłym gradientem) dla Firefoksa, albo sięgnij po polyfillcss-paint-polyfill, jeśli efekt musi działać wszędzie. - Nie planuj produkcyjnej funkcji na Layout API – to wciąż eksperyment bez realnego wsparcia przeglądarek, dobry do zabawy, nie do roadmapy.
- Pamiętaj, że
paint()to nie „darmowy” CSS – to Twój kod JavaScript, uruchamiany ponownie przy każdej zmianie zadeklarowanejinputProperties. Kosztowne obliczenia (generowanie szumu, złożone wzory proceduralne) trzeba optymalizować dokładnie tak, jak pętlę renderującą na<canvas>– łatwo tu przypadkiem zbudować animację, która przerysowuje cały element klatka po klatce na głównym wątku layoutu.
Podsumowanie
Houdini to nie jedna decyzja „używać czy nie” – to trzy, cztery osobne decyzje o różnym ryzyku. Po pierwsze, @property rozwiązuje realny, konkretny problem (animowanie custom properties) i jest dziś tak samo bezpieczne w użyciu jak każda inna właściwość CSS – to jedyna część tego ekosystemu, którą warto wdrożyć bez wahania. Po drugie, Paint API daje realną moc (worklet przerysowujący się tylko przy zmianie zadeklarowanych zależności, bez ręcznego nasłuchiwania zdarzeń), ale wymaga świadomego planu na przeglądarki bez wsparcia – to narzędzie do progresywnego wzbogacania, nie do funkcji krytycznych. Po trzecie, Layout API i Animation Worklet są dobrym przykładem tego, że specyfikacja sama w sobie nie gwarantuje adopcji – po latach jedna wciąż czeka na wdrożenie, a druga została częściowo zastąpiona przez prostsze, natywne CSS. Zanim sięgniesz po Houdini, sprawdź, w którym z tych trzech koszyków leży dokładnie ta jego część, której potrzebujesz.