A multi-step form is a useful way to break a long registration process into smaller, easier-to-complete sections.
In this tutorial, we'll build a Multi Step Registration Form using HTML, CSS and JavaScript. The form includes account details, profile information, a review screen, client-side validation, password strength feedback and a final success state.
What We're Building
Our registration form is divided into three steps:
- Account Details — Email, password and password confirmation
- Profile — First name, last name, username, role and bio
- Review — Review the entered information before creating the account
The component also includes:
- Step-by-step navigation
- Responsive layout
- Email validation
- Password confirmation
- Password strength indicator
- Show/hide password control
- Username validation
- Character counter for the bio
- Editable review screen
- Terms acceptance validation
- Success state
- Start Again option
HTML Structure
The component starts with a main wrapper and contains the header, progress indicator, registration form and success screen.
<div class="cr-step-form" id="crStepForm">
<div class="cr-step-form__card">
<div class="cr-step-form__header">
<span class="cr-step-form__eyebrow">
CREATE YOUR ACCOUNT
</span>
<h2>Join CodeRipple</h2>
<p>
Complete the steps below to set up your profile.
</p>
</div>
<div class="cr-step-form__progress">
<div
class="cr-step cr-step--active"
data-step-indicator="1">
<div class="cr-step__circle">
<span>1</span>
</div>
<div class="cr-step__text">
<strong>Account</strong>
<small>Login details</small>
</div>
</div>
<div class="cr-step__line"></div>
<div
class="cr-step"
data-step-indicator="2">
<div class="cr-step__circle">
<span>2</span>
</div>
<div class="cr-step__text">
<strong>Profile</strong>
<small>About you</small>
</div>
</div>
<div class="cr-step__line"></div>
<div
class="cr-step"
data-step-indicator="3">
<div class="cr-step__circle">
<span>3</span>
</div>
<div class="cr-step__text">
<strong>Review</strong>
<small>Confirm details</small>
</div>
</div>
</div>
<form
class="cr-step-form__form"
novalidate>
<!-- STEP 1 -->
<section
class="cr-form-step cr-form-step--active"
data-step="1">
<div class="cr-form-step__heading">
<span class="cr-form-step__number">
01
</span>
<div>
<h3>Account Details</h3>
<p>
Choose the credentials you'll use to sign in.
</p>
</div>
</div>
<div class="cr-field">
<label for="crEmail">
Email Address
</label>
<input
type="email"
id="crEmail"
placeholder="you@example.com"
autocomplete="email">
<span class="cr-field__error"></span>
</div>
<div class="cr-field">
<label for="crPassword">
Password
</label>
<div class="cr-password-wrap">
<input
type="password"
id="crPassword"
placeholder="Create a password"
minlength="8"
autocomplete="new-password">
<button
type="button"
class="cr-password-toggle"
aria-label="Show password">
Show
</button>
</div>
<div class="cr-password-strength">
<div class="cr-password-strength__bars">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<span class="cr-password-status">
Password strength
</span>
</div>
<span class="cr-field__error"></span>
</div>
<div class="cr-field">
<label for="crConfirmPassword">
Confirm Password
</label>
<input
type="password"
id="crConfirmPassword"
placeholder="Enter password again"
autocomplete="new-password">
<span class="cr-field__error"></span>
</div>
</section>
<!-- STEP 2 -->
<section
class="cr-form-step"
data-step="2">
<div class="cr-form-step__heading">
<span class="cr-form-step__number">
02
</span>
<div>
<h3>Your Profile</h3>
<p>
Tell us a little more about yourself.
</p>
</div>
</div>
<div class="cr-field">
<label for="crFirstName">
First Name
</label>
<input
type="text"
id="crFirstName"
placeholder="First name"
autocomplete="given-name">
<span class="cr-field__error"></span>
</div>
<div class="cr-field">
<label for="crLastName">
Last Name
</label>
<input
type="text"
id="crLastName"
placeholder="Last name"
autocomplete="family-name">
<span class="cr-field__error"></span>
</div>
<div class="cr-field">
<label for="crUsername">
Username
</label>
<input
type="text"
id="crUsername"
placeholder="coderipple_user"
minlength="3"
autocomplete="username">
<small>
Letters, numbers and underscores only.
</small>
<span class="cr-field__error"></span>
</div>
<div class="cr-field">
<label for="crRole">
Role
</label>
<select id="crRole">
<option value="">
Select your role
</option>
<option value="developer">
Developer
</option>
<option value="designer">
Designer
</option>
<option value="student">
Student
</option>
<option value="freelancer">
Freelancer
</option>
<option value="creator">
Content Creator
</option>
<option value="other">
Other
</option>
</select>
<span class="cr-field__error"></span>
</div>
<div class="cr-field">
<label for="crBio">
Short Bio
<span>Optional</span>
</label>
<textarea
id="crBio"
maxlength="160"
placeholder="Tell us a little about yourself..."></textarea>
<div class="cr-field__counter">
<span id="crBioCount">0</span>/160
</div>
</div>
</section>
<!-- STEP 3 -->
<section
class="cr-form-step"
data-step="3">
<div class="cr-form-step__heading">
<span class="cr-form-step__number">
03
</span>
<div>
<h3>Review & Submit</h3>
<p>
Check your information before creating your account.
</p>
</div>
</div>
<div class="cr-review">
<div class="cr-review__section">
<div class="cr-review__header">
<h4>Account</h4>
<button
type="button"
data-edit-step="1">
Edit
</button>
</div>
<div class="cr-review__row">
<span>Email Address</span>
<strong data-review="email">
Not provided
</strong>
</div>
<div class="cr-review__row">
<span>Password</span>
<strong data-review="password">
••••••••
</strong>
</div>
</div>
<div class="cr-review__section">
<div class="cr-review__header">
<h4>Profile</h4>
<button
type="button"
data-edit-step="2">
Edit
</button>
</div>
<div class="cr-review__row">
<span>Full Name</span>
<strong data-review="name">
Not provided
</strong>
</div>
<div class="cr-review__row">
<span>Username</span>
<strong data-review="username">
Not provided
</strong>
</div>
<div class="cr-review__row">
<span>Role</span>
<strong data-review="role">
Not provided
</strong>
</div>
<div class="cr-review__row">
<span>Bio</span>
<strong data-review="bio">
Not provided
</strong>
</div>
</div>
</div>
<label class="cr-terms">
<input
type="checkbox"
id="crTerms">
<span>
I agree to the
<a href="#">Terms of Service</a>
and
<a href="#">Privacy Policy</a>.
</span>
</label>
<span class="cr-terms-error"></span>
</section>
<!-- ACTIONS -->
<div class="cr-step-form__actions">
<button
type="button"
class="cr-btn cr-btn--back"
id="crBackBtn"
disabled>
← Back
</button>
<div class="cr-step-form__counter">
Step
<span id="crCurrentStep">1</span>
of 3
</div>
<button
type="button"
class="cr-btn cr-btn--next"
id="crNextBtn">
Next Step →
</button>
<button
type="submit"
class="cr-btn cr-btn--submit"
id="crSubmitBtn"
hidden>
Create Account
</button>
</div>
</form>
<!-- SUCCESS -->
<div
class="cr-success"
hidden>
<span class="cr-success__eyebrow">
ALL DONE
</span>
<h3>
Account Created!
</h3>
<p>
Your demo registration has been completed successfully.
</p>
<button
type="button"
class="cr-btn cr-btn--restart">
Start Again
</button>
</div>
</div>
</div>
Styling the Multi Step Form
The component uses a dark interface with purple and cyan accents.
CSS variables make the main colors easy to customize:
.cr-step-form {
--cr-bg: #070a14;
--cr-panel: #101426;
--cr-field: #080c19;
--cr-text: #ffffff;
--cr-muted: #8f9bb3;
--cr-purple: #8658ff;
--cr-cyan: #28d7ff;
--cr-green: #3de3a0;
--cr-danger: #ff6680;
width: 100%;
font-family: Inter, system-ui, sans-serif;
}
The main registration card can then use the dark theme:
.cr-step-form__card {
position: relative;
width: 100%;
max-width: 1180px;
margin: 0 auto;
border-radius: 24px;
overflow: hidden;
color: var(--cr-text);
}
Only the currently active form section needs to be displayed:
.cr-form-step {
display: none;
}
.cr-form-step--active {
display: block;
animation: crStepEnter 0.35s ease;
}
@keyframes crStepEnter {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
The form can also provide visual feedback for valid and invalid fields:
.cr-field--error input,
.cr-field--error select,
.cr-field--error textarea {
border-color: var(--cr-danger);
}
.cr-field--valid input,
.cr-field--valid select,
.cr-field--valid textarea {
border-color: var(--cr-green);
}
.cr-field__error {
color: var(--cr-danger);
font-size: 12px;
}
For smaller screens, the multi-column form can switch to a single-column layout:
@media (max-width: 650px) {
.cr-step-form {
padding: 16px;
}
.cr-form-step {
width: 100%;
}
.cr-step-form__actions {
grid-template-columns: 1fr 1fr;
}
}
The full CodeRipple version contains the complete responsive styling, progress states, review cards, form fields, buttons, password meter and success screen.
JavaScript Step Navigation
JavaScript keeps track of the current form step:
let currentStep = 1;
The showStep() function controls which section is visible:
const showStep = (stepNumber) => {
currentStep = Math.max(
1,
Math.min(stepNumber, 3)
);
formSteps.forEach((step) => {
const number =
Number(step.dataset.step);
step.classList.toggle(
"cr-form-step--active",
number === currentStep
);
});
backBtn.disabled =
currentStep === 1;
if (currentStep === 3) {
updateReview();
nextBtn.hidden = true;
submitBtn.hidden = false;
} else {
nextBtn.hidden = false;
submitBtn.hidden = true;
}
currentStepText.textContent =
String(currentStep);
updateProgress();
};
This keeps the navigation between steps synchronized with the progress indicator.
Validating Step One
The first step checks the email, password and password confirmation before allowing the user to continue.
const validateStepOne = () => {
const emailValid =
validateEmail();
const passwordValid =
validatePassword();
const confirmValid =
validateConfirmPassword();
return (
emailValid &&
passwordValid &&
confirmValid
);
};
If any field is invalid, the form remains on the current step and displays the relevant error.
Email Validation
The email field first checks whether a value has been entered.
It then checks the basic email format:
const validateEmail = () => {
const value =
email.value.trim();
if (!value) {
showError(
email,
"Email address is required."
);
return false;
}
const emailPattern =
/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
if (!emailPattern.test(value)) {
showError(
email,
"Enter a valid email address."
);
return false;
}
showValid(email);
return true;
};
This is client-side validation for the demo interface.
A production registration system should also validate and sanitize submitted data on the server.
Password Strength Indicator
The password strength meter checks four conditions:
- At least 8 characters
- At least one uppercase letter
- At least one number
- At least one special character
const getPasswordStrength = (value) => {
if (!value) return 0;
let score = 0;
if (value.length >= 8) {
score++;
}
if (/[A-Z]/.test(value)) {
score++;
}
if (/[0-9]/.test(value)) {
score++;
}
if (/[^A-Za-z0-9]/.test(value)) {
score++;
}
return score;
};
The interface can display the result as:
- Very weak
- Weak
- Fair
- Good
- Strong
The strength indicator updates while the user types.
Confirm Password Validation
The confirmation field checks whether the user entered a value and whether both passwords match:
const validateConfirmPassword = () => {
const value =
confirmPassword.value;
if (!value) {
showError(
confirmPassword,
"Please confirm your password."
);
return false;
}
if (value !== password.value) {
showError(
confirmPassword,
"Passwords do not match."
);
return false;
}
showValid(confirmPassword);
return true;
};
The confirmation is also revalidated when the original password changes.
Profile Validation
Step two validates the profile information.
const validateStepTwo = () => {
const firstValid =
validateName(
firstName,
"First name"
);
const lastValid =
validateName(
lastName,
"Last name"
);
const usernameValid =
validateUsername();
const roleValid =
validateRole();
return (
firstValid &&
lastValid &&
usernameValid &&
roleValid
);
};
The bio remains optional.
Username Validation
The username requires at least three characters.
It also accepts only letters, numbers and underscores:
const usernamePattern =
/^[A-Za-z0-9_]+$/;
If another character is entered, an error message is displayed.
Bio Character Counter
The profile bio has a maximum length of 160 characters.
The counter updates while the user types:
bio.addEventListener(
"input",
() => {
bioCount.textContent =
String(
bio.value.length
);
}
);
This gives the user immediate feedback without waiting for form submission.
Next and Back Buttons
The Next Step button validates the current section before moving forward.
nextBtn.addEventListener(
"click",
(event) => {
event.preventDefault();
if (currentStep === 1) {
if (!validateStepOne()) {
return;
}
showStep(2);
return;
}
if (currentStep === 2) {
if (!validateStepTwo()) {
return;
}
updateReview();
showStep(3);
}
}
);
The Back button returns to the previous step:
backBtn.addEventListener(
"click",
(event) => {
event.preventDefault();
if (currentStep > 1) {
showStep(
currentStep - 1
);
}
}
);
Building the Review Screen
Before creating the account, the third step displays the information entered in the previous sections.
A helper function updates each review value:
const setReviewValue = (name, value) => {
const target =
component.querySelector(
`[data-review="${name}"]`
);
if (!target) return;
const cleanValue =
String(value || "").trim();
target.textContent =
cleanValue || "Not provided";
};
The full name is created from the first and last name:
const fullName = [
firstName.value.trim(),
lastName.value.trim()
]
.filter(Boolean)
.join(" ");
The username is displayed with an @ prefix:
const usernameValue =
username.value.trim();
setReviewValue(
"username",
usernameValue
? `@${usernameValue}`
: ""
);
The password itself is not displayed.
Instead, the review screen shows masked dots:
const passwordDots =
password.value
? "•".repeat(
Math.min(
password.value.length,
12
)
)
: "••••••••";
Editing Information from the Review Step
The review cards include Edit buttons.
These buttons return the user to the relevant step:
editButtons.forEach((button) => {
button.addEventListener(
"click",
(event) => {
event.preventDefault();
const stepNumber =
Number(
button.dataset.editStep
);
if (!stepNumber) return;
showStep(stepNumber);
}
);
});
This allows users to correct their information without restarting the form.
Show and Hide Password
The password visibility button switches the input between password and text:
passwordToggle.addEventListener(
"click",
(event) => {
event.preventDefault();
const hidden =
password.type === "password";
password.type =
hidden
? "text"
: "password";
passwordToggle.setAttribute(
"aria-label",
hidden
? "Hide password"
: "Show password"
);
}
);
Updating the aria-label also keeps the control more accessible.
Terms Validation
Before the demo registration can be completed, the terms checkbox must be selected:
if (!terms.checked) {
termsError.textContent =
"Please accept the Terms of Service and Privacy Policy.";
return;
}
Once the checkbox is selected, the error is cleared automatically.
Success State
After the final validation succeeds, the registration wizard is hidden:
form.hidden = true;
if (progress) {
progress.hidden = true;
}
if (header) {
header.hidden = true;
}
The success state is then displayed:
success.hidden = false;
The user sees an Account Created! message and can start the demo again.
Resetting the Form
The Start Again button resets:
- Form values
- Validation states
- Terms checkbox
- Bio counter
- Password strength
- Password visibility
- Progress indicator
- Current step
The form then returns to Step 1.
This makes the component easy to test repeatedly.
Where Can You Use This Component?
A multi-step registration form can be adapted for many interfaces, including:
- Account registration
- SaaS onboarding
- Membership websites
- Developer communities
- Portfolio platforms
- Application forms
- Profile setup
- Multi-stage contact forms
The current CodeRipple component is a front-end demo.
For a real registration system, you would still need backend processing, secure password handling, server-side validation and database integration.
Live Demo and Complete Source Code
You can test the complete component and copy the full HTML, CSS and JavaScript on CodeRipple:
👉 Multi Step Registration Form - Full Tutorial
You can also experiment with the live component on CodePen:
👉 Multi Step Registration Form - CodePen Demo
Final Thoughts
Breaking a registration process into smaller steps can make a larger form easier to understand and navigate.
In this component:
- HTML provides the three-step form structure
- CSS creates the responsive dark interface
- JavaScript handles navigation, validation, password feedback, review data and the success state
You can customize the colors, fields, validation rules and number of steps to fit your own project.
For more ready-to-use HTML, CSS and JavaScript components, visit CodeRipple.
Top comments (0)