LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
BootstrapBeginner~1 hour

Multi-Field Signup Form

Create a validated registration form with Bootstrap form components.

FormsValidationUtilities

Overview

A signup form is where Bootstrap's form styling and its validation feedback have to work together, not separately. Bootstrap ships styled replacements for every native field — form-control, form-select, form-check — but the red-border, red-text "this field is wrong" feedback only appears once a form carries the was-validated class, which nothing adds automatically. That gap is exactly what this project fills in.

You will build a registration form with first/last name, email, password with confirmation, a country dropdown, and a required terms checkbox, then wire up the short vanilla-JavaScript snippet Bootstrap's own documentation recommends so submitting the form with invalid fields shows styled, per-field error messages instead of the browser's default validation bubbles.

What You'll Build
  • A centered card-wrapped registration form using form-label + form-control pairs for text/email/password fields.
  • A form-select dropdown for country and a required form-check checkbox for agreeing to terms.
  • Per-field invalid-feedback messages that appear only once a field actually fails validation.
  • A small JavaScript snippet using the browser's constraint validation API to trigger Bootstrap's was-validated styling on submit.
  • A full-width submit button using the d-grid utility.

Prerequisites

  • Native HTML form basics — <form>, <input>, <label>, and attributes like required, type, and minlength/pattern.
  • Bootstrap's grid (row/col-*) for laying fields out side by side.
  • Spacing and layout utility classes (g-3, d-grid, mt-1) used to space and arrange the fields.
  • Basic vanilla JavaScript — querySelectorAll, addEventListener, and reading a boolean from a method call — for Step 6.

Project Structure

The whole form lives in one file, signup.html. A container/row/col combination centers a card on the page, and the <form> inside that card carries two attributes from the very first line: novalidate, which turns off the browser's own default validation-bubble UI so Bootstrap's styled feedback can show instead, and needs-validation, a plain marker class with no built-in Bootstrap styling of its own — it exists purely as a hook the JavaScript in Step 6 looks for with querySelectorAll.

Every field that can fail validation gets an .invalid-feedback element immediately after it. That element is invisible by default; Bootstrap's CSS only reveals it when its :invalid sibling input sits inside a form carrying .was-validated — which is exactly the class the Step 6 script adds the moment the form is submitted.

Step 1: Set Up the Page and the Form Shell

Start with a standard HTML5 document, load Bootstrap, and build the centered card that will hold the form. The row justify-content-center + col-md-8 col-lg-6 combination keeps the form a readable width on large screens instead of letting it stretch edge to edge.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Your Account</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="row justify-content-center"> <!-- centers the card instead of letting it stretch full width -->
<div class="col-md-8 col-lg-6">
<div class="card shadow-sm">
<div class="card-body p-4">
<h1 class="h3 mb-4 text-center">Create Your Account</h1>
<!-- novalidate: turns off the browser's default validation popups so
Bootstrap's own styled feedback (added in later steps) shows instead -->
<!-- needs-validation: not a Bootstrap style, just a marker class the
script in Step 6 uses to find this form with querySelectorAll -->
<form class="needs-validation" novalidate>
<!-- fields added in the steps that follow -->
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Step 2: Add Name Fields

First and last name sit side by side using row g-3 + col-md-6, collapsing to one column per row automatically on narrow screens since no col-* breakpoint smaller than md is set. Each invalid-feedback div is written once here and, from this point on, follows the same pattern for every field: it sits immediately after the input it describes.

<!-- Goes inside the <form> from Step 1 -->
<div class="row g-3"> <!-- g-3: consistent gutter between the two columns below -->
<div class="col-md-6">
<label for="firstName" class="form-label">First name</label>
<input type="text" class="form-control" id="firstName" name="firstName" required>
<!-- .invalid-feedback is invisible until this input is :invalid AND the form
has .was-validated - both conditions come from the Step 6 script -->
<div class="invalid-feedback">First name is required.</div>
</div>
<div class="col-md-6">
<label for="lastName" class="form-label">Last name</label>
<input type="text" class="form-control" id="lastName" name="lastName" required>
<div class="invalid-feedback">Last name is required.</div>
</div>
</div>

Step 3: Add Email and Password Fields

The email field is wrapped in input-group so an @ symbol sits flush against its left edge — a purely visual affordance, since type="email" alone already handles the actual format validation. The password field's pattern requires at least one letter and one digit across 8+ characters; pattern and minlength both run through the same browser-native checkValidity() call the Step 6 script relies on, so no extra validation logic is needed for either.

<div class="row g-3 mt-1">
<div class="col-12">
<label for="email" class="form-label">Email</label>
<div class="input-group"> <!-- input-group lets the @ icon sit flush against the input -->
<span class="input-group-text">@</span>
<input type="email" class="form-control" id="email" name="email" required>
<div class="invalid-feedback">Please enter a valid email address.</div>
</div>
</div>
<div class="col-md-6">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required
minlength="8" pattern="(?=.*\d)(?=.*[a-zA-Z]).{8,}">
<!-- pattern requires at least one letter and one digit across 8+ characters -->
<div class="invalid-feedback">Password must be 8+ characters and include a letter and a number.</div>
</div>
<div class="col-md-6">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
<!-- Checking the two password fields actually match needs a few lines of custom
JS (setCustomValidity) beyond what "required" alone checks - see Extend This Project -->
<div class="invalid-feedback">Please confirm your password.</div>
</div>
</div>

Step 4: Add a Country Select and Terms Checkbox

form-select restyles a native <select> to match form-control's look, but it is still a genuine <select> underneath, so keyboard and screen-reader behavior is unchanged. The terms checkbox uses form-check — the same label-pairing pattern as form-label, just for checkboxes and radios — and required on a checkbox means the form is invalid until it is checked, not just present.

<div class="row g-3 mt-1">
<div class="col-md-6">
<label for="country" class="form-label">Country</label>
<select class="form-select" id="country" name="country" required>
<option value="" selected disabled>Choose...</option>
<!-- selected disabled: shown by default but not a real, selectable answer,
so leaving it chosen still fails the "required" check -->
<option value="IN">India</option>
<option value="US">United States</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
</select>
<div class="invalid-feedback">Please select your country.</div>
</div>
<div class="col-md-6 d-flex align-items-end">
<!-- form-check pairs a checkbox with its label, mirroring how form-label
pairs with the inputs above; required here since agreeing is mandatory -->
<div class="form-check">
<input class="form-check-input" type="checkbox" id="terms" name="terms" required>
<label class="form-check-label" for="terms">
I agree to the <a href="#">Terms of Service</a>
</label>
<div class="invalid-feedback">You must agree before signing up.</div>
</div>
</div>
</div>

Step 5: Add the Submit Button

The submit button closes out the form. d-grid makes it stretch to the container's full width — a common pattern for a form's single primary action, since a small button floating at the left edge of a wide card looks unfinished.

<!-- d-grid: makes the button span the full width of its container -->
<div class="d-grid mt-4">
<button class="btn btn-primary btn-lg" type="submit">Create Account</button>
</div>

Step 6: Wire Up Bootstrap's Validation JavaScript

This is the piece that actually turns on every .invalid-feedback message added in the earlier steps. form.checkValidity() runs the browser's built-in constraint validation — checking every required, pattern, minlength, and type="email" at once — without submitting anything. If it returns false, the real submit is cancelled with preventDefault(); either way, .was-validated is added, which is the class Bootstrap's CSS actually watches for before showing red borders and feedback text.

<!-- Add just before </body>, after the Bootstrap bundle <script> tag -->
<script>
(() => {
'use strict'
// Finds every form carrying the "needs-validation" marker class from Step 1
const forms = document.querySelectorAll('.needs-validation')
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
// checkValidity() runs the browser's native constraint validation
// (required, pattern, minlength, type="email", etc.) without submitting
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
// was-validated is what actually switches on Bootstrap's red/green field
// styling and reveals the matching .invalid-feedback text for each field
form.classList.add('was-validated')
}, false)
})
})()
</script>

Complete Code

Here is the full signup form with every field and the validation script assembled in order.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Your Account</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card shadow-sm">
<div class="card-body p-4">
<h1 class="h3 mb-4 text-center">Create Your Account</h1>
<form class="needs-validation" novalidate>
<div class="row g-3">
<div class="col-md-6">
<label for="firstName" class="form-label">First name</label>
<input type="text" class="form-control" id="firstName" name="firstName" required>
<div class="invalid-feedback">First name is required.</div>
</div>
<div class="col-md-6">
<label for="lastName" class="form-label">Last name</label>
<input type="text" class="form-control" id="lastName" name="lastName" required>
<div class="invalid-feedback">Last name is required.</div>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-12">
<label for="email" class="form-label">Email</label>
<div class="input-group">
<span class="input-group-text">@</span>
<input type="email" class="form-control" id="email" name="email" required>
<div class="invalid-feedback">Please enter a valid email address.</div>
</div>
</div>
<div class="col-md-6">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required
minlength="8" pattern="(?=.*\d)(?=.*[a-zA-Z]).{8,}">
<div class="invalid-feedback">Password must be 8+ characters and include a letter and a number.</div>
</div>
<div class="col-md-6">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
<div class="invalid-feedback">Please confirm your password.</div>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-md-6">
<label for="country" class="form-label">Country</label>
<select class="form-select" id="country" name="country" required>
<option value="" selected disabled>Choose...</option>
<option value="IN">India</option>
<option value="US">United States</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
</select>
<div class="invalid-feedback">Please select your country.</div>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="terms" name="terms" required>
<label class="form-check-label" for="terms">
I agree to the <a href="#">Terms of Service</a>
</label>
<div class="invalid-feedback">You must agree before signing up.</div>
</div>
</div>
</div>
<div class="d-grid mt-4">
<button class="btn btn-primary btn-lg" type="submit">Create Account</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
(() => {
'use strict'
const forms = document.querySelectorAll('.needs-validation')
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
</body>
</html>
Live Preview

Sample Run

What Happens

Click Run to see what this code prints.

Extend This Project

  • Add real password-match checking with confirmPassword.setCustomValidity(...), called on each input event, so the confirm field only passes once it actually equals the password field.
  • Swap form-label + form-control pairs for Bootstrap's form-floating variant so labels animate into a compact floating position once a field has a value.
  • Add a password-strength meter that updates live as the user types, using a progress bar styled by how many of the pattern requirements are currently met.
  • Replace the client-only validation with a real submit handler that posts the form data with fetch and shows a server-driven success or error state.
  • Add a profile-picture type="file" field, restricted with accept="image/*", above the submit button.

Summary

You built a fully validated signup form using Bootstrap's form components paired with its documented validation script: form-control/form-select/form-check for styled fields, invalid-feedback for per-field error text, and a short JavaScript snippet that turns the browser's built-in constraint validation into Bootstrap's styled was-validated feedback. That same three-part pattern — styled fields, feedback elements, and the validation script — is reusable on any form Bootstrap renders.