Form Validation
A form that accepts a 9-digit mobile number or an empty email will cause trouble later: SMS that never arrives, customers you cannot contact. Validation checks the input before it is sent and tells the user clearly what to fix. Bootstrap does not invent its own validation system. It styles the browser's built-in HTML validation and gives you classes for messages that come from your server.
In this lesson you will validate a form in the browser with HTML attributes and a few lines of JavaScript, add a custom rule, and show errors that come back from the server.
Step 1: rules in HTML
The browser already understands rules written as HTML attributes (see HTML Form Validation):
| Attribute | Rule |
|---|---|
required | Must not be empty |
type="email", type="url" | Must look like an email or a web address |
minlength, maxlength | Text length limits |
min, max, step | Number limits |
pattern | Must match a regular expression |
Each field gets a :valid or :invalid state that CSS can see and JavaScript can check with checkValidity().
Step 2: Bootstrap's was-validated pattern
The browser's own error bubbles look different in every browser and cannot be styled. Bootstrap's approach:
- Add
novalidateto the<form>, which turns off the browser bubbles (the rules still work). - Put a
divwithinvalid-feedback(and optionallyvalid-feedback) right after each field. They are hidden until validation runs. - On submit, check the form with JavaScript. If it is invalid, stop the submission. In both cases add the
was-validatedclass to the form, which makes Bootstrap colour each field red or green and show the matching feedback.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign-up validation</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-3" style="max-width: 560px">
<h1 class="h4 mb-3">Create your account</h1>
<form id="signup" class="row g-3" novalidate>
<div class="col-12">
<label for="fullName" class="form-label">Full name</label>
<input type="text" class="form-control" id="fullName" required minlength="3" aria-describedby="nameFeedback">
<div id="nameFeedback" class="invalid-feedback">Please enter at least 3 letters.</div>
</div>
<div class="col-md-6">
<label for="mobile" class="form-label">Mobile number</label>
<div class="input-group has-validation">
<span class="input-group-text">+91</span>
<input type="tel" class="form-control" id="mobile" required pattern="[6-9][0-9]{9}" inputmode="numeric" aria-describedby="mobileFeedback">
<div id="mobileFeedback" class="invalid-feedback">Enter a 10-digit mobile number starting with 6, 7, 8 or 9.</div>
</div>
</div>
<div class="col-md-6">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" required aria-describedby="emailFeedback">
<div id="emailFeedback" class="invalid-feedback">Enter an email such as priya@example.com.</div>
</div>
<div class="col-md-6">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required minlength="8" aria-describedby="passFeedback">
<div id="passFeedback" class="invalid-feedback">Use at least 8 characters.</div>
</div>
<div class="col-md-6">
<label for="confirm" class="form-label">Confirm password</label>
<input type="password" class="form-control" id="confirm" required aria-describedby="confirmFeedback">
<div id="confirmFeedback" class="invalid-feedback">Passwords must match.</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="terms" required aria-describedby="termsFeedback">
<label class="form-check-label" for="terms">I agree to the terms</label>
<div id="termsFeedback" class="invalid-feedback">You must agree before signing up.</div>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Sign up</button>
</div>
</form>
<p id="result" class="mt-3"></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script>
const form = document.getElementById("signup");
const password = document.getElementById("password");
const confirm = document.getElementById("confirm");
// A custom rule: setCustomValidity("") means valid, any other text means invalid.
function checkMatch() {
confirm.setCustomValidity(confirm.value === password.value ? "" : "Passwords do not match");
}
password.addEventListener("input", checkMatch);
confirm.addEventListener("input", checkMatch);
form.addEventListener("submit", (event) => {
checkMatch();
if (!form.checkValidity()) {
event.preventDefault();
form.querySelector(":invalid").focus(); // take the user to the first problem
} else {
event.preventDefault(); // demo only: a real form would be sent to the server
document.getElementById("result").textContent = "All good. The form would be sent now.";
}
form.classList.add("was-validated");
});
</script>
</body>
</html>Click "Sign up" with the form empty: every field turns red with its message, and the cursor jumps to the first problem. Type valid values and the fields turn green as you go, because was-validated stays on the form and the CSS reacts to :valid and :invalid live.
Key parts of the script:
form.checkValidity()returnstrueonly if every field passes its rules.setCustomValidity()adds rules HTML cannot express, such as "the two passwords must match". An empty string means valid. Bootstrap's red styling picks this up automatically, because it simply follows:invalid.- Inside an input group, add
has-validationto the group so the rounded corners stay correct when the feedback is shown. aria-describedbylinks each message to its field, so a screen reader reads "Mobile number, invalid entry, Enter a 10-digit mobile number...".
Server-side validation and is-invalid
Browser checks are only for convenience. Anyone can switch off JavaScript or send a request without your form, so your server (for example, PHP) must check everything again. When the server finds a problem, such as "this email is already registered", it sends the form back with the error marked using is-invalid on the field:
<div class="mb-3">
<label for="email2" class="form-label">Email</label>
<input type="email" class="form-control is-invalid" id="email2" value="meera@example.com" aria-describedby="email2Feedback">
<div id="email2Feedback" class="invalid-feedback">This email is already registered. Try logging in instead.</div>
</div>is-invalid and is-valid work without was-validated and without novalidate; they mark exactly the fields you choose. This is also the better choice when you want to mark only the fields with errors and not paint every correct field green.
Tooltip-style feedback
If the layout has no room for messages under the fields, use invalid-tooltip (or valid-tooltip) instead. The message appears as a small floating box, so the parent needs position-relative:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Server errors</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-3" style="max-width: 560px">
<div class="alert alert-danger" role="alert">Please fix 2 problems in the form.</div>
<form class="row gx-3 gy-5">
<div class="col-md-6 position-relative">
<label for="pin" class="form-label">PIN code</label>
<input type="text" class="form-control is-invalid" id="pin" value="12400" aria-describedby="pinTip">
<div id="pinTip" class="invalid-tooltip">A PIN code has 6 digits.</div>
</div>
<div class="col-md-6 position-relative">
<label for="city" class="form-label">City</label>
<input type="text" class="form-control is-valid" id="city" value="Rohtak">
<div class="valid-tooltip">Looks good.</div>
</div>
<div class="col-12 position-relative">
<label for="date" class="form-label">Delivery date</label>
<input type="date" class="form-control is-invalid" id="date" value="2026-01-01" aria-describedby="dateTip">
<div id="dateTip" class="invalid-tooltip">The date must be today or later.</div>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>The alert at the top summarises the problems, which helps screen-reader users and anyone who scrolled past the errors.
Common mistakes
- Forgetting
novalidate. The browser shows its own bubbles on top of Bootstrap's messages. - Feedback
divin the wrong place.invalid-feedbackmust be a sibling that comes after the field (inside the same parent), or Bootstrap's CSS cannot show it. - Trusting browser validation alone. It can be bypassed in seconds. Always validate again on the server.
- Messages that only say "Invalid". Say what is expected: "Enter a 6-digit PIN code".
Key points
- Write rules with HTML attributes such as
required,type,minlengthandpattern. - Add
novalidate, check withcheckValidity()on submit, and addwas-validatedto show Bootstrap's styles andinvalid-feedbackmessages. setCustomValidity()adds custom rules that Bootstrap styles automatically.- Use
is-invalidandis-validfor errors returned by the server, andinvalid-tooltipwhen space is tight. - Link messages with
aria-describedby, and always validate again on the server.
Exercise
Build a ticket booking form for a cricket match in Lucknow with: name (required), age (number, 5 to 90), number of tickets (1 to 6) and a seat stand select whose first option "Choose a stand" has an empty value and must not be accepted. Use the was-validated pattern with a clear message under every field.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Match tickets</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-3" style="max-width: 520px">
<form id="tickets" class="row g-3" novalidate>
<div class="col-12">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" required>
<div class="invalid-feedback">Please enter your name.</div>
</div>
<div class="col-6">
<label for="age" class="form-label">Age</label>
<input type="number" class="form-control" id="age" min="5" max="90" required>
<div class="invalid-feedback">Age must be between 5 and 90.</div>
</div>
<div class="col-6">
<label for="qty" class="form-label">Tickets</label>
<input type="number" class="form-control" id="qty" min="1" max="6" value="1" required>
<div class="invalid-feedback">You can book 1 to 6 tickets.</div>
</div>
<div class="col-12">
<label for="stand" class="form-label">Stand</label>
<select class="form-select" id="stand" required>
<option value="">Choose a stand</option>
<option>North stand</option>
<option>Pavilion</option>
</select>
<div class="invalid-feedback">Please choose a stand.</div>
</div>
<div class="col-12"><button class="btn btn-primary" type="submit">Book</button></div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script>
const form = document.getElementById("tickets");
form.addEventListener("submit", (event) => {
if (!form.checkValidity()) {
event.preventDefault();
}
form.classList.add("was-validated");
});
</script>
</body>
</html>A required select fails when the chosen option has an empty value, so "Choose a stand" is rejected. min and max handle the number ranges without any extra JavaScript.
Quick Quiz
1. What does the novalidate attribute on a form do?
The rules still apply to checkValidity() and :invalid, but the browser stops showing its native popups.
2. When does a div with invalid-feedback become visible?
Bootstrap's CSS shows the feedback based on the field's invalid state.
3. How do you add a rule such as "passwords must match"?
setCustomValidity("message") marks a field invalid, and an empty string marks it valid again.
4. Why must the server validate the data again?
Anyone can send a request without your page's JavaScript, so the server is the real gatekeeper.
Learn Bootstrap with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
