حلقة التقدّم

تقدّم حلقة التقدّم (أو شريط التقدّم الدائري) ملاحظات مرئية حول حالة المهمة. على عكس شريط التقدّم الخطي، يُعدّ شكله الدائري مثاليًا للوحات البيانات أو مكوّنات البطاقات أو أي مكان تكون فيه المساحة محدودة.

ينفّذ هذا الدليل حلقة تقدّم من خلال:

  • استخدام عنصر <progress> الأصلي كأساس دلالي لضمان إمكانية وصول برامج قراءة الشاشة ومستخدمي لوحة المفاتيح إلى المكوّن تلقائيًا
  • تنسيق المكوّن باستخدام conic-gradient() وmask-image، ما يتيح إنشاء حلقة متجاوبة بالكامل وقابلة للتخصيص بدون تعقيدات تعديل مسار SVG.
  • الاستفادة من خصائص CSS المخصّصة و@property لتفعيل عمليات انتقال سلسة ومسرَّعة من خلال وحدة معالجة الرسومات لملء شريط التقدّم

يُفضَّل هذا النهج على الحلول التي تستخدم SVG فقط لأنّه يستخدم عنصر <progress> الدلالي بدلاً من ARIA، ويتكامل بسهولة أكبر مع التنسيق وأنظمة التصميم وأسلوب الخط الحالي.

يمكنك الاطّلاع على مؤشر سريان العمل للتعرّف على كيفية التعامل مع حالات التحميل غير المحدّدة.

التنفيذ

1. Markup

استخدِم برنامج تضمين للاحتفاظ بكل من الحلقة المرئية والمحتوى المركزي الاختياري. يظل العنصر <progress> هو المصدر الدلالي للحقيقة. استخدِم فئة خدمات لإخفاء شريط التقدّم الأصلي بشكل مرئي مع إبقائه متاحًا.

<div class="ring-wrapper">
  <progress value="75" max="100" aria-label="Task progress" class="progress-ring"></progress>
  <!-- Optional: Content to display in the center -->
  <div class="ring-content">
    75%
  </div>
</div>

2. الأنماط

إخفاء واجهة المستخدم الأصلية

لتصميم عنصر <progress> كحلقة تقدّم، عليك أولاً إخفاء نمط المتصفّح التلقائي لأشرطة التقدّم.

/* Hide native bars */
progress.loading-spinner:indeterminate::-webkit-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-webkit-progress-value {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-moz-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::slider-fill {
  display: none;
  background: none;
}

الحاوية والحلقة

يوفّر الغلاف سياق تحديد الموضع. يعالج العنصر <progress> التدرج المرئي.

.progress-ring-wrapper {
  position: relative;
  display: grid;
  place-items: center;
}

progress.progress-ring {
  --size: 150px;
  --thickness: 16px;
  --track-color: #f1f5f9;
  --fill-color: #3b82f6;
  --value: attr(value type(<number>));
  width: var(--size);
  height: var(--size);

  transition: --value 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 50%;

  background: conic-gradient(
    var(--fill-color) calc(var(--value) * 1%),
    var(--track-color) 0
  );

  /* MANDATORY: Clip the background to the border-area. */
  background-clip: border-area;
  border: var(--thickness) solid transparent;
  background-origin: border-box;
}

.progress-ring-content {
  /* Positioned in the center of the wrapper */
  position: absolute;
}

يمكنك أيضًا استخدام radial-gradient لإنشاء أغطية طرفية مستديرة.

تفعيل الانتقالات السلسة باستخدام @property

لتحريك حلقة التقدّم بسلاسة عند تغيُّر القيمة، سجِّل --value كخاصية مخصّصة رقمية.

قد يجد المستخدمون الذين يعانون من حساسية الحركة أن الانتقال بين القيم مربك. يجب الالتزام بالاستعلام عن الوسائط prefers-reduced-motion من خلال ضبط مدة تبلغ 0 ثانية (فورية) تلقائيًا، وضبط مدة أطول للمستخدمين الذين ليس لديهم أي تفضيلات.

@property --value {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

progress.progress-ring {
  transition: --value 0s ease-in-out;
  @media (prefers-reduced-motion: no-preference) {
    transition-duration: 0.4s;
  }
}

3- آخر الأخبار حول مستوى التقدّم

عدِّل السمة value في العنصر <progress> والمحتوى النصي لـ .progress-ring-content (في حال عرض نسبة مئوية) كلما تغيّرت القيمة.

4. حالة النجاح الاختيارية

يمكنك استخدام أداة اختيار سمات CSS لتعديل مظهر الحلقة تلقائيًا (مثل تغيير اللون إلى الأخضر) عند اكتمال المهمة.

/* Change the fill color to green when the progress reaches 100% */
progress.progress-ring[value="100"] {
  --fill-color: #10b981;
}

استراتيجيات احتياطية

لا تضِف قيمة احتياطية داخل العنصر <progress>. ولا تستخدمه التكنولوجيا المساعِدة وتتجاهله جميع المتصفحات الحديثة.

الصور المتحركة الاحتياطية

إذا لم يكن @property متاحًا، ستنتقل الحلقة إلى القيمة الجديدة على الفور بدلاً من الانتقال بسلاسة. في معظم الحالات، لا بأس بذلك ولا يؤدي إلى تعطُّل أي وظيفة.

إذا كان الانتقال ضروريًا للغاية، يمكنك التحقّق من توفّر دعم @property واستخدام حلقة requestAnimationFrame() لاستيفاء --value في المتصفّحات القديمة.

بالنسبة إلى المتصفّحات التي لا تتيح استخدام background-clip: border-area بعد، يمكنك استخدام mask-image لتفريغ وسط العنصر <progress>.

@supports not (background-clip: border-area) {
  mask-image: radial-gradient(
    transparent calc(50% - var(--thickness)),
    black calc(50% - var(--thickness) + 0.5px)
  );
  border: 0;
}

بالنسبة إلى المتصفّحات التي لا تتيح استخدام دالة CSS‏ attr() لأي سمة، استخدِم MutationObserver لمزامنة السمة value تلقائيًا مع السمة المخصّصة --value.

if (!CSS.supports("width: attr(value type(<number>))")) {
  const observer = new MutationObserver(() => {
    progress.style.setProperty("--value", progress.getAttribute("value"));
  });
  observer.observe(progress, {
    attributes: true,
    attributeFilter: ["value"],
  });
}