{"id":135297,"date":"2025-11-07T07:48:23","date_gmt":"2025-11-07T07:48:23","guid":{"rendered":"https:\/\/phppot.com\/?p=24547"},"modified":"2025-11-07T07:48:23","modified_gmt":"2025-11-07T07:48:23","slug":"build-a-multi-step-form-in-react-with-validation-and-progress-bar","status":"publish","type":"post","link":"https:\/\/sickgaming.net\/blog\/2025\/11\/07\/build-a-multi-step-form-in-react-with-validation-and-progress-bar\/","title":{"rendered":"Build a Multi-Step Form in React with Validation and Progress Bar"},"content":{"rendered":"<div class=\"modified-on\" readability=\"7.1489361702128\"> by <a href=\"https:\/\/phppot.com\/about\/\">Vincy<\/a>. Last modified on November 18th, 2025.<\/div>\n<p>A multi-step form is one of the best ways to replace a long form to make the customer feel easy. Example: a student enrolment form will usually be very long. If it is partitioned into <a href=\"https:\/\/phppot.com\/php\/wizard-form\/\">multi-steps with section-wise sub forms<\/a>, it encourages enduser to proceed forward. And importantly the merit is that it will increase your signup rate.<\/p>\n<p>In this React tutorial, a registration form is partitioned into 4 steps. Those are to collect general, contact, personal, and authentication details from the users. Each step loads a sub-form with corresponding sections. Each subform is a separate component with proper structure and easy maintainability.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24678\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-multi-step-form-validation-progress-bar-550x288.jpeg\" alt=\"React Multi Step Form Validation Progress Bar\" width=\"550\" height=\"288\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-multi-step-form-validation-progress-bar-550x288.jpeg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-multi-step-form-validation-progress-bar-300x157.jpeg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-multi-step-form-validation-progress-bar-768x402.jpeg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-multi-step-form-validation-progress-bar.jpeg 1200w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<h2>Rendering multi-step registration form<\/h2>\n<p>This <code>RegisterForm<\/code> is created as a parent <a href=\"https:\/\/phppot.com\/react\/react-hook-user-registration-form\/\">React Form component<\/a>. It loads all the sub-components created for rendering a multi-step form with validation and a progress bar.<\/p>\n<p>It requires the following custom React component created for this example.<\/p>\n<ol>\n<li>GeneralInfo \u2013 to collect basic information, first and last names.<\/li>\n<li>ContactInfo \u2013 to collect phone or WhatsApp numbers.<\/li>\n<li>PersonalInfo \u2013 to collect a person\u2019s date of birth and gender.<\/li>\n<li>ConfirmInfo \u2013 is a last step to register confidential information and confirm registration.<\/li>\n<\/ol>\n<p>All information is stored in the formData by using the corresponding handleChange hook.<\/p>\n<p>Additionally, this JSX has a Toast container to display success or error responses on the user-entered data.<\/p>\n<p>There is a step navigation interface that helps to move along the registration steps. The step navigation helps to verify the data before clicking confirmation.<\/p>\n<p class=\"code-heading\"><code>src\/components\/RegisterForm.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">import { useState } from \"react\";\nimport { ToastContainer } from \"react-toastify\";\nimport \"react-toastify\/dist\/ReactToastify.css\";\nimport ProgressBar from \".\/ProgressBar\";\nimport GeneralInfo from \".\/FormSteps\/GeneralInfo\";\nimport ContactInfo from \".\/FormSteps\/ContactInfo\";\nimport PersonalInfo from \".\/FormSteps\/PersonalInfo\";\nimport Confirmation from \".\/FormSteps\/Confirmation\";\nimport \"..\/..\/public\/assests\/css\/RegisterForm.css\";\nconst RegisterForm = () =&gt; { const [step, setStep] = useState(1); const [formData, setFormData] = useState({ first_name: \"\", last_name: \"\", email: \"\", phone: \"\", dob: \"\", gender: \"\", username: \"\", password: \"\", terms: false, }); const nextStep = () =&gt; setStep(prev =&gt; prev + 1); const prevStep = () =&gt; setStep(prev =&gt; prev - 1); const handleChange = (e) =&gt; { const { name, value, type, checked } = e.target; setFormData({ ...formData, [name]: type === \"checkbox\" ? checked : value, }); };\nreturn (\n&lt;div className=\"container\"&gt; &lt;header&gt;Register With Us&lt;\/header&gt; &lt;ProgressBar step={step} \/&gt; &lt;div className=\"form-outer\"&gt; {step === 1 &amp;&amp; &lt;GeneralInfo formData={formData} handleChange={handleChange} nextStep={nextStep} \/&gt;} {step === 2 &amp;&amp; &lt;ContactInfo formData={formData} handleChange={handleChange} nextStep={nextStep} prevStep={prevStep} \/&gt;} {step === 3 &amp;&amp; &lt;PersonalInfo formData={formData} handleChange={handleChange} nextStep={nextStep} prevStep={prevStep} \/&gt;} {step === 4 &amp;&amp; &lt;Confirmation formData={formData} handleChange={handleChange} prevStep={prevStep} setFormData={setFormData} setStep={setStep} \/&gt;} &lt;\/div&gt; &lt;ToastContainer position=\"top-center\" autoClose={3000} hideProgressBar={false} newestOnTop closeOnClick pauseOnHover\/&gt;\n&lt;\/div&gt;\n);\n};\nexport default RegisterForm;\n<\/code><\/pre>\n<h2>Form progress bar with numbered in-progress state of registration<\/h2>\n<p>When a multi-step form interface is used, the progress bar and <a href=\"https:\/\/phppot.com\/php\/how-to-add-pagination-in-php-with-mysql\/\">prev-next navigation<\/a> controls are very important usability.<\/p>\n<p>This example provides both of these controls which will be useful to learn how to make this for other similar cases.<\/p>\n<p>The progress bar contains circled, numbered nodes represent each step. This node is a container that denotes the title and the step number. It checks the useState for the current step and highlights the node accordingly.<\/p>\n<p>The conditional statements load the CSS className \u2018active\u2019 dynamically when loading the progress bar to the UI.<\/p>\n<p>All the completed steps are highlighted by a filled background and shows clarity on the current state.<\/p>\n<p class=\"code-heading\"><code>src\/components\/ProgressBar.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">const ProgressBar = ({ step }) =&gt; {\nreturn (\n&lt;div className=\"progress-bar\"&gt; &lt;div className={`step ${step &gt;= 1 ? \"active\" : \"\"}`}&gt; &lt;p&gt;General&lt;\/p&gt; &lt;div className={`bullet ${step &gt; 1 ? \"active\" : \"\"}`}&gt; &lt;span className=\"black-text\"&gt;1&lt;\/span&gt; &lt;\/div&gt; &lt;\/div&gt; &lt;div className={`step ${step &gt;= 2 ? \"active\" : \"\"}`}&gt; &lt;p&gt;Contact&lt;\/p&gt; &lt;div className={`bullet ${step &gt; 2 ? \"active\" : \"\"}`}&gt; &lt;span className=\"black-text\"&gt;2&lt;\/span&gt; &lt;\/div&gt; &lt;\/div&gt; &lt;div className={`step ${step &gt;= 3 ? \"active\" : \"\"}`}&gt; &lt;p&gt;Personal&lt;\/p&gt; &lt;div className={`bullet ${step &gt; 3 ? \"active\" : \"\"}`}&gt; &lt;span className=\"black-text\"&gt;3&lt;\/span&gt; &lt;\/div&gt; &lt;\/div&gt; &lt;div className={`step ${step &gt;= 4 ? \"active\" : \"\"}`}&gt; &lt;p&gt;Confirm&lt;\/p&gt; &lt;div className=\"bullet\"&gt; &lt;span className=\"black-text\"&gt;4&lt;\/span&gt; &lt;\/div&gt; &lt;\/div&gt;\n&lt;\/div&gt;\n);\n};\nexport default ProgressBar;\n<\/code><\/pre>\n<h2>React Form components collecting types of user information<\/h2>\n<p>We have seen all 4 sub-form components created for this React example. Those component purposes are described in the explanation of the parent React container.<\/p>\n<p>Each form component accepts the formData, handleChange, nextStep references. The parent component has the scope of reading all the sub-form field data. It supplies the data with the corresponding handleChange hook to each step.<\/p>\n<p>The main RegisterForm JSX contains conditional statements to check the current step. Then, it load the corresponding sub form components based on the in-progressing step managed in a React useState.<\/p>\n<h3>Step 1 \u2013 Collecting general information<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24551\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-registered-multi-step-form-550x547.jpg\" alt=\"react registered multi step form\" width=\"550\" height=\"547\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-registered-multi-step-form-550x547.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-registered-multi-step-form-150x150.jpg 150w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-registered-multi-step-form-768x764.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-registered-multi-step-form.jpg 833w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<p class=\"code-heading\"><code>src\/components\/FormSteps\/GeneralInfo.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">import { useState } from \"react\";\nconst GeneralInfo = ({ formData, handleChange, nextStep }) =&gt; { const [errors, setErrors] = useState({}); const validate = () =&gt; { const newErrors = {}; if (!formData.first_name.trim()) newErrors.first_name = \"First name is required\"; if (!formData.last_name.trim()) newErrors.last_name = \"Last name is required\"; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; return ( &lt;div className=\"page slidepage\"&gt; &lt;div className=\"title\"&gt;General Information&lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;First Name&lt;\/div&gt; &lt;input type=\"text\" name=\"first_name\" value={formData.first_name} onChange={handleChange} className={errors.first_name ? \"is-invalid\" : \"\"} \/&gt; {errors.first_name &amp;&amp; &lt;div className=\"ribbon-alert\"&gt;{errors.first_name}&lt;\/div&gt;} &lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;Last Name&lt;\/div&gt; &lt;input type=\"text\" name=\"last_name\" value={formData.last_name} onChange={handleChange} className={errors.last_name ? \"is-invalid\" : \"\"} \/&gt; {errors.last_name &amp;&amp; &lt;div className=\"ribbon-alert\"&gt;{errors.last_name}&lt;\/div&gt;} &lt;\/div&gt; &lt;div className=\"field nextBtn\"&gt; &lt;button type=\"button\" onClick={() =&gt; validate() &amp;&amp; nextStep()}&gt; Continue &lt;\/button&gt; &lt;\/div&gt; &lt;\/div&gt; );\n};\nexport default GeneralInfo;\n<\/code><\/pre>\n<h3>Step 2: Collecting contact information<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24553\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-contact-info-form-550x496.jpg\" alt=\"React Contact Info Form\" width=\"550\" height=\"496\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-contact-info-form-550x496.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-contact-info-form-300x271.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-contact-info-form-768x693.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-contact-info-form.jpg 1017w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<p class=\"code-heading\"><code>src\/components\/FormSteps\/ContactInfo.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">import { useState } from \"react\";\nconst ContactInfo = ({ formData, handleChange, nextStep, prevStep }) =&gt; { const [errors, setErrors] = useState({}); const validate = () =&gt; { const newErrors = {}; const emailRegex = \/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\/; if (!formData.email.trim()) newErrors.email = \"Email is required\"; else if (!emailRegex.test(formData.email)) newErrors.email = \"Enter a valid email address\"; if (formData.phone.length &lt; 10) newErrors.phone = \"Phone number must be at least 10 digits\"; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; return ( &lt;div className=\"page\"&gt; &lt;div className=\"title\"&gt;Contact Information&lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;Email Address&lt;\/div&gt; &lt;input type=\"text\" name=\"email\" value={formData.email} onChange={handleChange} className={errors.email ? \"is-invalid\" : \"\"} \/&gt; {errors.email &amp;&amp; &lt;div className=\"ribbon-alert\"&gt;{errors.email}&lt;\/div&gt;} &lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;WhatsApp Number&lt;\/div&gt; &lt;input type=\"number\" name=\"phone\" value={formData.phone} onChange={handleChange} className={errors.phone ? \"is-invalid\" : \"\"} \/&gt; {errors.phone &amp;&amp; &lt;div className=\"ribbon-alert\"&gt;{errors.phone}&lt;\/div&gt;} &lt;\/div&gt; &lt;div className=\"field btns\"&gt; &lt;button type=\"button\" onClick={prevStep}&gt;Back&lt;\/button&gt; &lt;button type=\"button\" onClick={() =&gt; validate() &amp;&amp; nextStep()}&gt;Continue&lt;\/button&gt; &lt;\/div&gt; &lt;\/div&gt; );\n};\nexport default ContactInfo;\n<\/code><\/pre>\n<h3>Step3 \u2013 Collecting personal information<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24555\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-personal-info-form-550x500.jpg\" alt=\"react personal info form\" width=\"550\" height=\"500\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-personal-info-form-550x500.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-personal-info-form-300x273.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-personal-info-form-768x698.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-personal-info-form.jpg 1003w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<p class=\"code-heading\"><code>src\/components\/FormSteps\/PersonalInfo.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">import { useState } from \"react\";\nconst PersonalInfo = ({ formData, handleChange, nextStep, prevStep }) =&gt; { const [errors, setErrors] = useState({}); const validate = () =&gt; { const newErrors = {}; if (!formData.dob) newErrors.dob = \"Please select your date of birth\"; if (!formData.gender) newErrors.gender = \"Please select your gender\"; setErrors(newErrors); return Object.keys(newErrors).length === 0; };\nreturn ( &lt;div className=\"page\"&gt; &lt;div className=\"title\"&gt;Personal Information&lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;DOB&lt;\/div&gt; &lt;input type=\"date\" name=\"dob\" value={formData.dob} onChange={handleChange} className={errors.dob ? \"is-invalid\" : \"\"} \/&gt; {errors.dob &amp;&amp; &lt;div className=\"ribbon-alert\"&gt;{errors.dob}&lt;\/div&gt;} &lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;Gender&lt;\/div&gt; &lt;select name=\"gender\" value={formData.gender} onChange={handleChange} className={errors.gender ? \"is-invalid\" : \"\"} &gt; &lt;option value=\"\"&gt;Select Gender&lt;\/option&gt; &lt;option&gt;Male&lt;\/option&gt; &lt;option&gt;Female&lt;\/option&gt; &lt;option&gt;Other&lt;\/option&gt; &lt;\/select&gt; {errors.gender &amp;&amp; &lt;div className=\"ribbon-alert\"&gt;{errors.gender}&lt;\/div&gt;} &lt;\/div&gt; &lt;div className=\"field btns\"&gt; &lt;button type=\"button\" onClick={prevStep}&gt;Back&lt;\/button&gt; &lt;button type=\"button\" onClick={() =&gt; validate() &amp;&amp; nextStep()}&gt;Continue&lt;\/button&gt; &lt;\/div&gt; &lt;\/div&gt;\n);\n};\nexport default PersonalInfo;\n<\/code><\/pre>\n<h3>Step 4 \u2013 Collecting user consent and confidential information<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24556\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-confirm-info-form-550x502.jpg\" alt=\"react confirm info form\" width=\"550\" height=\"502\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-confirm-info-form-550x502.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-confirm-info-form-300x274.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-confirm-info-form-768x701.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-confirm-info-form.jpg 979w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<p class=\"code-heading\"><code>src\/components\/FormSteps\/Confirmation.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">import { useState } from \"react\";\nimport { toast } from \"react-toastify\";\nimport \"react-toastify\/dist\/ReactToastify.css\";\nimport axios from \"axios\";\nimport SERVER_SIDE_API_ROOT from \"..\/..\/config\";\nconst Confirmation = ({ formData, handleChange, prevStep, setFormData, setStep }) =&gt; { const [errors, setErrors] = useState({}); const handleSubmit = async (e) =&gt; { e.preventDefault(); const newErrors = {}; if (!formData.username) newErrors.username = \"Username is required\"; if (!formData.password) newErrors.password = \"Password is required\"; else if (formData.password.length &lt; 6) newErrors.password = \"Password must be at least 6 characters\"; if (!formData.terms) newErrors.terms = \"You must agree to the terms\"; setErrors(newErrors); if (Object.keys(newErrors).length &gt; 0) return; try { const res = await axios.post(`${SERVER_SIDE_API_ROOT}\/multi-step-form.php`, formData); if (res.data.success) { toast.success(res.data.message || \"User registered successfully!\"); setFormData({ first_name: \"\", last_name: \"\", email: \"\", phone: \"\", dob: \"\", gender: \"\", username: \"\", password: \"\", terms: false, }); setStep(1); setErrors({}); } else { toast.error(res.data.message || \"Registration failed!\"); } } catch (err) { console.error(err); toast.error(\"Error while saving user data.\"); } }; const renderError = (field) =&gt; errors[field] ? &lt;div className=\"ribbon-alert\"&gt;{errors[field]}&lt;\/div&gt; : null;\nreturn ( &lt;div className=\"page\"&gt; &lt;div className=\"title\"&gt;Confirm&lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;Username&lt;\/div&gt; &lt;input type=\"text\" name=\"username\" value={formData.username} onChange={handleChange} className={errors.username ? \"is-invalid\" : \"\"} \/&gt; {renderError(\"username\")} &lt;\/div&gt; &lt;div className=\"field\"&gt; &lt;div className=\"label\"&gt;Password&lt;\/div&gt; &lt;input type=\"password\" name=\"password\" value={formData.password} onChange={handleChange} className={errors.password ? \"is-invalid\" : \"\"} \/&gt; {renderError(\"password\")} &lt;\/div&gt; &lt;div className=\"field-terms\"&gt; &lt;label&gt; &lt;input type=\"checkbox\" name=\"terms\" checked={formData.terms} onChange={handleChange} \/&gt;{\" \"} I agree with the terms. &lt;\/label&gt; {renderError(\"terms\")} &lt;\/div&gt; &lt;div className=\"field btns\"&gt; &lt;button type=\"button\" onClick={prevStep}&gt;Back&lt;\/button&gt; &lt;button type=\"submit\" onClick={handleSubmit}&gt;Register&lt;\/button&gt; &lt;\/div&gt; &lt;\/div&gt;\n);\n};\nexport default Confirmation;\n<\/code><\/pre>\n<h2>PHP endpoint processing multi-step form data<\/h2>\n<p>It is a usual PHP file which not need to describe if you are already familiar with how the <a href=\"https:\/\/phppot.com\/php\/user-registration-in-php-with-login-form-with-mysql-and-code-download\/\">PHP user registration<\/a> works. It reads the form data posted by the front-end multi-step React form.<\/p>\n<p>With this form data, it builds the database insert query to save the user-entered information to the backend.<\/p>\n<p>This example has the server-side validation for a few fields. If the validation process catches any problem with the submitted data, then it composes an error response to the React frontend.<\/p>\n<p>Mainly, it validates <a href=\"https:\/\/phppot.com\/javascript\/javascript-validate-email-regex\/\">email format<\/a> and <a href=\"https:\/\/phppot.com\/php\/php-password-validation\/\">password-strength<\/a> (minimally by its length). Password strength checking has no limitations. Based on the application sensitivity we are free to add as much validation as possible which is good for a security point of view.<\/p>\n<p><strong>Note:&nbsp;<\/strong>The SQL script for the user database is in the downloadable source code attached with this tutorial in <code>multi-step-form-validation-api\/users.sql<\/code>.<\/p>\n<p class=\"code-heading\"><code>multi-step-form-validation-api\/multi-step-form.php<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">&lt;?php\nheader(\"Access-Control-Allow-Origin: *\");\nheader(\"Access-Control-Allow-Headers: Content-Type\");\nheader(\"Access-Control-Allow-Methods: POST\");\nheader(\"Content-Type: application\/json\");\ninclude 'db.php';\n$data = json_decode(file_get_contents(\"php:\/\/input\"), true);\n$firstName = $data[\"first_name\"] ?? \"\";\n$lastName = $data[\"last_name\"] ?? \"\";\n$email = $data[\"email\"] ?? \"\";\n$phone = $data[\"phone\"] ?? \"\";\n$dob = $data[\"dob\"] ?? \"\";\n$gender = $data[\"gender\"] ?? \"\";\n$username = $data[\"username\"] ?? \"\";\n$password = $data[\"password\"] ?? \"\";\nif (!$firstName || !$email || !$password) { echo json_encode([\"success\" =&gt; false, \"message\" =&gt; \"Required fields missing\"]); exit;\n}\nif (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode([\"success\" =&gt; false, \"message\" =&gt; \"Invalid email\"]); exit;\n}\nif (strlen($password) &lt; 6) { echo json_encode([\"success\" =&gt; false, \"message\" =&gt; \"Password too short\"]); exit;\n}\n$hashedPassword = password_hash($password, PASSWORD_BCRYPT);\n$stmt = $conn-&gt;prepare(\"INSERT INTO users (first_name, last_name, email, phone, dob, gender, username, password) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\");\n$stmt-&gt;bind_param(\"ssssssss\", $firstName, $lastName, $email, $phone, $dob, $gender, $username, $hashedPassword);\nif ($stmt-&gt;execute()) { echo json_encode([\"success\" =&gt; true, \"message\" =&gt; \"User registered successfully\"]);\n} else { echo json_encode([\"success\" =&gt; false, \"message\" =&gt; \"DB insert failed\"]);\n}\n?&gt;\n<\/code><\/pre>\n<h2>How to set up this application<\/h2>\n<p>The below steps help to set up this example to run in your environment.<\/p>\n<ol>\n<li>Download the source code into your React project directory.<\/li>\n<li>Copy the multi-step-form-validation-api into your PHP root.<\/li>\n<li>Create a database <code>multistep_form_validation_db<\/code> and import the user.sql<\/li>\n<li>Configure database details with db.php<\/li>\n<li>Configure the PHP endpoint URL in React in <code>src\/config.js<\/code><\/li>\n<li>Run <code>npm install<\/code> and then, <code>npm run dev<\/code>.<\/li>\n<li>Copy the dev server URL and run it to render the React Multi-step form.<\/li>\n<\/ol>\n<h2>Conclusion:<\/h2>\n<p>So, we have seen a simple React example to understand how to create and manage the state of a multi-step form. By splitting the mail and sub form components we had a structural code base that is more feasible for enhancements.<\/p>\n<p>The navigation between steps gives a scope for verification before confirm the signup. And the progress bar indicates the state in progress at a quick glance.<\/p>\n<p>Definitely, the <a href=\"https:\/\/phppot.com\/php\/php-form-validation\/\">PHP validation<\/a> and database processing can have add-on features to make the backend more solid. If you have a requirement to create a multi-step form in React, share your specifications in the comments.<\/p>\n<p><a class=\"download\" href=\"https:\/\/phppot.com\/downloads\/react\/react-multi-step-form-validation-progress-bar.zip\">Download<\/a><\/p>\n<div class=\"written-by\" readability=\"9.8427672955975\">\n<div class=\"author-photo\"> <img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/phppot.com\/wp-content\/themes\/solandra\/images\/Vincy.jpg\" alt=\"Vincy\" width=\"100\" height=\"100\" title=\"Vincy\"> <\/div>\n<div class=\"written-by-desc\" readability=\"14.764150943396\"> Written by <a href=\"https:\/\/phppot.com\/about\/\">Vincy<\/a>, a web developer with 15+ years of experience and a Masters degree in Computer Science. She specializes in building modern, lightweight websites using PHP, JavaScript, React, and related technologies. Phppot helps you in mastering web development through over a decade of publishing quality tutorials. <\/div>\n<\/p><\/div>\n<p> <!-- #comments --> <\/p>\n<div class=\"related-articles\">\n<h2>Related Tutorials<\/h2>\n<\/p><\/div>\n<p> <a href=\"https:\/\/phppot.com\/react\/react-multi-step-form-validation-progress-bar\/#top\" class=\"top\">\u2191 Back to Top<\/a> <\/p>\n","protected":false},"excerpt":{"rendered":"<p>by Vincy. Last modified on November 18th, 2025. A multi-step form is one of the best ways to replace a long form to make the customer feel easy. Example: a student enrolment form will usually be very long. If it is partitioned into multi-steps with section-wise sub forms, it encourages enduser to proceed forward. And [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":135298,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[65],"tags":[],"class_list":["post-135297","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-php-updates"],"_links":{"self":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/135297","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/comments?post=135297"}],"version-history":[{"count":0,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/135297\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media\/135298"}],"wp:attachment":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media?parent=135297"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/categories?post=135297"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/tags?post=135297"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}