{"id":"l-css-4-3","courseId":"css-mastery","lang":"ar","kind":"lesson","summary":"قُد حركات CSS بموضع التمرير باستخدام خطوط زمن scroll() وview()، واختر نطاقات الحركة، وأطلقها كتحسين تدريجي ريثما يكتمل دعم Firefox.","html":"<p>جدول Waypoint طويل: يومان، وأربعة مسارات، وستون جلسة. يريد الفريق لمستين صغيرتين: شريط تقدّم رفيع تحت الرأس يبيّن إلى أين وصلت في الجدول، وبطاقات متحدّثين تنساب إلى مجال الرؤية حين تصل إليها بالتمرير. الطريقة القديمة كانت مستمعًا لحدث <code dir=\"ltr\">scroll</code> يقرأ <code dir=\"ltr\">scrollY</code>، ويجري بعض الحسابات، ويكتب الأنماط، ستين مرة في الثانية، على الـ main thread، متنافسًا مع كل ما عداه.</p>\n<p>الحركات المرتبطة بالتمرير (scroll-driven animations) تترك هذا العمل لـ CSS. تكتب حركة <code dir=\"ltr\">@keyframes</code> عادية وتستبدل ساعتها: بدل الزمن، يأتي تقدّمها من التمرير.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">نوعان من خطوط الزمن</h2>\n<p><strong>خط زمن تقدّم التمرير</strong>، <code dir=\"ltr\">scroll()</code>، يمضي من 0% إلى 100% بينما تُمرَّر حاوية التمرير من الأعلى إلى الأسفل. <code dir=\"ltr\">scroll(root)</code> هي المستند؛ و<code dir=\"ltr\">scroll(nearest)</code>، وهي الافتراضية، أقرب سلف قابل للتمرير.</p>\n<p><strong>خط زمن تقدّم الظهور</strong>، <code dir=\"ltr\">view()</code>، يتتبّع عنصرًا واحدًا وهو يعبر المنطقة المرئية من حاوية التمرير (الـ scrollport): من لحظة ظهور حافته من الأسفل إلى لحظة مغادرته من الأعلى.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">progress.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> grow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  to</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.progress</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  position</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">fixed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> auto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  height</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--track-design</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  transform-origin</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 50</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: grow </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">linear</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> both</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-timeline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scroll</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(root);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>في أعلى الصفحة يكون حجم الشريط صفرًا؛ وفي أسفلها يمتدّ بالعرض الكامل؛ وفي أيّ موضع بينهما يكون متناسبًا تمامًا. و<code dir=\"ltr\">linear</code> مهمة هنا: مع <code dir=\"ltr\">ease</code> الافتراضية سيندفع الشريط في المنتصف ويزحف عند الطرفين. و<code dir=\"ltr\">both</code> تُبقي أول keyframe وآخرها مطبّقين خارج النطاق.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>إعلان خط الزمن قبل الاختصار</span></p>\n<p>الاختصار <code dir=\"ltr\">animation</code> يعيد ضبط كل خصائص الحركة المفصّلة، بما فيها <code dir=\"ltr\">animation-timeline</code>، إلى قيمتها الافتراضية <code dir=\"ltr\">auto</code>. اكتب <code dir=\"ltr\">animation-timeline</code> <em>بعد</em> <code dir=\"ltr\">animation</code>، وإلا تحوّلت الحركة بصمت إلى حركة زمنية بمدة صفر، ولن يتحرّك الشريط أبدًا.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">الظهور التدريجي مع النطاقات</h2>\n<p>لبطاقات المتحدّثين، خط زمن الظهور هو الساعة المناسبة، و<code dir=\"ltr\">animation-range</code> تختار أيّ جزء من مسار البطاقة يشغّل الحركة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">speakers.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> reveal</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 24</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  to</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.speaker</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: reveal </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">linear</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> both</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-timeline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: view();</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-range</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: entry </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> cover</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 30</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 260\" role=\"img\" aria-labelledby=\"l-css-4-3-t1\"><defs><marker id=\"arrow-l-css-4-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-css-4-3-t1\">مستطيل طويل يمثّل الـ scrollport. تظهر بطاقة في ثلاثة مواضع: تدخل للتوّ عند الحافة السفلية، ثم داخله بالكامل، ثم تغادر عند الحافة العلوية. تحدّد التسميات نطاق entry عند الحافة السفلية، ونطاق contain وهي داخله بالكامل، ونطاق exit عند الحافة العلوية؛ ويمتدّ cover من أول دخول إلى آخر خروج.</title>\n  <rect class=\"d-box-accent d-dashed\" x=\"200\" y=\"40\" width=\"240\" height=\"180\" rx=\"8\"/>\n  <text class=\"d-label-muted\" x=\"210\" y=\"60\">scrollport</text>\n  <rect class=\"d-box-primary\" x=\"250\" y=\"200\" width=\"140\" height=\"44\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"290\" y=\"227\">بطاقة</text>\n  <rect class=\"d-box-primary\" x=\"250\" y=\"110\" width=\"140\" height=\"44\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"290\" y=\"137\">بطاقة</text>\n  <rect class=\"d-box-primary\" x=\"250\" y=\"18\" width=\"140\" height=\"44\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"290\" y=\"45\">بطاقة</text>\n  <text class=\"d-code\" x=\"460\" y=\"226\">entry</text>\n  <text class=\"d-code\" x=\"460\" y=\"138\">contain</text>\n  <text class=\"d-code\" x=\"460\" y=\"46\">exit</text>\n  <path class=\"d-line\" d=\"M150 244 L150 18\"/>\n  <text class=\"d-code\" x=\"60\" y=\"136\">cover</text>\n  <path class=\"d-arrow\" d=\"M600 236 L600 30\" marker-end=\"url(#arrow-l-css-4-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"560\" y=\"254\">التمرير إلى الأسفل</text>\n</svg><figcaption>خط زمن الظهور يتتبّع العنصر عبر الـ scrollport، والنطاقات المسمّاة تحدّد أجزاء ذلك المسار</figcaption></figure>\n<p>النطاقات المسمّاة تصف مسار العنصر: <strong>entry</strong> وهو يعبر الحافة السفلية، و<strong>contain</strong> وهو داخل المنطقة بالكامل (أو يغطّيها بالكامل إن كان أطول منها)، و<strong>exit</strong> وهو يعبر الحافة العلوية، و<strong>cover</strong> للرحلة كلها من أول بكسل يدخل إلى آخر بكسل يخرج. و<code dir=\"ltr\">entry 0% cover 30%</code> تعني «ابدأ حين تبدأ البطاقة بالدخول، وانتهِ حين تقطع 30% من رحلتها الكاملة»، فتكون كل بطاقة ظاهرة بالكامل قبل وقت كافٍ من وصول المستخدم إليها.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">أطلقها كتحسين</h2>\n<p>هذه صورة الدعم في أكتوبر 2026: الحركات المرتبطة بالتمرير موجودة في Chromium منذ الإصدار 115 (2023) وفي Safari منذ الإصدار 26 (سبتمبر 2025). ولدى Firefox تطبيق خلف إعداد تجريبي (flag)، وهي من مجالات التركيز في Interop 2026، لكن نسخة Firefox المستقرة لا تدعمها بعد، لذا فالميزة ليست ضمن Baseline.</p>\n<p>وهذا يحدّد طريقة كتابتك لها. الـ keyframes الخاصة بالظهور تبدأ من <code dir=\"ltr\">opacity: 0</code>، وفي متصفّح يفهم <code dir=\"ltr\">animation-timeline</code> لا مشكلة في ذلك. أما في متصفّح لا يفهمها، فإن <code dir=\"ltr\">animation: reveal linear both</code> بلا خط زمن حركة زمنية بمدة صفر تنتهي عند آخر keyframe، فتظل البطاقات ظاهرة. لكن لا تعتمد على هذه الدقيقة الخفيّة؛ اجعل النيّة صريحة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@supports</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">animation-timeline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: view()) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  @media</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (prefers-reduced-motion: no-preference) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    .speaker</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: reveal </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">linear</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> both</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      animation-timeline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: view();</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      animation-range</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: entry </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> cover</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 30</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>المتصفّحات غير الداعمة ومستخدمو تقليل الحركة يحصلون على الشبكة العادية الظاهرة بالكامل. والمتصفّحات الداعمة تحصل على الظهور التدريجي.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>لماذا يتفوّق هذا على مستمع التمرير</span></p>\n<p>الحركة المرتبطة بالتمرير لـ <code dir=\"ltr\">opacity</code> أو <code dir=\"ltr\">translate</code> أو <code dir=\"ltr\">scale</code> يمكن أن تعمل على الـ compositor، متزامنةً مع التمرير، حتى حين يكون الـ main thread مشغولًا بتشغيل JavaScript لديك. أما مستمع التمرير فيعمل على الـ main thread ويتأخّر دائمًا بإطار. ونسخة CSS أقصر وأنعم، ولا كود فيها يحتاج إلى تنظيف.</p>\n</aside>\n<p>وتستطيع أيضًا تسمية خطوط الزمن (<code dir=\"ltr\">view-timeline-name: --speaker</code>) ليتبعها عنصر آخر، واستخدام <code dir=\"ltr\">timeline-scope</code> لجعل الاسم مرئيًا في مستوى أعلى من الشجرة. الجأ إلى ذلك حين يجب أن يقود موضع تمرير عنصر ما عنصرًا <em>آخر</em>؛ أما للتأثيرات المكتفية بذاتها، فتكفي <code dir=\"ltr\">scroll()</code> و<code dir=\"ltr\">view()</code> بلا أسماء.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">دورك الآن</h2>\n<p>في التمرين أضف شريط تقدّم القراءة والظهور التدريجي للمتحدّثين، بالترتيب الصحيح، ومحميّين بـ <code dir=\"ltr\">@supports</code>. تقرأ الفحوصات خطوط الزمن والنطاقات المحسوبة. وبعدها ستتراجع خطوة عن التأثيرات المنفردة لتنظّم ملف الأنماط كله بحيث يستطيع فريق أن يعمل عليه.</p>\n","toc":[{"id":"sec-1","text":"نوعان من خطوط الزمن","level":2},{"id":"sec-2","text":"الظهور التدريجي مع النطاقات","level":2},{"id":"sec-3","text":"أطلقها كتحسين","level":2},{"id":"sec-4","text":"دورك الآن","level":2}],"takeaways":["<code dir=\"ltr\">animation-timeline: scroll()</code> تربط تقدّم الحركة بمقدار ما مُرِّرت حاوية التمرير؛ و<code dir=\"ltr\">view()</code> تربطه بمرور عنصر عبر المنطقة المرئية.","<code dir=\"ltr\">animation-range</code> تختار أيّ جزء من خط الزمن يشغّل الحركة، مثل <code dir=\"ltr\">entry 0% cover 30%</code> لظهور تدريجي ينتهي بعد قليل من ظهور العنصر.","الاختصار <code dir=\"ltr\">animation</code> يعيد ضبط <code dir=\"ltr\">animation-timeline</code>، لذا أعلن خط الزمن بعد الاختصار.","الحركات المرتبطة بالتمرير تعمل في Chromium وSafari 26 لكن ليس في نسخة Firefox المستقرة بعد، لذا غلّفها في <code dir=\"ltr\">@supports</code> وتأكّد من أن المحتوى ظاهر بالكامل دونها."],"further":[{"title":"Scroll-driven animation timelines (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations/Timelines"},{"title":"animation-timeline (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/animation-timeline"},{"title":"animation-range (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/animation-range"}],"quiz":[{"q":"<p>شريط التقدّم لا يتحرّك أبدًا. لماذا؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.progress</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-timeline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scroll</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(root);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: grow </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">linear</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> both</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">scroll(root)</code> غير صالحة؛ يجب أن تكون <code dir=\"ltr\">scroll(html)</code>.","why":"<code dir=\"ltr\">root</code> هي الكلمة المفتاحية الصحيحة لحاوية تمرير المستند؛ و<code dir=\"ltr\">html</code> غير مقبولة هناك."},{"text":"الاختصار <code dir=\"ltr\">animation</code> يأتي ثانيًا فيعيد ضبط <code dir=\"ltr\">animation-timeline</code> إلى <code dir=\"ltr\">auto</code>، فتعمل الحركة على الزمن بلا مدة.","why":"صحيح. الاختصار يعيد ضبط كل خصائص الحركة المفصّلة، بما فيها خط الزمن. ضع <code dir=\"ltr\">animation-timeline</code> بعده."},{"text":"الحركات المرتبطة بالتمرير تحتاج إلى <code dir=\"ltr\">animation-duration: 1s</code>.","why":"المدة لا تنطبق على خطوط زمن التمرير؛ التقدّم يأتي من موضع التمرير. و<code dir=\"ltr\">auto</code> مناسبة."}],"answer":1},{"q":"<p>يجب أن تظهر بطاقة متحدّث تدريجيًا وهي تدخل الـ viewport، وأن تكون ظاهرة بالكامل حين يظهر 30% منها. أيّ التصريحات تحقّق ذلك؟</p>","options":[{"text":"<code dir=\"ltr\">animation-timeline: scroll(); animation-range: 0% 30%;</code>","why":"خط زمن <code dir=\"ltr\">scroll()</code> يتتبّع موضع تمرير الصفحة كلها، فيشير النطاق إلى أول 30% من الصفحة، لا إلى البطاقة."},{"text":"<code dir=\"ltr\">animation-timeline: view(); animation-range: exit 0% exit 30%;</code>","why":"نطاق exit هو حين تغادر البطاقة الـ viewport، أي الطرف المقابل من مسارها."},{"text":"<code dir=\"ltr\">animation-timeline: view(); animation-range: entry 0% cover 30%;</code>","why":"صحيح. <code dir=\"ltr\">view()</code> تتتبّع ظهور البطاقة، والنطاق يبدأ حين تبدأ بالدخول وينتهي عند 30% من نطاق cover الكامل."}],"answer":2},{"q":"<p>في 2026، يريد زميلك إخفاء شبكة المتحدّثين حتى تُظهر الحركات المرتبطة بالتمرير كل بطاقة تدريجيًا. ما الخطر الرئيسي؟</p>","options":[{"text":"في المتصفّحات التي لا تدعم خطوط زمن التمرير، مثل نسخة Firefox المستقرة، قد لا تظهر أبدًا بطاقة تبدأ بـ opacity صفر.","why":"صحيح. الحركات المرتبطة بالتمرير ليست ضمن Baseline بعد. ضع حالة البداية المخفية داخل <code dir=\"ltr\">@supports (animation-timeline: view())</code> لتعرض المتصفّحات غير الداعمة البطاقات بشكل طبيعي."},{"text":"الحركات المرتبطة بالتمرير تحجز الـ main thread دائمًا وتسبّب تقطّعًا.","why":"حركات الخصائص الملائمة للـ compositor مثل opacity وtransform يمكن أن تعمل خارج الـ main thread؛ وهذه إحدى مزاياها على مستمعات التمرير."},{"text":"قارئات الشاشة تعلن كل إطار من الحركة.","why":"قارئات الشاشة لا تعلن إطارات الحركة البصرية."},{"text":"كل بطاقة تحتاج إلى خط زمن مسمّى خاص بها.","why":"خط زمن <code dir=\"ltr\">view()</code> المجهول الاسم خاص بكل عنصر تلقائيًا؛ والأسماء لا تلزم إلا لمشاركة خط زمن بين عناصر."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<div class=\"progress\" aria-hidden=\"true\"></div>\n<h2>Speakers</h2>\n<section class=\"speakers\">\n  <article class=\"speaker\">Ana Ortiz</article>\n  <article class=\"speaker\">Kenji Watanabe</article>\n  <article class=\"speaker\">Layla Haddad</article>\n  <article class=\"speaker\">Tom Becker</article>\n  <article class=\"speaker\">Priya Nair</article>\n  <article class=\"speaker\">Marcus Lee</article>\n</section>","css":".progress {\n  position: fixed;\n  inset: 0 0 auto;\n  height: 4px;\n  background: #6d28d9;\n  transform-origin: 0 50%;\n}\n.speakers { display: grid; gap: 16px; }\n.speaker {\n  padding: 40px 16px;\n  border-radius: 12px;\n  background: #f5f3ff;\n}\n\n@keyframes grow {\n  from { scale: 0 1; }\n  to { scale: 1 1; }\n}\n@keyframes reveal {\n  from { opacity: 0; translate: 0 24px; }\n  to { opacity: 1; translate: 0 0; }\n}\n\n/* A first attempt: why doesn't the bar move? */\n.progress {\n  animation-timeline: scroll(root);\n  animation: grow linear both;\n}\n"},"solution":{"html":"<div class=\"progress\" aria-hidden=\"true\"></div>\n<h2>Speakers</h2>\n<section class=\"speakers\">\n  <article class=\"speaker\">Ana Ortiz</article>\n  <article class=\"speaker\">Kenji Watanabe</article>\n  <article class=\"speaker\">Layla Haddad</article>\n  <article class=\"speaker\">Tom Becker</article>\n  <article class=\"speaker\">Priya Nair</article>\n  <article class=\"speaker\">Marcus Lee</article>\n</section>","css":".progress {\n  position: fixed;\n  inset: 0 0 auto;\n  height: 4px;\n  background: #6d28d9;\n  transform-origin: 0 50%;\n}\n.speakers { display: grid; gap: 16px; }\n.speaker {\n  padding: 40px 16px;\n  border-radius: 12px;\n  background: #f5f3ff;\n}\n\n@keyframes grow {\n  from { scale: 0 1; }\n  to { scale: 1 1; }\n}\n@keyframes reveal {\n  from { opacity: 0; translate: 0 24px; }\n  to { opacity: 1; translate: 0 0; }\n}\n\n@supports (animation-timeline: view()) {\n  @media (prefers-reduced-motion: no-preference) {\n    .progress {\n      animation: grow linear both;\n      animation-timeline: scroll(root);\n    }\n    .speaker {\n      animation: reveal linear both;\n      animation-timeline: view();\n      animation-range: entry 0% cover 30%;\n    }\n  }\n}\n"},"checks":[{"id":"progress-scroll-timeline","test":"const src = [...document.querySelectorAll('style')].map((s) => s.textContent).join('\\n'); const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches || !CSS.supports('animation-timeline', 'view()'); if (reduced) return /animation-timeline:\\s*scroll\\(/.test(src); return style('.progress', 'animation-timeline').startsWith('scroll(') && style('.progress', 'animation-name') === 'grow';"},{"id":"progress-linear","test":"const src = [...document.querySelectorAll('style')].map((s) => s.textContent).join('\\n'); const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches || !CSS.supports('animation-timeline', 'view()'); if (reduced) return /grow\\s+linear/.test(src); return style('.progress', 'animation-timing-function') === 'linear' && ['both', 'forwards'].includes(style('.progress', 'animation-fill-mode'));"},{"id":"speaker-view-timeline","test":"const src = [...document.querySelectorAll('style')].map((s) => s.textContent).join('\\n'); const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches || !CSS.supports('animation-timeline', 'view()'); if (reduced) return /animation-timeline:\\s*view\\(/.test(src); return $$('.speaker').every((el) => style(el, 'animation-timeline').startsWith('view(') && style(el, 'animation-name') === 'reveal');"},{"id":"speaker-range","test":"const src = [...document.querySelectorAll('style')].map((s) => s.textContent).join('\\n'); const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches || !CSS.supports('animation-timeline', 'view()'); if (reduced) return /animation-range:\\s*entry/.test(src); return style('.speaker', 'animation-range-start').startsWith('entry') && style('.speaker', 'animation-range-end') === 'cover 30%';"},{"id":"guarded","test":"const src = [...document.querySelectorAll('style')].map((s) => s.textContent).join('\\n'); if (!CSS.supports('animation-timeline', 'view()')) return /@supports\\s*\\(\\s*animation-timeline/.test(src); const all = []; const walk = (rules, ctx) => { for (const r of rules) { all.push({ r, ctx }); if (r.cssRules) walk(r.cssRules, ctx + ' ' + (r.conditionText || '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); return all.some(({ r, ctx }) => r.selectorText === '.speaker' && /view\\(/.test(r.cssText) && ctx.includes('animation-timeline'));"}],"title":"تقدّم وظهور تدريجي مرتبطان بالتمرير","prompt":"<p>الـ keyframes <code dir=\"ltr\">grow</code> و<code dir=\"ltr\">reveal</code> جاهزة. اربطها بالتمرير:</p>\n<ol>\n<li>أصلح شريط التقدّم ليعمل <code dir=\"ltr\">grow</code> على <strong>خط زمن تمرير المستند</strong>، بتوقيت <code dir=\"ltr\">linear</code> وبنمط تعبئة يُبقي الـ keyframes مطبّقة (<code dir=\"ltr\">both</code>).</li>\n<li>اجعل كل <code dir=\"ltr\">.speaker</code> تشغّل <code dir=\"ltr\">reveal</code> على <strong>خط زمن ظهورها</strong>، من بداية نطاق <strong>entry</strong> إلى <strong>30% من نطاق cover</strong>.</li>\n<li>ضع القواعد المرتبطة بالتمرير داخل <code dir=\"ltr\">@supports (animation-timeline: view())</code> لتعرض المتصفّحات غير الداعمة الصفحة العادية. (ويُنصح بتغليفها أيضًا في <code dir=\"ltr\">prefers-reduced-motion: no-preference</code>.)</li>\n</ol>\n","hints":["انظر إلى الترتيب في كود البداية: أيّ تصريح يعيد ضبط الآخر؟","للمتحدّثين: <code dir=\"ltr\">animation: reveal linear both;</code> ثم <code dir=\"ltr\">animation-timeline: view();</code> و<code dir=\"ltr\">animation-range: …</code>.","<code dir=\"ltr\">@supports (animation-timeline: view()) { .progress { animation: grow linear both; animation-timeline: scroll(root); } .speaker { animation: reveal linear both; animation-timeline: view(); animation-range: entry 0% cover 30%; } }</code>"],"explanation":"<p>لم يكن شريط البداية يتحرّك لأن الاختصار <code dir=\"ltr\">animation</code> جاء بعد <code dir=\"ltr\">animation-timeline</code> فأعاد ضبطها إلى <code dir=\"ltr\">auto</code>، تاركًا حركة زمنية بطول صفر. إعلان خط الزمن بعد الاختصار يربط <code dir=\"ltr\">grow</code> بحاوية تمرير الجذر، و<code dir=\"ltr\">linear</code> تُبقي الشريط متناسبًا مع مسافة التمرير. وخط زمن <code dir=\"ltr\">view()</code> المجهول الاسم لكل متحدّث يتتبّع تلك البطاقة عبر الـ viewport، و<code dir=\"ltr\">entry 0% cover 30%</code> تجعل الظهور ينتهي بعد قليل من ظهور البطاقة. وحماية <code dir=\"ltr\">@supports</code> تُبعد الظهور التدريجي عن المتصفّحات التي لا تستطيع تشغيله، مثل نسخة Firefox المستقرة في 2026، حيث تبقى البطاقات ظاهرة ببساطة.</p>\n","checkLabels":{"progress-scroll-timeline":"حركة الشريط تعمل على خط زمن scroll()","progress-linear":"الشريط يستخدم توقيتًا linear ويُبقي الـ keyframes مطبّقة","speaker-view-timeline":"كل بطاقة متحدّث تشغّل reveal على خط زمن view()","speaker-range":"الظهور التدريجي يمتدّ من entry إلى cover 30%","guarded":"القواعد داخل @supports (animation-timeline: …)"}},"words":599,"readMinutes":4}