Stepper
کامپوننت Stepper برای نمایش پیشرفت در یک فرآیند چندمرحلهای استفاده میشود؛ مثل ثبتنام، تکمیل پروفایل یا checkout. کاربر میبیند الان در کدام مرحله است، چند مرحله را تمام کرده و چند مرحله باقی مانده.
استپر از دو قسمت تشکیل شده که هر کدام کار جداگانهای انجام میدهند:
Stepper— فقط نمایش است: نوار مراحل را با شماره، تیک مراحل تکمیلشده و هایلایت مرحلهی فعلی رسم میکند. خودش هیچ چیزی را به خاطر نمیسپارد.useStepper— فقط منطق است: نگه میدارد الان کدام مرحله فعال است، چه مراحلی تکمیل شده و توابع جابهجایی (goNext،goBackو ...) را میدهد.
این یعنی Stepper بدون useStepper کاری نمیکند — چیزی برای نمایش ندارد. این دو را همیشه با هم استفاده کن.
Import
import { Stepper, useStepper } from "fara-ui";
اگر هنوز FaraUI را نصب و راهاندازی نکردهای، ابتدا صفحهی شروع به کار را ببین.
Basic Usage
سادهترین حالت ممکن — یک استپر سهمرحلهای با دو دکمهی «قبلی» و «بعدی»:
import { Stepper, useStepper, Button } from "fara-ui";
function MyWizard() {
// ۱) مراحل را تعریف کن — هر آیتم حداقل یک label دارد
const steps = [{ label: "اطلاعات پایه" }, { label: "امنیت" }, { label: "تأیید نهایی" }];
// ۲) state و توابع جابهجایی را از هوک بگیر
const { activeStep, completedSteps, goNext, goBack, isFirstStep, isLastStep } = useStepper({
totalSteps: steps.length,
});
// ۳) به Stepper بده تا نمایش دهد + دکمهها برای جابهجایی
return (
<div>
<Stepper steps={steps} activeStep={activeStep} completedSteps={completedSteps} />
<div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
<Button variant="secondary" onClick={goBack} disabled={isFirstStep}>
مرحله قبل
</Button>
<Button onClick={goNext}>{isLastStep ? "پایان" : "مرحله بعد"}</Button>
</div>
</div>
);
}
بیایید قدمبهقدم ببینیم چه اتفاقی میافتد:
- تعریف مراحل — یک آرایهی ساده میسازی. ترتیب آرایه همان ترتیب نمایش است و اندیس هر مرحله از صفر شروع میشود (مرحلهی اول =
0). - فراخوانی هوک —
useStepper({ totalSteps: 3 })یک state داخلی میسازد. نیازی بهuseStateدستی نیست؛ خودش همهچیز را مدیریت میکند. - اتصال دو قسمت —
activeStepوcompletedStepsکه از هوک گرفتی را به کامپوننت پاس میدهی. هر وقتgoNextصدا زده شود، این مقادیر تغییر میکنند و استپر بهصورت خودکار دوباره رندر میشود. لازم نیست خودت کاری انجام دهی. - دکمهها —
goNextوgoBackرا مستقیم بهonClickدکمهها وصل میکنی. باisFirstStepوisLastStepدکمهها را در ابتدا و انتهای مسیر غیرفعال یا متن دکمه را مناسب میکنی.
دربارهی
completedSteps: این مقدار از نوعSet<number>است — یعنی مجموعهای از اندیسها بدون تکرار، مثلSet {0, 1}. لازم نیست با جزئیاتSetکار کنی؛ فقط آن را از هوک بگیر و همانطور بهStepperبده. خود هوک موقعgoNextاندیس مرحلهی فعلی را به این مجموعه اضافه میکند.
Playground
با دکمهها جابهجا شو و روی مراحل تکمیلشده (تیکدار) کلیک کن:
const { activeStep, completedSteps, goToStep } = useStepper({ totalSteps: 3 });
<Stepper
steps={steps}
activeStep={activeStep}
completedSteps={completedSteps}
onStepClick={goToStep}
/>
Real-World Example: Multi-Step Registration Form
مثال زیر یک فرم ثبتنام واقعی است — همان چیزی که احتمالاً میخواهی بسازی. این مثال را کامل بخوان چون همهی نکتههای مهم در آن هست: محتوای هر مرحله با content، اعتبارسنجی قبل از رفتن به مرحلهی بعد، پرش به مراحل قبلی با کلیک، و کاری که بعد از پایان باید انجام شود.
import { useState } from "react";
import { Stepper, useStepper, Button, Input, Textarea, Alert } from "fara-ui";
function RegistrationForm() {
// دادههای فرم — یک state برای همهی فیلدها
const [form, setForm] = useState({ name: "", email: "", password: "", bio: "" });
// خطاهای اعتبارسنجی هر مرحله
const [errors, setErrors] = useState({});
function setField(field) {
return (e) => {
setForm((f) => ({ ...f, [field]: e.target.value }));
setErrors((prev) => ({ ...prev, [field]: undefined })); // پاک کردن خطا هنگام تایپ
};
}
const steps = [
{
label: "اطلاعات پایه",
description: "نام و ایمیل",
content: (
<div style={{ display: "grid", gap: "12px" }}>
<label>
نام و نام خانوادگی
<Input value={form.name} onChange={setField("name")} error={Boolean(errors.name)} />
</label>
{errors.name && <span className="error-text">{errors.name}</span>}
<label>
ایمیل
<Input value={form.email} onChange={setField("email")} error={Boolean(errors.email)} />
</label>
{errors.email && <span className="error-text">{errors.email}</span>}
</div>
),
},
{
label: "امنیت",
description: "رمز عبور",
content: (
<Input
type="password"
value={form.password}
onChange={setField("password")}
error={Boolean(errors.password)}
/>
),
},
{
label: "درباره شما",
description: "معرفی کوتاه (اختیاری)",
content: <Textarea value={form.bio} onChange={setField("bio")} rows={4} />,
},
];
const { activeStep, completedSteps, goNext, goBack, goToStep, isFinished, isLastStep, reset } =
useStepper({
totalSteps: steps.length,
onFinish: () => console.log("ارسال به سرور:", form), // ← فقط یک بار، در پایان
});
// قبل از goNext چک کن که فیلدهای «همین مرحله» درست باشند
function validateStep() {
const next = {};
if (activeStep === 0) {
if (!form.name.trim()) next.name = "نام الزامی است.";
if (!form.email.includes("@")) next.email = "ایمیل معتبر وارد کن.";
}
if (activeStep === 1 && form.password.length < 6) {
next.password = "رمز عبور باید حداقل ۶ کاراکتر باشد.";
}
setErrors(next);
return Object.keys(next).length === 0; // یعنی خطایی نبود
}
function handleNext() {
if (!validateStep()) return; // اگر خطا داشت، جلو نرو
goNext();
}
return (
<div style={{ maxWidth: "560px" }}>
{/* onStepClick باعث میشود مراحل تکمیلشده قابل کلیک باشند */}
<Stepper
steps={steps}
activeStep={activeStep}
completedSteps={completedSteps}
onStepClick={goToStep}
/>
<div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
<Button variant="secondary" onClick={goBack} disabled={activeStep === 0}>
مرحله قبل
</Button>
<Button onClick={handleNext} disabled={isFinished}>
{isLastStep ? "پایان" : "مرحله بعد"}
</Button>
</div>
{isFinished && (
<div style={{ marginTop: "16px", display: "grid", gap: "12px" }}>
<Alert variant="success">ثبتنام کامل شد! خوش آمدی {form.name} 🎉</Alert>
<Button variant="secondary" onClick={reset}>
شروع مجدد
</Button>
</div>
)}
</div>
);
}
این هم نسخهی زندهی همان فرم — امتحانش کن (فیلد خالی بگذار تا خطا را ببینی):
نکتههای کلیدی این الگو:
- state فرم جدا از state استپر است.
formمال خود تو است وuseStepperفقط دربارهی «کدام مرحله» است. این دو به هم کاری ندارند. - اعتبارسنجی را قبل از
goNextانجام بده — داخلhandleNext. اگرgoNextرا مستقیم به دکمه بدهی، هیچوقت نمیتوانی جلوی رفتن به مرحلهی بعد را بگیری. onFinishفقط یک بار در پایان (یعنیgoNextروی آخرین مرحله) صدا زده میشود — جای درست برای ارسال دادهها به سرور.
Step Content (content)
هر مرحله میتواند content داشته باشد:
const steps = [
{ label: "اطلاعات پایه", content: <BasicInfoForm /> },
{ label: "امنیت", content: <SecurityForm /> },
];
نحوهی نمایش content به orientation بستگی دارد:
- افقی (پیشفرض) — محتوای مرحلهی فعلی زیر کل نوار مراحل رندر میشود.
- عمودی — محتوا داخل همان مرحلهی فعال، زیر عنوانش نمایش داده میشود.
پس نیازی به switch یا رندر شرطی دستی برای محتوای مراحل نداری؛ خود کامپوننت انجام میدهد.
Vertical Orientation (vertical)
اگر مراحل زیادند یا عنوانها بلند، حالت عمودی خواناتر است:
<Stepper
steps={steps}
activeStep={activeStep}
completedSteps={completedSteps}
orientation="vertical"
/>
useStepper
هوک useStepper تمام state لازم را مدیریت میکند:
| مقدار / تابع | نوع | Description |
|---|---|---|
activeStep | number | اندیس مرحلهی فعلی (از صفر) |
completedSteps | Set<number> | اندیس مراحل تکمیلشده |
isFinished | boolean | بعد از goNext در مرحلهی آخر true میشود |
isFirstStep / isLastStep | boolean | موقعیت فعلی |
goNext() | - | مرحلهی بعد؛ مرحلهی فعلی را تکمیلشده علامت میزند |
goBack() | - | مرحلهی قبلی |
goToStep(i) | - | پرش مستقیم — فقط به مراحل تکمیلشده یا فعلی |
reset() | - | بازگشت به ابتدای فرآیند |
const stepper = useStepper({
totalSteps: steps.length,
onFinish: () => submitForm(), // بعد از آخرین goNext صدا زده میشود
});
دو رفتار مهم که باید بدانی:
goBackمرحله را «تکمیلنشده» نمیکند. اگر از مرحلهی ۲ به ۱ برگردی، مرحلهی ۱ هنوز تیک دارد و باgoNextدوباره به ۲ میروی. یعنی برگشتن برای ویرایش است، نه شروع دوباره — معمولاً همان چیزی است که کاربر انتظار دارد.isFinishedفقط بعد از فراخوانیgoNextروی مرحلهی آخرtrueمیشود. پس اگر متن دکمهی ادامه را باisFinishedشرطی کنی، روی مرحلهی آخر دکمه هنوز «مرحله بعد» است در حالی که مرحلهی بعدی وجود ندارد. الگوی درست برای متن دکمه، استفاده ازisLastStepاست:<Button onClick={goNext} disabled={isFinished}>{isLastStep ? "پایان" : "مرحله بعد"}</Button>
Navigating Between Steps
goToStep (و onStepClick روی کامپوننت) فقط به مراحل تکمیلشده یا فعلی اجازهی پرش میدهد — کاربر نمیتواند از مراحل ناتمام رد شود و جلو بزند. مراحل تکمیلشده با آیکون ✓ نمایش داده میشوند.
برای فعالکردن کلیک روی مراحل، کافی است تابع goToStep را به onStepClick بدهی:
<Stepper
steps={steps}
activeStep={activeStep}
completedSteps={completedSteps}
onStepClick={goToStep}
/>
اگر onStepClick ندهی، هیچ مرحلهای قابل کلیک نیست و استپر صرفاً نمایشی است.
Common Mistakes
- فراموشکردن
activeStepیاcompletedSteps— این دو prop الزامیاند. اگر رد شوند، استپر نمیداند چه چیزی نمایش دهد. - صدا زدن
goNext()درonClick={() => goNext()}با پرانتز اشتباه — همیشهonClick={goNext}یاonClick={() => goNext()}؛ نوشتنonClick={goNext()}تابع را همان لحظهی رندر اجرا میکند. - گرفتن مقدار فرم داخل
onFinishبا closure قدیمی —onFinishرا همانجا در کامپوننت تعریف کن (مثل مثال بالا) تا همیشه آخرین state را ببیند. - انتظار داشتن ذخیرهی خودکار هر مرحله —
useStepperفقط جابهجایی را مدیریت میکند؛ ذخیرهی دادهها (اگر لازم است) کار خودت است.
Accessibility
- استپر با
role="list"و مراحل باrole="listitem"پیاده شدهاند. - مرحلهی فعال با
aria-current="step"علامت میخورد. - مراحل قابلکلیک بهصورت
buttonرندر میشوند و با کیبورد قابل استفادهاند. labelهر مرحله باید کوتاه و معنادار باشد؛descriptionبرای توضیح تکمیلی نمایش داده میشود.Stepperخودش دکمههای «بعدی» و «قبلی» یاaria-liveبرای تغییر محتوای مرحله تولید نمیکند؛ این کنترلها و اعلام وضعیت را در wrapper صفحهی خودت مدیریت کن.
Data Attributes and Customize CSS
Stepper hookهای پایدار زیر را ارائه میکند:
| Attribute | کاربرد |
|---|---|
data-fara-stepper | wrapper اصلی |
data-orientation | جهت: horizontal یا vertical |
data-fara-stepper-list | نوار مراحل با role="list" |
data-fara-stepper-step | هر مرحله |
data-active | مرحله یا دایرهی فعال |
data-completed | مرحله، دایره یا connector تکمیلشده |
data-fara-stepper-header | عنوان هر مرحله؛ button فقط در حالت قابلکلیک |
data-fara-stepper-circle | شماره یا دایرهی مرحله |
data-fara-stepper-check-icon | آیکون تیک مرحلهی تکمیلشده |
data-fara-stepper-text | ستون متن مرحله |
data-fara-stepper-label | label مرحله |
data-fara-stepper-description | description مرحله |
data-fara-stepper-connector | خط بین مراحل |
data-fara-stepper-content | محتوای مرحلهی فعال |
<Stepper
className="checkout-stepper"
steps={steps}
activeStep={activeStep}
completedSteps={completedSteps}
/>
[data-fara-stepper-list] {
gap: 16px;
}
[data-fara-stepper-step][data-active] [data-fara-stepper-label] {
color: #2563eb;
font-weight: 700;
}
[data-fara-stepper-circle][data-completed] {
background: #16a34a;
color: white;
}
[data-fara-stepper-connector][data-completed] {
background: #16a34a;
}
[data-fara-stepper][data-orientation="vertical"] [data-fara-stepper-content] {
margin-inline-start: 32px;
}
classNameرویdata-fara-stepper-listقرار میگیرد، نه روی wrapperdata-fara-stepper. برای استایلدادن به wrapper از selector[data-fara-stepper]استفاده کن.
Props — Stepper
| Prop | Type | Default | Description |
|---|---|---|---|
steps | StepperStep[] | - | تعریف مراحل (الزامی) |
activeStep | number | - | اندیس مرحلهی فعلی (الزامی) |
completedSteps | Set<number> | - | اندیس مراحل تکمیلشده (الزامی) |
onStepClick | (index: number) => void | - | با پاسدادن آن، مراحل تکمیلشده قابل کلیک میشوند |
orientation | "horizontal" | "vertical" | "horizontal" | جهت نمایش مراحل |
className | string | - | کلاس CSS اضافی روی نوار مراحل |
interface StepperStep {
label: string;
description?: string;
content?: ReactNode;
}