Progress Bar vs Spinner vs Ring
مؤشّر التقدّمrole="progressbar"
also calledprogress bar، spinner، loader، progress ring، activity indicator، loading indicator، شريط التقدّم، بروجرس بار، سبينر، مؤشّر التحميل، حلقة التقدّم، لودر
A progress indicator tells the user that an operation is running, and how much of it is left when that is known. Three shapes cover two meanings. A determinate bar knows the fraction and shows it; a spinner does not know it and says only that something is happening; a progress ring is the determinate bar wrapped into a circle, a visual choice that changes nothing in the semantics or in ARIA. Which one to reach for is a question of time: show nothing for the first 400ms or so, because an indicator that flashes and vanishes reads as a glitch; a spinner is enough up to about four seconds; past that people want a determinate bar with a percentage; and when the work cannot be measured, name the current step instead of inventing a number. This is not a skeleton, which draws the shape of the content that is coming so the layout does not jump, while a progress indicator measures time. It is not a stepper either, because a stepper is navigation the user can move through and a progress indicator is a readout of work the user does not control. And it is not a slider: the moment the value can be dragged you have a slider whatever it looks like, since progressbar takes no focus and accepts no input.
If you called it…
Live specimen
Interact with the demo. Every part is real and numbered.
aria-valuenow="5"يتحرّك كل ٥٪ لا مع كل إطارAnatomy: every part, named
Hover a row to locate it in the demo above.
Build prompt
How it behaves right-to-left
This section is ours alone.
Which way the bar fills
mirrorsThe bar fills from the logical start, which in Arabic is the right edge. The reliable build is a fill element pinned with inset-inline-start: 0 whose inline-size animates from 0% to the value: it mirrors by itself, because the edge it is anchored to is the one that moved. If you insist on scaleX, note that transform-origin has no logical keyword at all; its keywords are the physical left, right, top, bottom and center alongside lengths and percentages, so you end up writing transform-origin: right by hand under [dir="rtl"], or signing the scale with a direction variable.
The indeterminate sweep
mirrorsAn indeterminate bar slides a short segment across the track, and setting off from the logical start is what matches the reading order. That motion lives in a @keyframes block written with translateX, and transform is physical with no logical counterpart anywhere in CSS, so fixed keyframes send the segment crawling against the reading direction in Arabic. Multiply the offset by a sign variable, translateX(calc(var(--sweep) * var(--dir))) with --dir set to -1 under [dir="rtl"], or ship a second @keyframes rule scoped to [dir="rtl"].
The spinner rotation
never mirrorsRotation is not a reading direction, so the spinner stays exactly as it is when the page flips. A positive angle in rotate() turns clockwise under both directions, and a blanket [dir="rtl"] svg { transform: scaleX(-1) } reverses it to counter-clockwise while moving the rounded stroke-linecap to the other end of the arc, so the tail of the arc now leads. Scope any mirroring to a class you put on directional icons only, and keep the spinner and the clock glyph out of it.
Ring geometry against its label
never mirrorsThe ring is drawn in the SVG coordinate system, where the direction property has no effect on geometry. The start point of stroke-dashoffset therefore stays put: the equivalent path of a <circle> begins at the rightmost point and runs clockwise, and the rotate(-90) that moves the start to the top is a fixed visual decision with no language in it. The number in the middle is the part that does react. Plain HTML text follows page direction, and if you draw it with SVG <text> instead, text-anchor start and end resolve against the inline direction of that text and swap in RTL, so use text-anchor: middle or overlay an HTML element on the ring.
Numerals and numbering system
never mirrorsDigits do not reverse: the sequence inside a number reads left to right even in Arabic text, and the bidi algorithm treats the run as one unit. The real bug is mixing the two systems on one screen, a bar reading 42% beside a ring reading ٤٢٪. Generate every numeral from one place: new Intl.NumberFormat("ar-EG", { style: "percent" }) returns Arabic-Indic digits with the Arabic percent sign U+066A, while ar-EG-u-nu-latn returns Latin 0 to 9 with an ASCII %, because the percent symbol follows the numbering system and not the language. Add font-variant-numeric: tabular-nums so the number holds its width as it counts and nothing beside it twitches.
In code
Each row is one framework's word for the same thing. Take the row your project speaks.
See also
Updated · 2026-08-19