Sick Gaming
[Tut] Build a Multi-Step Form in React with Validation and Progress Bar - Printable Version

+- Sick Gaming (https://www.sickgaming.net)
+-- Forum: Programming (https://www.sickgaming.net/forum-76.html)
+--- Forum: PHP Development (https://www.sickgaming.net/forum-82.html)
+--- Thread: [Tut] Build a Multi-Step Form in React with Validation and Progress Bar (/thread-103624.html)



[Tut] Build a Multi-Step Form in React with Validation and Progress Bar - xSicKxBot - 12-09-2025

[Tut] Build a Multi-Step Form in React with Validation and Progress Bar

<div style="margin: 5px 5% 10px 5%;"><img src="https://www.sickgaming.net/blog/wp-content/uploads/2025/12/build-a-multi-step-form-in-react-with-validation-and-progress-bar.jpg" width="550" height="288" title="" alt="" /></div><div><div class="modified-on" readability="7.1489361702128"> by <a href="https://phppot.com/about/">Vincy</a>. Last modified on November 18th, 2025.</div>
<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>
<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>
<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>
<h2>Rendering multi-step registration form</h2>
<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>
<p>It requires the following custom React component created for this example.</p>
<ol>
<li>GeneralInfo – to collect basic information, first and last names.</li>
<li>ContactInfo – to collect phone or WhatsApp numbers.</li>
<li>PersonalInfo – to collect a person’s date of birth and gender.</li>
<li>ConfirmInfo – is a last step to register confidential information and confirm registration.</li>
</ol>
<p>All information is stored in the formData by using the corresponding handleChange hook.</p>
<p>Additionally, this JSX has a Toast container to display success or error responses on the user-entered data.</p>
<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>
<p class="code-heading"><code>src/components/RegisterForm.jsx</code></p>
<pre class="prettyprint"><code class="language-jsx">import { useState } from "react";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import ProgressBar from "./ProgressBar";
import GeneralInfo from "./FormSteps/GeneralInfo";
import ContactInfo from "./FormSteps/ContactInfo";
import PersonalInfo from "./FormSteps/PersonalInfo";
import Confirmation from "./FormSteps/Confirmation";
import "../../public/assests/css/RegisterForm.css";
const 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, }); };
return (
&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;
&lt;/div&gt;
);
};
export default RegisterForm;
</code></pre>
<h2>Form progress bar with numbered in-progress state of registration</h2>
<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>
<p>This example provides both of these controls which will be useful to learn how to make this for other similar cases.</p>
<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>
<p>The conditional statements load the CSS className ‘active’ dynamically when loading the progress bar to the UI.</p>
<p>All the completed steps are highlighted by a filled background and shows clarity on the current state.</p>
<p class="code-heading"><code>src/components/ProgressBar.jsx</code></p>
<pre class="prettyprint"><code class="language-jsx">const ProgressBar = ({ step }) =&gt; {
return (
&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;
&lt;/div&gt;
);
};
export default ProgressBar;
</code></pre>
<h2>React Form components collecting types of user information</h2>
<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>
<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>
<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>
<h3>Step 1 – Collecting general information</h3>
<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>
<p class="code-heading"><code>src/components/FormSteps/GeneralInfo.jsx</code></p>
<pre class="prettyprint"><code class="language-jsx">import { useState } from "react";
const 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; );
};
export default GeneralInfo;
</code></pre>
<h3>Step 2: Collecting contact information</h3>
<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>
<p class="code-heading"><code>src/components/FormSteps/ContactInfo.jsx</code></p>
<pre class="prettyprint"><code class="language-jsx">import { useState } from "react";
const 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; );
};
export default ContactInfo;
</code></pre>
<h3>Step3 – Collecting personal information</h3>
<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>
<p class="code-heading"><code>src/components/FormSteps/PersonalInfo.jsx</code></p>
<pre class="prettyprint"><code class="language-jsx">import { useState } from "react";
const 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; };
return ( &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;
);
};
export default PersonalInfo;
</code></pre>
<h3>Step 4 – Collecting user consent and confidential information</h3>
<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>
<p class="code-heading"><code>src/components/FormSteps/Confirmation.jsx</code></p>
<pre class="prettyprint"><code class="language-jsx">import { useState } from "react";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import axios from "axios";
import SERVER_SIDE_API_ROOT from "../../config";
const 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;
return ( &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;
);
};
export default Confirmation;
</code></pre>
<h2>PHP endpoint processing multi-step form data</h2>
<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>
<p>With this form data, it builds the database insert query to save the user-entered information to the backend.</p>
<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>
<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>
<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>
<p class="code-heading"><code>multi-step-form-validation-api/multi-step-form.php</code></p>
<pre class="prettyprint"><code class="language-php">&lt;?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: Content-Type");
header("Access-Control-Allow-Methods: POST");
header("Content-Type: application/json");
include 'db.php';
$data = json_decode(file_get_contents("php://input"), true);
$firstName = $data["first_name"] ?? "";
$lastName = $data["last_name"] ?? "";
$email = $data["email"] ?? "";
$phone = $data["phone"] ?? "";
$dob = $data["dob"] ?? "";
$gender = $data["gender"] ?? "";
$username = $data["username"] ?? "";
$password = $data["password"] ?? "";
if (!$firstName || !$email || !$password) { echo json_encode(["success" =&gt; false, "message" =&gt; "Required fields missing"]); exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode(["success" =&gt; false, "message" =&gt; "Invalid email"]); exit;
}
if (strlen($password) &lt; 6) { echo json_encode(["success" =&gt; false, "message" =&gt; "Password too short"]); exit;
}
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
$stmt = $conn-&gt;prepare("INSERT INTO users (first_name, last_name, email, phone, dob, gender, username, password) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt-&gt;bind_param("ssssssss", $firstName, $lastName, $email, $phone, $dob, $gender, $username, $hashedPassword);
if ($stmt-&gt;execute()) { echo json_encode(["success" =&gt; true, "message" =&gt; "User registered successfully"]);
} else { echo json_encode(["success" =&gt; false, "message" =&gt; "DB insert failed"]);
}
?&gt;
</code></pre>
<h2>How to set up this application</h2>
<p>The below steps help to set up this example to run in your environment.</p>
<ol>
<li>Download the source code into your React project directory.</li>
<li>Copy the multi-step-form-validation-api into your PHP root.</li>
<li>Create a database <code>multistep_form_validation_db</code> and import the user.sql</li>
<li>Configure database details with db.php</li>
<li>Configure the PHP endpoint URL in React in <code>src/config.js</code></li>
<li>Run <code>npm install</code> and then, <code>npm run dev</code>.</li>
<li>Copy the dev server URL and run it to render the React Multi-step form.</li>
</ol>
<h2>Conclusion:</h2>
<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>
<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>
<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>
<p><a class="download" href="https://phppot.com/downloads/react/react-multi-step-form-validation-progress-bar.zip">Download</a></p>
<div class="written-by" readability="9.8427672955975">
<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>
<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>
</p></div>
<p> <!-- #comments --> </p>
<div class="related-articles">
<h2>Related Tutorials</h2>
</p></div>
<p> <a href="https://phppot.com/react/react-multi-step-form-validation-progress-bar/#top" class="top">↑ Back to Top</a> </p>
</div>


https://www.sickgaming.net/blog/2025/11/07/build-a-multi-step-form-in-react-with-validation-and-progress-bar/