UIDex

Steps / Stepper

مؤشّر الخطوات

aria-current="step"

also calledstepper، step indicator، progress steps، wizard steps، multi-step form، checkout steps، ستيبر، شريط الخطوات، مؤشّر المراحل، خطوات الإنجاز، الويزرد، نموذج متعدّد الخطوات

A stepper is a row of named stages that splits one long task (signing up, checking out, uploading documents) into an ordered sequence, and shows the user where they are and how much is left. What defines it is that the order is enforced and the end is known: the task is finished at the last stage. It is not a breadcrumb trail, because breadcrumbs describe your position in a site hierarchy, not a journey with a start and a finish. Nor is it a progress bar: a progress bar is a percentage with no named stages, while every step here carries a name that says what happens in it. Tabs differ too, because tabs are peers at one level that open in any order, whereas step 4 means nothing before step 3. Pagination is a different thing again, one that walks equal slices of a single collection with nothing to complete. One naming trap: "stepper" is also what many libraries call the small numeric field with + and − buttons, a completely unrelated control.

If you called it…

"the numbered circles above the form""the step 2 of 4 thing""the circles that turn into checkmarks""the line connecting the stages at checkout""the bar at the top of a multi-page signup"

Live specimen

Interact with the demo. Every part is real and numbered.

الخطوة 2 من 4 · الشحن

RTLالخطوة الأولى تبدأ من اليمين، وعلامة الصح لا تنعكس

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a horizontal four-stage stepper. Derive every visual state from one zero-based currentStep index: a step is completed when index < currentStep, current when index === currentStep, and upcoming when it is greater. Do not keep separate isDone and isActive flags. Structure it as an <ol> with an aria-label; each <li> holds an indicator circle marked aria-hidden="true" showing the number or a checkmark, a short label, and visually hidden text naming the state. Put aria-current="step" on the current item only, make completed steps buttons the user can go back to, and mark upcoming ones disabled. Render the connector as an aria-hidden element whose fill animates inline-size from 0 to 100%, not scaleX. Use logical properties everywhere (margin-inline, padding-inline, inset-inline-start, border-inline-start) and never write left, right, or margin-left. Generate all numerals from a single Intl.NumberFormat so Latin and Arabic-Indic digits never mix, and never mirror the checkmark in RTL: scope scaleX(-1) to the navigation chevrons. Validate each stage before allowing the move forward, and keep the user's input when they step back.

How it behaves right-to-left

This section is ours alone.

Sequence direction

mirrors

The whole sequence mirrors: step 1 sits at the far right and the last step ends at the left. A flex-direction: row container does this by itself, since the inline axis follows direction. Never add flex-direction: row-reverse under [dir="rtl"], because that double-flips and drops step 1 back on the left. Any connector pinned with position: absolute and left: 50% has to become inset-inline-start: 50%.

How the connector fills

mirrors

The fill grows from the logical start of the journey, which in Arabic is the right. Implement it as an animated inline-size from 0 to 100% and it mirrors for free, because the fill element is laid out against its parent's logical start edge. transform: scaleX() is physical instead: transform-origin does not accept inline-start at all, and its only values are left/right/percentages. Set transform-origin: right under [dir="rtl"]. The same holds for linear-gradient(to right, …): gradients have no logical keywords, so flip it to to left.

The numerals in the circles

never mirrors

Digits do not mirror and do not need <bdi>: 0-9 carry the bidi class EN, so they always render left-to-right and "10" stays "10" inside an Arabic line, never "01". The decision to make is the numbering system: Latin 1 2 3 or Arabic-Indic ١ ٢ ٣, and never a mix like "خطوة ٢ من 4". Produce both the circle number and the counter text from one source: Intl.NumberFormat("ar-EG") for Arabic-Indic, Intl.NumberFormat("ar-EG-u-nu-latn") for Latin. If you number the circles from CSS, the equivalent is counter(step, arabic-indic). Add font-variant-numeric: tabular-nums so the text stops jumping past ten.

The checkmark never mirrors

never mirrors

A checkmark is not a directional icon: the Bidi_Mirrored property of ✓ is No, and its meaning does not change with page direction. A blanket [dir="rtl"] svg { transform: scaleX(-1) } breaks it and makes it look drawn backwards. Scope the flip to a class such as .icon-directional and put that only on the next/previous chevrons. The same exemption covers the lock icon and the spinner inside a step that is still processing.

The vertical stepper on mobile

mirrors

When the steps stack vertically the block axis does not change: step 1 stays at the top in both languages. The rail is what moves, from the left of the text to its right, so use border-inline-start instead of border-left, inset-inline-start instead of left, and padding-inline-start to push the text clear of the circles. A hard-coded left value here leaves the rail running straight through the Arabic step names.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIAaria-current="step"The one marker that names the active stage. It belongs on a single element only.
HTML<ol> + <li>An ordered list, because the sequence is part of the meaning, not a row of divs.
Material UI<Stepper activeStep={1}>One zero-based index from which every <Step> derives its state; orientation="vertical" for mobile.
Ant Design<Steps current={1} items={…} />Adds status="error" for a stage that failed validation. Most hand-rolled versions forget that fourth state.
Chakra UIuseSteps({ index, count })The hook returns activeStep and setActiveStep, so the source of truth stays a single number.
shadcn/uidata-[state=completed]:No ready-made Stepper in the registry; you compose one from an <ol> plus a data-state attribute derived from the index.
CSScounter(step, arabic-indic)Numbers the circles from CSS with an explicit numeral system instead of hard-coding digits in the markup.

See also

Updated · 2026-08-19