يُعلم مؤشر التحميل الدوّار (أو مؤشر النشاط) المستخدمين بأنّ هناك عملية جارية عندما تكون المدة المحدّدة غير معروفة. على عكس حلقة التقدّم، يكون مؤشر التحميل "غير محدّد" ويستخدم عادةً الصور المتحركة للإشارة إلى النشاط.
ينفّذ هذا الدليل مؤشر تحميل من خلال:
- استخدام عنصر
<progress>الأصلي كأساس دلالي من خلال حذف السمةvalue، يتعامل المتصفّح معها كشريط تقدّم غير محدّد، ما يضمن الإشارة إليها بشكلٍ صحيح من خلال التكنولوجيا المساعِدة. - تنسيق المكوّن باستخدام
conic-gradient()لإنشاء "مسار" مرئي وbackground-clip: border-areaلتفريغ المركز إلى حلقة. - تحريك مؤشر التحميل بكفاءة باستخدام تحويلات CSS ومراعاة
prefers-reduced-motionلضمان تجربة مريحة لجميع المستخدمين
يمكنك الاطّلاع على حلقة التقدّم لمعالجة المهام المحدّدة ذات المدة المعروفة.
التنفيذ
1. Markup
استخدِم عنصر <progress> الأصلي كمصدر دلالي للحقيقة والمكوّن المرئي. بدون السمة value، تكون القيمة غير محدّدة ضمنيًا.
<progress aria-label="Loading" class="loading-spinner"></progress>
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;
}
حلقة دوّارة ومسار
يستخدم المؤشر الدوّار conic-gradient لإنشاء مسار مرئي.
@property --arc-start {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --arc-end {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
/* Use a custom property to conditionally include the dash animation */
@property --progress-dash-animation {
syntax: "*";
inherits: false;
initial-value: , progress-dash 3s ease-in-out infinite;
}
progress.loading-spinner:indeterminate {
--_from: calc(90deg + var(--arc-start, 0deg));
--_to: calc(90deg + var(--arc-end, 158deg));
--size: 40px;
--thickness: 2px;
--spinner-color: #3b82f6;
--track-color: #e2e5e7;
--spinner-duration: 1.5s;
--_used-spinner-duration: var(--spinner-duration);
--spinner-timing: linear;
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
appearance: none;
/* Create the fading trail with dynamic angles */
background: conic-gradient(
from var(--_from),
var(--spinner-color) calc(var(--_to) - var(--_from)),
transparent 0
)
var(--track-color);
@supports (background-clip: border-area) {
background-clip: border-area;
border: var(--thickness) solid transparent;
background-origin: border-box;
}
/* ... fallback for background-clip: border-area ... */
/* The dash animation is only included if @property is supported */
animation:
progress-spin var(--_used-spinner-duration) linear infinite
var(--progress-dash-animation, );
}
@keyframes progress-spin {
to {
rotate: 1turn;
}
}
@keyframes progress-dash {
from {
--arc-start: 0deg;
--arc-end: 3deg;
}
50% {
--arc-start: 100deg;
--arc-end: 358deg;
}
to {
--arc-start: 360deg;
--arc-end: 363deg;
}
}
مراعاة الإعدادات المفضّلة للحركة
قد يجد المستخدمون الذين يعانون من حساسية الحركة أنّ العناصر التي تدور بسرعة تسبّب لهم الارتباك. يجب دائمًا الالتزام بالاستعلام عن الوسائط prefers-reduced-motion. اضبط السمة الداخلية --_used-spinner-duration لإلغاء قيمة --spinner-duration للمستخدم.
@media (prefers-reduced-motion: reduce) {
.loading-spinner {
/* Slow down the animation significantly rather than stopping it entirely,
so the user still knows that the process is active. */
--_used-spinner-duration: 6s;
}
}
استراتيجيات احتياطية
إذا كان @property متوافقًا، يتم تضمين الحركة المتقطعة تلقائيًا من خلال initial-value للسمة --progress-dash-animation. في المتصفّحات التي لا تتيح استخدام @property، يتم تجاهل تسجيل السمة، وتعود الحركة إلى دوران بسيط. لا يلزم استخدام JavaScript لهذا الإجراء الاحتياطي.
بالنسبة إلى المتصفّحات التي لا تتيح استخدام background-clip: border-area بعد، يمكنك استخدام mask-image لتفريغ الوسط.
/* Fallback: use mask-image to create the ring */
@supports not (background-clip: border-area) {
--clip-boundary: calc(100% - var(--thickness));
mask-image: radial-gradient(
farthest-side,
transparent var(--clip-boundary),
black var(--clip-boundary)
);
border: 0;
}