پرش به مطلب اصلی

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>
);
}

بیایید قدم‌به‌قدم ببینیم چه اتفاقی می‌افتد:

  1. تعریف مراحل — یک آرایه‌ی ساده می‌سازی. ترتیب آرایه همان ترتیب نمایش است و اندیس هر مرحله از صفر شروع می‌شود (مرحله‌ی اول = 0).
  2. فراخوانی هوک — useStepper({ totalSteps: 3 }) یک state داخلی می‌سازد. نیازی به useState دستی نیست؛ خودش همه‌چیز را مدیریت می‌کند.
  3. اتصال دو قسمت — activeStep و completedSteps که از هوک گرفتی را به کامپوننت پاس می‌دهی. هر وقت goNext صدا زده شود، این مقادیر تغییر می‌کنند و استپر به‌صورت خودکار دوباره رندر می‌شود. لازم نیست خودت کاری انجام دهی.
  4. دکمه‌ها — goNext و goBack را مستقیم به onClick دکمه‌ها وصل می‌کنی. با isFirstStep و isLastStep دکمه‌ها را در ابتدا و انتهای مسیر غیرفعال یا متن دکمه را مناسب می‌کنی.

درباره‌ی completedSteps: این مقدار از نوع Set<number> است — یعنی مجموعه‌ای از اندیس‌ها بدون تکرار، مثل Set {0, 1}. لازم نیست با جزئیات Set کار کنی؛ فقط آن را از هوک بگیر و همان‌طور به Stepper بده. خود هوک موقع goNext اندیس مرحله‌ی فعلی را به این مجموعه اضافه می‌کند.

Playground​

با دکمه‌ها جابه‌جا شو و روی مراحل تکمیل‌شده (تیک‌دار) کلیک کن:

2امنیترمز عبور
3تأیید نهاییبازبینی اطلاعات
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>
);
}

این هم نسخه‌ی زنده‌ی همان فرم — امتحانش کن (فیلد خالی بگذار تا خطا را ببینی):

2امنیترمز عبور
3درباره شمامعرفی کوتاه (اختیاری)

نکته‌های کلیدی این الگو:

  • 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"
/>
1آدرس تحویلکجا سفارش را بفرستیم؟
2پرداختاطلاعات کارت
3تأیید سفارشبازبینی نهایی

useStepper​

هوک useStepper تمام state لازم را مدیریت می‌کند:

مقدار / تابعنوعDescription
activeStepnumberاندیس مرحله‌ی فعلی (از صفر)
completedStepsSet<number>اندیس مراحل تکمیل‌شده
isFinishedbooleanبعد از goNext در مرحله‌ی آخر true می‌شود
isFirstStep / isLastStepbooleanموقعیت فعلی
goNext()-مرحله‌ی بعد؛ مرحله‌ی فعلی را تکمیل‌شده علامت می‌زند
goBack()-مرحله‌ی قبلی
goToStep(i)-پرش مستقیم — فقط به مراحل تکمیل‌شده یا فعلی
reset()-بازگشت به ابتدای فرآیند
const stepper = useStepper({
totalSteps: steps.length,
onFinish: () => submitForm(), // بعد از آخرین goNext صدا زده می‌شود
});

دو رفتار مهم که باید بدانی:

  1. goBack مرحله را «تکمیل‌نشده» نمی‌کند. اگر از مرحله‌ی ۲ به ۱ برگردی، مرحله‌ی ۱ هنوز تیک دارد و با goNext دوباره به ۲ می‌روی. یعنی برگشتن برای ویرایش است، نه شروع دوباره — معمولاً همان چیزی است که کاربر انتظار دارد.
  2. isFinished فقط بعد از فراخوانی goNext روی مرحله‌ی آخر true می‌شود. پس اگر متن دکمه‌ی ادامه را با isFinished شرطی کنی، روی مرحله‌ی آخر دکمه هنوز «مرحله بعد» است در حالی که مرحله‌ی بعدی وجود ندارد. الگوی درست برای متن دکمه، استفاده از isLastStep است:
<Button onClick={goNext} disabled={isFinished}>
{isLastStep ? "پایان" : "مرحله بعد"}
</Button>

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-stepperwrapper اصلی
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-labellabel مرحله
data-fara-stepper-descriptiondescription مرحله
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 قرار می‌گیرد، نه روی wrapper data-fara-stepper. برای استایل‌دادن به wrapper از selector [data-fara-stepper] استفاده کن.

Props — Stepper​

PropTypeDefaultDescription
stepsStepperStep[]-تعریف مراحل (الزامی)
activeStepnumber-اندیس مرحله‌ی فعلی (الزامی)
completedStepsSet<number>-اندیس مراحل تکمیل‌شده (الزامی)
onStepClick(index: number) => void-با پاس‌دادن آن، مراحل تکمیل‌شده قابل کلیک می‌شوند
orientation"horizontal" | "vertical""horizontal"جهت نمایش مراحل
classNamestring-کلاس CSS اضافی روی نوار مراحل
interface StepperStep {
label: string;
description?: string;
content?: ReactNode;
}