שימוש ב-CSS Houdini: יצירת אפקטים מותאמים אישית שאינם זמינים ב-CSS רגיל
שימוש ב-CSS Houdini: יצירת אפקטים מותאמים אישית שאינם זמינים ב-CSS רגיל
נסו לעשות משהו שנראה פשוט לכאורה: להנפיש בצורה חלקה זווית בתוך conic-gradient() בעת hover. אתם מגדירים custom property בשם --kat: 0deg, מוסיפים transition: --kat 0.4s, מעבירים את העכבר מעל האלמנט – ושום דבר לא קורה. הגרדיאנט משתנה בקפיצה, כאילו ה-transition בכלל לא קיים. זו לא שגיאה בקוד שלכם. מבחינת הדפדפן, --kat הוא סתם string אטום – הוא לא יודע שמדובר בזווית, ולכן אין לו דרך לבצע אינטרפולציה בינה לבין 0deg ו-180deg. הדפדפן יודע לבצע אינטרפולציה על מספרים, צבעים, אורכים – כי הוא מכיר את הסוגים (types) שלהם. ל-custom properties, בניגוד לתכונות CSS מובנות, אין שום סוג. בדיוק בפער הזה ממוקם CSS Houdini.
מה Houdini באמת הוא
זו אי-הבנה נפוצה: Houdini הוא לא טכנולוגיה אחת ואפילו לא מפרט אחד. זהו מטרייה שמאגדת כמה הצעות עצמאיות ב-W3C, המחוברות על ידי רעיון אחד – לחשוף חלקים ממנוע הרינדור (parsing של ערכים, layout, ציור, אנימציה) שקודם לכן היו מוסתרים לחלוטין מהמפתח. במקום לחכות שב-CSS תופיע תכונה מוכנה שעושה בדיוק את מה שאתם צריכים, אתם מקבלים hook ברמה נמוכה לשלב ספציפי בתהליך הזה.
ה-hooks האלה ממומשים כworklets – מודולי JavaScript קטנים, קרובים מבחינה קונספטואלית ל-Web Workers, אך מתוכננים במיוחד עבור צינור הרינדור (rendering pipeline). worklet פועל מחוץ ל-thread הראשי, אין לו גישה ל-window, ל-document או ל-DOM, והוא מתקשר אך ורק דרך חוזה מוגדר בקפדנות (למשל paint(ctx, size, properties)). זו לא מגבלה שנובעת מעצלנות של יוצרי המפרט – זו החלטה ארכיטקטונית מודעת. הודות לבידוד הזה, המנוע יכול להריץ worklets במקביל, ב-threads נפרדים, ולבצע cache לתוצאה בלי סיכון שהקוד שלכם יגע במשהו שאסור לו לגעת בו. אותו בידוד הוא הסיבה לכך שבתוך worklet לא תמצאו Date, performance.now() או fetch – הגבלת הגישה לזמן מדויק ולרשת היא הגנה מודעת מפני התקפות מסוג timing/fingerprinting מרמת הקוד שמרנדר את הדף.
החלקים השונים של המטרייה הזו נמצאים היום בשלבים שונים לגמרי: אחד הוא כבר מזמן חלק מ-Baseline, אחר עובד רק ב-Chrome, ועוד אחד, אחרי עשור שלם, עדיין בגדר ניסוי. ההבחנה הזו קריטית – ורוב המאמרים המבואיים על Houdini מתעלמים ממנה, ומתייחסים לכל המכלול כאל טכנולוגיה אחת מוכנה לשימוש.
ה-Houdini היחיד שבאמת כדאי להשתמש בו היום: @property
CSS Properties and Values API פותר בדיוק את הבעיה שממנה נפתח המאמר הזה. אתם רושמים (register) custom property עם סוג (type) מוגדר (syntax), ערך התחלתי, ומידע האם היא צריכה לעבור בירושה (inherit) – ומאותו רגע הדפדפן מתייחס אליה כאל ערך לגיטימי לחלוטין, שניתן להנפיש, ולא כאל 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;}
הדוגמה הזו עושה בפועל משהו שאי אפשר להשיג ב-CSS רגיל – בלי @property, ה-transition שלמעלה פשוט לא היה עובד, בדיוק כמו בתרחיש שבתחילת המאמר. syntax: '<angle>' אומר לדפדפן ש---kat היא זווית, ולכן הוא יכול לבצע עליה אינטרפולציה חלקה; inherits: false מונע ירושה בלתי מכוונת על ידי elements צאצאים, מה שאצל custom properties נוטה להיות מקור לבאגים שקשה לאתר.
אפשר להשיג את אותה תוצאה מרמת JavaScript, מה שהגיוני כאשר אתם רושמים תכונות באופן דינמי (למשל, מיוצרות על סמך נתונים) במקום בצורה קבועה בגיליון הסגנונות:
javascript
if ('registerProperty' in CSS) {CSS.registerProperty({name: '--kat',syntax: '<angle>',inherits: false,initialValue: '0deg',});}
זהו החלק היחיד ב-Houdini שיש לו היום סטטוס Baseline – הוא עובד באופן native ב-Chrome, ב-Firefox (מגרסה 128) וב-Safari (מגרסה 16.4), ללא polyfills וללא @supports. אם מכל ה-Houdini הייתם צריכים לזכור וליישם דבר אחד בלבד, זה בדיוק זה.
CSS Paint API: worklet שבאמת מצייר
Paint API מאפשר להחליף את background בפונקציה paint(), שמציירת ישירות על האלמנט, תוך שימוש בממשק מצומצם שדומה ל-Canvas 2D (PaintRenderingContext2D) – ללא fillText, drawImage או קריאת פיקסלים. מה שמבדיל את הגישה הזו מציור רגיל על <canvas> הוא inputProperties: ה-worklet מצהיר אילו custom properties הוא רוצה לעקוב אחריהן, ומצייר את עצמו מחדש באופן אוטומטי רק כאשר אחת מהן משתנה – בלי אפילו שורת JavaScript אחת שאחראית על האזנה לאירועים.
להלן דוגמה לתבנית פסים אלכסוניים מסוג פס אזהרה, שהזווית שלהם נשלטת על ידי אותה תכונה שנרשמה קודם, --kat (וליתר דיוק, הגרסה המקומית שלה --stripe-angle) – מה שמדגים כיצד @property ו-Paint API משלימים זה את זה באופן טבעי.
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);
כדאי לשים לב לשורה properties.get('--stripe-angle').to('deg').value – זהו CSS Typed OM, אלמנט נוסף מאותה משפחת API. במקום לפרסר string באופן ידני (parseFloat, חיתוך "deg"), אתם מקבלים ערך מוקלד (typed) מסוג CSSUnitValue, שאותו אתם ממירים באופן מפורש ליחידה שאתם צריכים. הודות לכך שהזווית של הפסים המצוירים תלויה אך ורק ב-custom property שהוצהרה ב-inputProperties, המנוע יודע בדיוק מתי צריך לצייר מחדש את האלמנט – שינוי של תכונת CSS לא קשורה במקום אחר בדף לא יפעיל מחדש את paint().
Layout API ו-Animation Worklet: המציאות של 2026
כאן מתחיל החלק שרוב המאמרים המבואיים על Houdini שותקים לגביו. CSS Layout API – העמוד השלישי של אותו מפרט, המאפשר להגדיר אלגוריתם layout משלכם (מקבילה ל-display: grid פרטי משלכם) – קיים כטיוטה (draft) מאז 2015, ואחרי עשור עדיין ניסיוני. אף דפדפן יציב לא יישם אותו באופן native; הוא זמין רק מאחורי flags או ב-origin trials של Chrome. אם תיתקלו במאמר שמציג display: layout(name) כפתרון מוכן לשימוש, בדקו את תאריך הפרסום – מדובר ככל הנראה בהדגמה מ-origin trial מלפני שנים, לא במשהו שתריצו היום בדפדפן של המשתמש.
Animation Worklet היה אמור לתת שליטה מלאה על אנימציות המונעות על ידי scroll, המבוצעות מחוץ ל-thread הראשי. בפועל, רק Chromium תומך בו. וחשוב מכך – שימוש המפתח (use case) העיקרי שלו הפסיק להיות סיבה לפנות ל-Houdini, כי CSS קיבל פתרון native לאותה בעיה בדיוק: animation-timeline: scroll() מאפשר היום לקשור אנימציה ל-scroll בלי אפילו שורת JavaScript אחת ובלי worklets, עם תמיכה הולכת וגדלה בדפדפנים. זו דוגמה טובה לדפוס רחב יותר: חלק מהסיבות שבגללן Houdini נוצר מלכתחילה הגיעו עם הזמן ישירות למפרט ה-CSS, במקום להישאר API ברמה נמוכה עבור מפתחים.
מתי יש בכך היגיון בפרקטיקה
- השתמשו ב-
@propertyבאומץ, בלי אמצעי הגנה – יש לו סטטוס Baseline, הוא לא צריך@supportsאו polyfill. - התייחסו ל-Paint API כאל שיפור פרוגרסיבי (progressive enhancement) – עטפו אותו ב-
@supports(background: paint(x))עם fallback הגיוני ב-CSS טהור (למשל גרדיאנט רגיל) עבור Firefox, או השתמשו ב-polyfill בשםcss-paint-polyfill, אם האפקט חייב לעבוד בכל מקום. - אל תתכננו פיצ'ר לסביבת ייצור על בסיס Layout API – זהו עדיין ניסוי ללא תמיכה אמיתית בדפדפנים, טוב למשחק, לא ל-roadmap.
- זכרו ש-
paint()הוא לא CSS "בחינם" – זהו קוד JavaScript שלכם, שמופעל מחדש בכל שינוי שלinputPropertiesשהוצהרו. חישובים יקרים (יצירת רעש, תבניות פרוצדורליות מורכבות) צריך לייעל בדיוק כמו לולאת רינדור על<canvas>– קל לבנות כאן בטעות אנימציה שמציירת מחדש את כל האלמנט פריים אחר פריים על ה-thread הראשי של ה-layout.
סיכום
Houdini הוא לא החלטה אחת של "להשתמש או לא" – אלה שלוש, ארבע החלטות נפרדות עם רמות סיכון שונות. ראשית, @property פותר בעיה אמיתית וקונקרטית (הנפשת custom properties) והוא היום בטוח לשימוש בדיוק כמו כל תכונת CSS אחרת – זהו החלק היחיד באקוסיסטם הזה שכדאי ליישם ללא היסוס. שנית, Paint API נותן כוח אמיתי (worklet שמצייר את עצמו מחדש רק כשהתלויות שהוצהרו משתנות, בלי האזנה ידנית לאירועים), אך דורש תוכנית מודעת לדפדפנים ללא תמיכה – זהו כלי לשיפור פרוגרסיבי, לא לפיצ'רים קריטיים. שלישית, Layout API ו-Animation Worklet הם דוגמה טובה לכך שמפרט כשלעצמו לא מבטיח אימוץ (adoption) – אחרי שנים, האחד עדיין מחכה ליישום, והשני הוחלף חלקית על ידי CSS native ופשוט יותר. לפני שאתם פונים ל-Houdini, בדקו באיזה מבין שלושת הסלים האלה נמצא בדיוק החלק שאתם צריכים.