استخدام CSS Houdini: إنشاء تأثيرات مخصصة غير متوفرة في CSS العادي
استخدام CSS Houdini: إنشاء تأثيرات مخصصة غير متوفرة في CSS العادي
جرّب أن تفعل شيئاً يبدو بسيطاً: تحريك زاوية في conic-gradient() بسلاسة عند hover. تُعرّف custom property باسم --kat: 0deg، وتضيف transition: --kat 0.4s، وتُمرّر الماوس فوق العنصر - ولا يحدث شيء. يتغيّر التدرج اللوني بقفزة مفاجئة، وكأن transition غير موجود أصلاً. هذا ليس خطأً في كودك. بالنسبة للمتصفح، --kat مجرد نص (string) غير شفاف - لا يعرف أنها زاوية، لذا لا توجد طريقة لتنفيذ interpolation بينها بين 0deg و180deg. يستطيع المتصفح تنفيذ interpolation للأرقام والألوان والأطوال - لأنه يعرف أنواعها. أما custom properties، على عكس خصائص CSS المدمجة، فليس لها أي نوع (type). وفي هذه الفجوة بالتحديد يأتي دور CSS Houdini.
ما هو Houdini فعلياً
هذا سوء فهم شائع: Houdini ليست تقنية واحدة، ولا حتى مواصفة (specification) واحدة. إنها مظلة تجمع عدة مقترحات مستقلة في W3C، تربط بينها فكرة واحدة - كشف أجزاء من محرك العرض (parsing القيم، layout، الرسم (painting)، الحركة) كانت من قبل مخفية تماماً عن المطور. فبدلاً من انتظار ظهور خاصية جاهزة في CSS تقوم بالضبط بما تحتاجه، تحصل على hook منخفض المستوى (low-level) في مرحلة محددة من هذه العملية.
تُنفَّذ هذه الـ hooks على شكل worklets - وحدات JavaScript صغيرة، قريبة من الناحية المفاهيمية من Web Workers، لكنها مصمَّمة خصيصاً لتناسب pipeline العرض (rendering). يعمل الـ worklet خارج الخيط الرئيسي (main thread)، وليس لديه وصول إلى window أو document أو DOM، ويتواصل فقط عبر عقد (contract) محدد بدقة (مثل paint(ctx, size, properties)). هذا ليس قيداً ناتجاً عن كسل واضعي المواصفة - بل قرار معماري واعٍ. بفضل هذا العزل، يستطيع المحرك تشغيل الـ worklets بالتوازي، على خيوط (threads) منفصلة، وتخزين النتيجة مؤقتاً (cache) دون خطر أن يلمس كودك شيئاً لا ينبغي له لمسه. هذا العزل نفسه هو سبب عدم وجود Date أو performance.now() أو fetch داخل الـ worklet - فتقييد الوصول إلى التوقيت الدقيق والشبكة هو حماية واعية من هجمات من نوع timing/fingerprinting انطلاقاً من كود يُعرِض الصفحة.
أجزاء هذه المظلة المختلفة تقف اليوم في مراحل متباينة تماماً: جزء منها أصبح منذ فترة طويلة جزءاً من Baseline، وآخر يعمل فقط في Chrome، وثالث لا يزال بعد عقد كامل مجرد تجربة. هذا التمييز جوهري - ومعظم المقالات التعريفية عن Houdini تتجاهله، وتتعامل مع الكل وكأنه تقنية واحدة جاهزة للاستخدام.
الجزء الوحيد من Houdini الذي يستحق فعلاً استخدامه اليوم: @property
يحل CSS Properties and Values API بالضبط المشكلة التي فتحنا بها هذا المقال. تُسجِّل custom property بنوع (type) محدد (syntax)، وقيمة ابتدائية، ومعلومة عمّا إذا كانت ستُورَّث (inherited) أم لا - ومنذ تلك اللحظة يتعامل المتصفح معها كقيمة كاملة الصلاحية وقابلة للتحريك (animatable)، وليس كنص (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 زاوية، لذا يستطيع تنفيذ interpolation لها بسلاسة؛ أما inherits: false فيمنع التوريث (inheritance) العرضي من قِبل العناصر الفرعية، وهو أمر كثيراً ما يكون مصدراً لأخطاء يصعب تتبعها مع 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 مسؤول عن الاستماع للأحداث (event listening).
في الأسفل نموذج لخطوط تحذيرية مائلة، تُتحكَّم زاويتها بواسطة نفس الخاصية --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، عنصر آخر من نفس عائلة الـ APIs. فبدلاً من تحليل (parsing) النص يدوياً (parseFloat، وقطع "deg")، تحصل على قيمة مُصنَّفة (typed) من نوع CSSUnitValue، تُحوِّلها بشكل صريح إلى الوحدة التي تحتاجها. وبما أن زاوية الخطوط المرسومة تعتمد فقط على custom property المُعلَنة في inputProperties، فإن المحرك يعرف بالضبط متى يجب إعادة رسم العنصر - تغيير خاصية CSS غير مرتبطة في مكان آخر من الصفحة لن يُشغِّل paint() من جديد.
Layout API وAnimation Worklet: واقع عام 2026
هنا يبدأ الجزء الذي تصمت عنه معظم المقالات التعريفية عن Houdini. CSS Layout API - الركيزة الثالثة لنفس المواصفة، والتي تتيح تعريف خوارزمية تخطيط (layout) خاصة بك (ما يعادل display: grid مخصصاً) - موجودة في المشروع منذ عام 2015 ولا تزال، بعد عقد كامل، تجريبية. لم يُطبِّقها أي متصفح مستقر بشكل أصلي (native)؛ فهي متاحة فقط عبر flags أو ضمن origin trials في Chrome. إن صادفت مقالاً يعرض display: layout(name) كحل جاهز للاستخدام، تحقّق من تاريخ نشره - فمن المرجَّح أنه عرض توضيحي (demo) من origin trial يعود لسنوات مضت، وليس شيئاً ستُشغِّله اليوم في متصفح المستخدم.
كان من المفترض أن يمنح Animation Worklet تحكماً كاملاً في الحركات المدفوعة بالتمرير (scroll)، والمُنفَّذة خارج الخيط الرئيسي. عملياً، لا يدعمه سوى Chromium. والأهم من ذلك - أن حالة الاستخدام الرئيسية له لم تعد سبباً كافياً للجوء إلى Houdini، لأن CSS حصل على حل أصلي (native) لنفس المشكلة: يتيح لك animation-timeline: 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إن كان التأثير يجب أن يعمل في كل مكان. - لا تُخطِّط لميزة إنتاجية (production) تعتمد على Layout API - فهي لا تزال تجربة دون دعم فعلي من المتصفحات، جيدة للتجريب لا لخارطة الطريق (roadmap).
- تذكّر أن
paint()ليست CSS "مجانياً" - إنها كود JavaScript خاص بك، يُعاد تشغيله عند كل تغيير فيinputPropertiesالمُعلَنة. الحسابات المكلفة (توليد الضوضاء (noise)، الأنماط الإجرائية (procedural) المعقدة) يجب تحسينها تماماً كما تُحسِّن حلقة رسم (render loop) على<canvas>- فمن السهل هنا أن تبني عن غير قصد حركة تُعيد رسم العنصر بالكامل إطاراً تلو إطار على الخيط الرئيسي لـ layout.
الخلاصة
Houdini ليست قراراً واحداً بـ "استخدمها أو لا" - بل ثلاثة أو أربعة قرارات منفصلة بمستويات مخاطرة مختلفة. أولاً، تحل @property مشكلة حقيقية وملموسة (تحريك custom properties)، وهي اليوم آمنة للاستخدام تماماً مثل أي خاصية CSS أخرى - وهي الجزء الوحيد من هذا النظام (ecosystem) الذي يستحق التطبيق دون تردد. ثانياً، يمنحك Paint API قوة حقيقية (worklet يُعيد رسم نفسه فقط عند تغيّر الاعتماديات (dependencies) المُعلَنة، دون استماع يدوي للأحداث)، لكنه يتطلب خطة واعية للمتصفحات التي لا تدعمه - فهو أداة للتحسين التدريجي، لا للميزات الحرجة. ثالثاً، يُعدّ Layout API وAnimation Worklet مثالاً جيداً على أن وجود المواصفة وحده لا يضمن التبنّي (adoption) - فبعد سنوات، لا يزال أحدهما ينتظر التطبيق، بينما استُبدِل الآخر جزئياً بحل CSS أصلي وأبسط. قبل أن تلجأ إلى Houdini، تحقّق في أي من هذه السلال الثلاث يقع بالضبط الجزء الذي تحتاجه منه.