import { useState, Fragment } from "react";
import "./styles.css";
const Stepper = ({ steps, onFinish }) => {
const [active, setActive] = useState(0);
const total = steps.length;
const isLast = active === total - 1;
const goNext = () => {
const current = steps[active];
if (current?.validate && !current.validate()) return;
if (!isLast) setActive((s) => s + 1);
else onFinish?.();
};
const goBack = () => {
if (active > 0) setActive((s) => s - 1);
};
return (
<div className="stepperLayout">
<div className="stepperRail">
{steps.map((_, i) => {
const state =
i < active ? "completed" : i === active ? "active" : "upcoming";
return (
<Fragment key={i}>
<div className={`stepperDot ${state}`} />
{i !== steps.length - 1 && (
<div
className={`stepperLine ${i < active ? "completed" : ""}`}
/>
)}
</Fragment>
);
})}
</div>
<div className="stepperPanel">
<div className="stepperContent">{steps[active].content}</div>
<div className="stepperActions">
<button
type="button"
className="btn"
onClick={goBack}
disabled={active === 0}
>
Back
</button>
<button type="button" className="btn primary" onClick={goNext}>
{isLast ? "Submit" : "Next"}
</button>
</div>
</div>
</div>
);
};
const FormA = ({ name, setName, age, setAge }) => (
<form className="form" onSubmit={(e) => e.preventDefault()}>
<div className="formRow">
<label htmlFor="name" className="formLabel">
Name
</label>
<input
id="name"
className="formInput"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
</div>
<div className="formRow">
<label htmlFor="age" className="formLabel">
Age
</label>
<input
id="age"
className="formInput"
type="number"
value={age}
onChange={(e) => setAge(e.target.value)}
placeholder="Enter your age"
/>
</div>
</form>
);
const FormB = ({ email, setEmail }) => (
<form className="form" onSubmit={(e) => e.preventDefault()}>
<div className="formRow">
<label htmlFor="email" className="formLabel">
Email
</label>
<input
id="email"
className="formInput"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@company.com"
/>
</div>
</form>
);
const FormC = ({ notes, setNotes }) => (
<form className="form" onSubmit={(e) => e.preventDefault()}>
<div className="formRow">
<label htmlFor="notes" className="formLabel">
Notes
</label>
<input
id="notes"
className="formInput"
type="text"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Optional"
/>
</div>
</form>
);
export default function App() {
const [name, setName] = useState("");
const [age, setAge] = useState("");
const [email, setEmail] = useState("");
const [notes, setNotes] = useState("");
const steps = [
{
label: "Form 1",
content: (
<FormA name={name} setName={setName} age={age} setAge={setAge} />
),
validate: () => name.trim().length > 0 && Number(age) > 0,
},
{
label: "Form 2",
content: <FormB email={email} setEmail={setEmail} />,
validate: () => /\S+@\S+\.\S+/.test(email),
},
{
label: "Form 3",
content: <FormC notes={notes} setNotes={setNotes} />,
},
];
const handleFinish = () => {
alert(`Submitted: ${JSON.stringify({ name, age, email, notes }, null, 2)}`);
};
return (
<div className="App">
<h2>Vertical Stepper Form</h2>
<Stepper steps={steps} onFinish={handleFinish} />
</div>
);
}
.App {
font-family: sans-serif;
text-align: center;
padding: 24px;
}
.stepperLayout {
display: grid;
grid-template-columns: 48px 1fr;
gap: 16px;
align-items: start;
}
.stepperRail {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
margin-top: 8px;
}
.stepperDot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid #d0d5dd;
background: #fff;
}
.stepperDot.active {
border-color: #276ef1;
background: #276ef1;
}
.stepperDot.completed {
border-color: #276ef1;
background: #276ef1;
opacity: 0.8;
}
.stepperLine {
width: 2px;
height: 40px;
background: #e6e8eb;
}
.stepperLine.completed {
background: #276ef1;
}
.stepperPanel {
display: flex;
flex-direction: column;
gap: 16px;
}
.stepperContent {
flex: 1;
}
.stepperActions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.btn {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid #c5c7d0;
background: #fff;
cursor: pointer;
}
.btn.primary {
border-color: #276ef1;
background: #276ef1;
color: #fff;
}
/* Form styles */
.form {
background: #ffffff;
border-radius: 8px;
padding: 16px;
}
.formRow {
display: flex;
flex-direction: row;
gap: 6px;
margin-bottom: 12px;
align-items: center;
width: 100%;
}
.formLabel {
font-size: 14px;
color: #333;
width: 20%;
}
.formInput {
padding: 8px 10px;
border: 1px solid #c5c7d0;
border-radius: 6px;
font-size: 14px;
outline: none;
background: #fff;
width: 100%;
}
.formInput:focus {
border-color: #276ef1;
box-shadow: 0 0 0 3px rgba(39, 110, 241, 0.15);
}
.formInput::placeholder {
color: #9aa0a6;
}

Top comments (0)