UIDex

التمرير اللانهائي

Infinite Scroll

IntersectionObserver

اسمه كمانسكرول لانهائي، انفينيت سكرول، التمرير المتواصل، التحميل عند التمرير، القائمة اللانهائية، الخلاصة المتواصلة، endless scroll، continuous scroll، infinite list، auto load on scroll، endless feed، scroll pagination

التمرير اللانهائي أسلوب لعرض قائمة طويلة: كلما اقترب المستخدم من آخر ما هو معروض بدأ طلب الدفعة التالية، وتُلحَق الدفعة بما قبلها في نفس الصفحة. والشرط الذي يميّزه أن موضع التمرير هو ما يطلق الطلب، فلا يوجد قرار صريح من المستخدم بتحميل المزيد. وهو ليس ترقيم صفحات، لأن الترقيم يمنح كل شريحة عنواناً ثابتاً مثل ‎?page=7‎ ويستبدل المحتوى استبدالاً كاملاً فيبقى حجم الصفحة كما هو، بينما هنا تتراكم العناصر في DOM واحد بلا سقف وبلا عنوان يعود إليه أحد. وهو ليس زرّ «تحميل المزيد» كذلك، فالميكانيكا واحدة (نفس الطلب، ونفس الإلحاق أسفل القائمة)، والفرق الوحيد أن الزرّ ينتظر ضغطة، وهذه الضغطة وحدها تحلّ أغلب مشاكل الوصولية التي يخلقها التحميل التلقائي. وهو ليس القائمة الافتراضية (virtualization)، فتلك تقنية رسم تُبقي في DOM ما يقع قرب النافذة وحده، وتصلح كذلك لبيانات محمّلة بالكامل، والاثنان يُجمعان عادةً لأن تمريراً لانهائياً بلا تدوير للعناصر يخنق الصفحة بعد بضع دفعات. وقبل أن تختاره اسأل: هل سيحتاج أحد أن يرسل رابطاً لعنصر بعينه، وهل يوجد تحت القائمة شيء يحتاجه المستخدم؟ إن كان الجواب نعم في أيّهما فالترقيم أو الزرّ هو ما تريده.

لو قلت عليه…

«الصفحة اللي بتفضل تحمّل لوحدها كل ما أنزل تحت»«الليستة اللي مالهاش آخر زي التطبيقات»«مش قادر أوصل للفوتر خالص»«رجعت بالباك لقيت نفسي من أول الأوّل»«كل ما أوصل تحت يجيب حاجات جديدة من نفسه»

العيّنة الحيّة

تفاعل مع الديمو. كل الأجزاء حقيقية ومرقّمة.

آخر التحديثات
١تحديث سياسة الخصوصية
٢تقرير أداء الأسبوع
٣ملاحظات اجتماع الفريق
٤مراجعة تصميم الصفحة
٥طلبات الدعم الجديدة
٦قائمة المهامّ المتأخّرة
عنصر الاستشعار

المعروض ٦ من ١٢

RTLالتمرير على المحور الكتلي، فترتيب الدفعات لا ينعكس مع اتجاه الصفحة

تشريح العنصر: كل جزء واسمه

مرّر على أي سطر ليتحدّد مكانه في الديمو فوق.

برومبت البناء

ابنِ قائمة بتمرير لانهائي فوق ترقيم بمؤشّر (cursor) لا بإزاحة. أَلحِق كل دفعة بالقائمة الموجودة بمفتاح ثابت لكل صفّ مأخوذ من المعرّف لا من ترتيب المصفوفة. ضع بعد آخر صفّ عنصر استشعار فارغاً يراقبه IntersectionObserver بـ rootMargin سفلي نحو ٤٠٠ بكسل، ومرّر في root الحاوية التي تُمرَّر فعلاً إن لم تكن النافذة. احرس الطلب بعلم isLoading وفُكّ المراقبة أثناءه حتى لا تُجلب نفس الصفحة مرّتين. أضف زرّ «تحميل المزيد» حقيقياً بـ ‎<button type="button">‎ واتركه ظاهراً دائماً، فهو وحده ما يستطيع القارئ تفعيله عن قصد. ضع في الصفحة منذ أول رسم منطقة ‎aria-live="polite"‎ مخفية بصرياً تُكتب بعد كل دفعة بصيغة «أُضيف ٢٠ عنصراً، المعروض ٦٠ من ٣٤٨»، وضع ‎aria-busy="true"‎ على القائمة أثناء الطلب. احجز لمؤشّر التحميل ارتفاع الصفوف القادمة نفسه حتى لا يقفز موضع التمرير. حدّث ‎?cursor=‎ في العنوان بـ ‎history.replaceState‎ كلما تغيّرت الدفعة الظاهرة، واضبط ‎history.scrollRestoration = "manual"‎ واحفظ موضع التمرير وعدد الدفعات المحمّلة في sessionStorage لتستعيدهما عند الرجوع بزرّ المتصفّح. أظهر رسالة نهاية صريحة بعدد العناصر عند ‎hasNextPage === false‎ وأزل عندها عنصر الاستشعار، وأظهر رسالة فشل مع زرّ «إعادة المحاولة» بدل إعادة الطلب في حلقة. لا تضع في الفوتر رابطاً يحتاجه أحد، وانقل روابطه إلى الرأس أو الشريط الجانبي. استخدم الخصائص المنطقية في كل موضع: ‎inset-inline-end‎ لزرّ العودة إلى الأعلى وللمؤشّر، و‎text-align: start‎، و‎padding-inline‎، ولا تكتب left ولا right. ولو كانت القائمة أفقية فاكتشف النهاية من عنصر الاستشعار الموضوع آخر DOM لا من scrollLeft.

سلوكه في الاتجاه من اليمين لليسار

الجزء ده حصري عندنا.

أي محور ينعكس أصلاً

لا ينعكس

الخلاصة الرأسية لا تنعكس: اتجاه الصفحة يحكم المحور المضمّن (الأفقي)، والتمرير هنا يجري على المحور الكتلي، فترتيب الصفوف واتجاه نموّ القائمة واحد في العربية والإنجليزية. والمصيدة الوحيدة هي rootMargin في IntersectionObserver، فهو قيمة فيزيائية بحتة تُكتب بترتيب هوامش CSS (أعلى، يمين، أسفل، يسار) ولا تقبل أي كلمة منطقية. القائمة الرأسية تعنيها القيمة الثالثة وحدها وهي نفسها في الاتجاهين، أما القائمة الأفقية فتحتاج القيمة الثانية في LTR والرابعة في RTL، فاحسبها من direction بدل كتابتها ثابتة.

المؤشّر الدوّار والصفوف الهيكلية

لا ينعكس

المؤشّر الدوّار لا يُقلب أبداً: قاعدة شاملة مثل ‎[dir="rtl"] svg { transform: scaleX(-1) }‎ تعكس اتجاه دورانه فيبدو وكأنه يدور إلى الخلف. الذي ينعكس هو موضعه فقط، فثبّته بـ inset-inline-end إن كان معلّقاً في ركن، أو ضعه في صفّ flex واترك المحور المضمّن يتكفّل به. والصفوف الهيكلية التي تلمع بـ ‎linear-gradient(to right, …)‎ تحتاج تدخّلاً يدوياً، لأن التدرّجات لا تعرف الكلمات المنطقية: اللمعة تسير عكس اتجاه القراءة في العربية ما لم تقلبها إلى to left داخل ‎[dir="rtl"]‎.

عدّاد العناصر

لا ينعكس

الرقم نفسه لا ينقلب: خوارزمية Unicode ثنائية الاتجاه تصنّف الأرقام ضمن EN و AN وتُبقيها تُقرأ من اليسار إلى اليمين داخل نصّ عربي، فـ«٦٠ من ٣٤٨» تحتفظ بترتيبها الداخلي بلا أي تدخّل منك. القرار الوحيد المطلوب هو نظام الأرقام، والالتزام به في كل موضع: ‎new Intl.NumberFormat("ar-EG").format(348)‎ يعطيك ٣٤٨ بالأرقام العربية الهندية، وإضافة ‎-u-nu-latn‎ إلى الوسم تعطيك 348 بالأرقام اللاتينية. ولا تخلط النظامين في شاشة واحدة، فعدّاد بالهندية فوق أرقام صفوف باللاتينية يجعل المستخدم يظنّ أنهما يعدّان شيئين مختلفين.

القائمة الأفقية

ينعكس

في التمرير اللانهائي الأفقي تنتقل نهاية القائمة إلى اليسار في العربية، وينتقل معها عنصر الاستشعار. ولا تحسب النهاية بـ ‎scrollLeft + clientWidth >= scrollWidth‎: المتصفّحات الحديثة (Chrome و Firefox و Safari) توافقت على نموذج واحد يجعل scrollLeft صفراً عند البداية المنطقية ثم يصبح سالباً كلما تقدّم المستخدم نحو النهاية في RTL، فالشرط لا يتحقّق أبداً، وإن اضطررت إلى الحساب فاستعمل ‎Math.abs(el.scrollLeft)‎. والأنظف أن تتخلّى عن الحساب أصلاً: اجعل عنصر الاستشعار آخر عقدة في DOM ودع IntersectionObserver يقرّر، لأن ترتيب DOM هو الترتيب المنطقي في الاتجاهين، ولأن ‎flex-direction: row‎ يضع أول عنصر عند البداية المنطقية وحده بلا row-reverse.

اتجاه نصّ العناصر الواردة

لا ينعكس

محتوى الخلاصة يصل من الخادم مختلطاً: منشور عربي، ثم اسم مستودع بالإنجليزية، ثم سطر يبدأ برقم. جهة الصفّ تُحسم من الصفّ نفسه لا من الصفحة، فاضبط ‎dir="auto"‎ على العنصر النصّي داخل كل صفّ لا على القائمة كلها، وحاذِ بـ ‎text-align: start‎ لا بـ ‎text-align: right‎ حتى تتبع المحاذاة الجهة التي حُسمت. وانعكاس الصفحة لا يغيّر شيئاً في صفّ لاتيني، وهذا هو المقصود، ولو تركت القرار للحاوية وحدها ظهرت النقطة والقوسان في الطرف الخطأ من كل سطر لاتيني.

أسماؤه في الكود

كل سطر هو كلمة مكتبة واحدة عن نفس الشيء. خد السطر اللي بيكلّم مشروعك.

DOMnew IntersectionObserver(cb, { root, rootMargin })المراقب الذي يغني عن حساب scrollTop؛ و rootMargin فيه فيزيائي بترتيب هوامش CSS.
ARIAaria-live="polite" + aria-busy="true"الإشارة الوحيدة التي يتلقّاها قارئ الشاشة عند إلحاق صفوف جديدة.
HTML<button type="button">طلب صريح للدفعة التالية؛ البديل الوحيد أن يمرّر المستخدم حتى يقع التقاطع.
TanStack QueryuseInfiniteQuery({ getNextPageParam })يعطيك fetchNextPage و hasNextPage و isFetchingNextPage جاهزة فوق ترقيم بمؤشّر.
CSScontent-visibility: auto + contain-intrinsic-sizeيخرج الصفوف البعيدة من الرسم؛ وبلا الحجم المقدّر يهتزّ شريط التمرير.
DOMhistory.scrollRestoration = "manual"الاستعادة التلقائية تفشل لأن الصفوف لم تُحمَّل بعد، فاحفظ الموضع واستعده بنفسك.

شوف كمان

آخر تحديث · 2026-08-19