UIDex

مؤشّر التقدّم

Progress Bar vs Spinner vs Ring

role="progressbar"

اسمه كمانشريط التقدّم، بروجرس بار، سبينر، مؤشّر التحميل، حلقة التقدّم، لودر، progress bar، spinner، loader، progress ring، activity indicator، loading indicator

مؤشّر التقدّم عنصر عرض يقول إن عملية تجري الآن، وكم بقي منها إن كان ذلك معلوماً. وهو ثلاثة أشكال لمعنيين اثنين: الشريط المحدّد (determinate) يعرف النسبة فيعرضها، والمؤشّر الدوّار (spinner) لا يعرفها فيقول «انتظر» ولا شيء غير ذلك، وحلقة التقدّم هي الشريط نفسه ملفوفاً في دائرة، واختيارها بدل الشريط قرار بصري لا يغيّر شيئاً في الدلالة ولا في ARIA. وقاعدة الاختيار زمنية لا ذوقية: لا تعرض شيئاً في أوّل ٤٠٠ ميلي ثانية تقريباً لأن مؤشّراً يومض ثم يختفي يُقرأ كخلل في الواجهة، والمؤشّر الدوّار يكفي حتى نحو أربع ثوانٍ، وما زاد على ذلك يحتاج شريطاً محدّداً بنسبة مئوية، وإن كان العمل غير قابل للقياس فسمِّ الخطوة الجارية بدل اختراع رقم. وهو ليس الهيكل العظمي (skeleton)، فذاك يرسم شكل المحتوى القادم في مكانه ليمنع قفزة التخطيط، أمّا مؤشّر التقدّم فيقيس زمناً. وليس شريط الخطوات (stepper)، لأن الخطوات تنقّل يملك المستخدم التحرّك فيه، بينما مؤشّر التقدّم قراءة لعمل لا يد له فيه. وليس المنزلق (slider): متى صار المستخدم قادراً على سحب القيمة فأنت أمام slider مهما كان الشكل، والفارق التقني أن progressbar لا يقبل التركيز ولا يقبل إدخالاً.

لو قلت عليه…

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

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

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

جارٍ رفع «العرض-النهائي.pdf»٥٪
aria-valuenow="5"يتحرّك كل ٥٪ لا مع كل إطار
٥٪
جارٍ الاتصال بالخادم
RTLالتعبئة تبدأ من اليمين، والدوّار لا ينعكس معها

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

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

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

ابنِ مؤشّر تقدّم بثلاثة أشكال: شريط محدّد، وحلقة بنفس القيمة، ومؤشّر دوّار غير محدّد. اجعل الشكل تابعاً للزمن لا للذوق: لا تعرض شيئاً قبل ٤٠٠ ميلي ثانية، واكتفِ بالمؤشّر الدوّار حتى نحو أربع ثوانٍ، وانتقل إلى شريط محدّد بنسبة مئوية فيما زاد على ذلك، وإن كانت النسبة غير قابلة للقياس فاعرض خطوات مسمّاة لا رقماً مخترعاً. ضع role="progressbar" على المسار نفسه مع aria-valuenow و aria-valuemin="0" و aria-valuemax="100"، واربطه بتسمية مرئية عبر aria-labelledby لأن هذا الدور لا يأخذ اسمه من محتواه. في الحالة غير المحدّدة احذف aria-valuenow ولا تضع مكانه صفراً، وأضف role="status" ونصّاً مصاحباً حتى لا يكون الدوران صامتاً. استعمل aria-valuetext حين يكون العدد الخام بلا معنى مثل «٣ ملفّات من ١٢»، ولا تحدّث القيمة مع كل إطار بل عند عتبات أو كل ثانيتين. اضمن تبايناً لا يقلّ عن ٣:١ بين التعبئة والمسار، وداخل ‎@media (prefers-reduced-motion: reduce)‎ أوقف الدوران واستبدل به نبضة شفافية أو نصّاً ثابتاً. واستخدم الخصائص المنطقية في كل مكان: ثبّت التعبئة بـ inset-inline-start: 0 وحرّك inline-size، ولا تكتب left: 0 ولا transform-origin: left. واكتب زحف الشريط غير المحدّد بـ translateX(calc(var(--sweep) * var(--dir))) مع ضبط ‎--dir: -1 داخل [dir="rtl"]، فـ transform لا مكافئ منطقي لها. ولا تقلب المؤشّر الدوّار ولا هندسة الحلقة عند الانعكاس، ووحّد نظام الأرقام في الصفحة كلها عبر Intl.NumberFormat مع font-variant-numeric: tabular-nums.

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

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

اتجاه امتلاء الشريط

ينعكس

الشريط يمتلئ من البداية المنطقية، أي من اليمين في العربية. وأضمن بناء لذلك عنصر تعبئة مثبّت بـ inset-inline-start: 0 تحرّك فيه inline-size من ٠٪ إلى القيمة، فينعكس من تلقاء نفسه لأن الحافّة التي رُسِي عليها هي التي تبدّلت. وإن أصررت على scaleX فاعلم أن transform-origin لا كلمة منطقية لها أصلاً: كلماتها فيزيائية (left و right و top و bottom و center) إلى جانب الأطوال والنِسَب، ومعنى ذلك أنك ستكتب transform-origin: right يدوياً داخل [dir="rtl"] أو تُشير المقياس بمتغيّر اتجاه.

زحف الشريط غير المحدّد

ينعكس

الشريط غير المحدّد يمرّر شريحة قصيرة عبر المسار، وانطلاقها من البداية المنطقية هو ما يوافق القراءة. غير أن هذه الحركة مكتوبة داخل @keyframes بـ translateX، و transform خاصّية فيزيائية لا مكافئ منطقي لها في CSS كلّها، فالإطارات الثابتة تجعل الشريحة تزحف عكس اتجاه القراءة في العربية. اضرب الإزاحة في متغيّر إشارة: translateX(calc(var(--sweep) * var(--dir))) مع ضبط ‎--dir: -1 داخل [dir="rtl"]، أو اكتب مجموعة @keyframes ثانية مقصورة على [dir="rtl"].

دوران المؤشّر

لا ينعكس

الدوران ليس اتجاه قراءة، فالمؤشّر الدوّار يبقى كما هو حرفياً حين تنقلب الصفحة. الزاوية الموجبة في rotate() تدور مع عقارب الساعة في الاتجاهين معاً، والقاعدة الشاملة [dir="rtl"] svg { transform: scaleX(-1) } تقلبها إلى عكس عقارب الساعة وتنقل معها الطرف المستدير للقوس (stroke-linecap) إلى الجهة الأخرى، فيصير ذيل القوس مقدّمته. اقصر أي قلب على صنف تضعه على الأيقونات الاتجاهية وحدها، واترك المؤشّر الدوّار وأيقونة الساعة خارجه.

هندسة الحلقة مقابل تسميتها

لا ينعكس

الحلقة مرسومة داخل نظام إحداثيات SVG، وخاصّية direction لا أثر لها على الهندسة هناك. لذلك تبقى نقطة بداية stroke-dashoffset في مكانها: المسار المكافئ لـ <circle> يبدأ عند أقصى يمين الدائرة ويسير مع عقارب الساعة، والدوران بـ rotate(-90) هو ما ينقل البداية إلى أعلاها، وهو قرار بصري ثابت لا لغة فيه. أمّا الرقم في وسط الحلقة فهو الجزء الذي يستجيب: نصّ HTML عادي يتبع اتجاه الصفحة، وإن رسمته بـ <text> داخل الـ SVG فقيمتا text-anchor: start و end تُحسبان من اتجاه النصّ نفسه فتتبادلان في RTL، فاستعمل text-anchor: middle أو ضع الرقم عنصر HTML فوق الحلقة.

الأرقام ونظام الترقيم

لا ينعكس

الأرقام نفسها لا تنقلب: خانات العدد تُقرأ من اليسار إلى اليمين حتى داخل نصّ عربي، وخوارزمية Bidi تعامل الرقم كمقطع واحد. المشكلة الحقيقية خلط النظامين في الشاشة الواحدة، شريط يقول 42% وحلقة تقول ٤٢٪. ولّد كل رقم من مكان واحد: ‎new Intl.NumberFormat("ar-EG", { style: "percent" })‎ يعطيك الأرقام العربية الهندية مع علامة النسبة ٪ (U+066A)، و ‎ar-EG-u-nu-latn‎ يعطيك أرقام ٠ إلى ٩ اللاتينية مع علامة % اللاتينية، لأن رمز النسبة يتبع نظام الترقيم لا اللغة. وأضف font-variant-numeric: tabular-nums حتى لا يتغيّر عرض الرقم مع كل تحديث فيهتزّ ما حوله.

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

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

ARIArole="progressbar" + aria-valuenowالزوج الذي يعرّف العنصر؛ احذف aria-valuenow في الحالة غير المحدّدة.
HTML<progress value="42" max="100">العنصر الأصلي؛ حذف السمة value وحدها يحوّله إلى غير محدّد، وتنسيق شريطه يمرّ عبر عناصر زائفة خاصّة بكل محرّك.
Radix UI<Progress.Root value> + <Progress.Indicator>تمرير value={null} يضبط data-state="indeterminate" فتنسّق الحالتين من CSS واحد.
Material UI<LinearProgress variant="determinate" value={n} />الافتراضي هو indeterminate، و <CircularProgress /> هو نفس المنطق بشكل حلقة.
Ant Design<Progress type="circle" percent={42} />مكوّن واحد يبدّل بين line و circle و dashboard بخاصّية type وحدها.
SwiftUIProgressView(value:total:)المُهيّئ بلا value يعطيك مؤشّراً دوّاراً، ومع value يعطيك شريطاً محدّداً.
CSS@media (prefers-reduced-motion: reduce)الاستعلام الذي يبدّل الدوران المتّصل بنبضة شفافية أو بنصّ ثابت.

شوف كمان

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