Forms are where users give you information, so they need to be clear and easy to complete. Browsers style form controls differently, which makes plain forms look inconsistent. With Bootstrap forms, every input, select and checkbox gets the same clean look across browsers, with clear focus states. You still write normal HTML forms underneath, so everything from the HTML Forms lesson applies.
Text inputs, labels and help text
The basic pattern in Bootstrap forms is a wrapper with a bottom margin, a <label> with form-label, and an input with form-control. The label’s for must match the input’s id. That link lets screen readers announce the label and makes the label clickable, which helps on phones.
form-controlworks on text-like inputs (text,email,tel,password,date…) and on<textarea>. It makes them full width with consistent padding and a focus ring.form-selectstyles a<select>. It replaced Bootstrap 4’scustom-select.form-textis for help text under a field. Connect it to the input witharia-describedbyso screen readers read it too.form-control-lg,form-control-sm,form-select-lgandform-select-smchange the size.
Add the disabled attribute to a field the user cannot use at the moment; it is greyed out and skipped when the form is submitted. Use readonly instead when the value should be visible and submitted but not edited, such as a fixed order number. Good autocomplete values like email and tel let phones fill fields in one tap.
Laying out Bootstrap forms with the grid
Bootstrap 5 removed the old form-group class. Use spacing utilities such as mb-3 between fields, or put fields in a row g-3 and size them with column classes. On phones every field is full width; from 768px, short fields can sit side by side.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A registration form with grid layout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container py-4" style="max-width: 720px;">
<h1 class="h3 mb-3">Workshop registration</h1>
<form class="row g-3" id="regForm">
<div class="col-md-6">
<label for="firstName" class="form-label">First name</label>
<input type="text" class="form-control" id="firstName" autocomplete="given-name">
</div>
<div class="col-md-6">
<label for="lastName" class="form-label">Last name</label>
<input type="text" class="form-control" id="lastName" autocomplete="family-name">
</div>
<div class="col-12">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" aria-describedby="emailHelp" autocomplete="email">
<div id="emailHelp" class="form-text">We send the joining link to this address.</div>
</div>
<div class="col-md-6">
<label for="city" class="form-label">City</label>
<select id="city" class="form-select">
<option value="">Choose…</option>
<option>Chennai</option>
<option>Coimbatore</option>
<option>Madurai</option>
<option>Puducherry</option>
</select>
</div>
<div class="col-md-6">
<label for="session" class="form-label">Preferred date</label>
<input type="date" class="form-control" id="session">
</div>
<div class="col-12">
<label for="notes" class="form-label">Anything we should know?</label>
<textarea class="form-control" id="notes" rows="3"></textarea>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>
<p id="out" class="mt-3 text-success"></p>
</div>
<script>
document.getElementById('regForm').addEventListener('submit', (event) => {
event.preventDefault();
const name = document.getElementById('firstName').value.trim() || 'friend';
const city = document.getElementById('city').value || 'your city';
document.getElementById('out').textContent = 'Thanks, ' + name + '! We will contact you about the session in ' + city + '.';
});
</script>
</body>
</html>Checkboxes, radios, switches and ranges
Checkboxes and radios use a wrapper with form-check, an input with form-check-input and a label with form-check-label. Add form-check-inline to the wrapper to place options side by side. Radios in the same group must share a name. A switch is a checkbox in a form-check form-switch wrapper; add role="switch" so assistive technology announces it as on or off. For sliders, use <input type="range" class="form-range">.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Checks, radios, switch and range</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container py-4" style="max-width: 560px;">
<fieldset class="mb-3">
<legend class="fs-6 fw-semibold">Topics you like</legend>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="topicHtml" checked>
<label class="form-check-label" for="topicHtml">HTML and CSS</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="topicUx">
<label class="form-check-label" for="topicUx">UX design</label>
</div>
</fieldset>
<fieldset class="mb-3">
<legend class="fs-6 fw-semibold">Batch</legend>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="batch" id="batchMorning" checked>
<label class="form-check-label" for="batchMorning">Morning</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="batch" id="batchEvening">
<label class="form-check-label" for="batchEvening">Evening</label>
</div>
</fieldset>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" role="switch" id="whatsapp">
<label class="form-check-label" for="whatsapp">Send reminders on WhatsApp</label>
</div>
<label for="hours" class="form-label">Hours per week: <strong id="hoursOut">5</strong></label>
<input type="range" class="form-range" min="1" max="20" value="5" id="hours">
</div>
<script>
const hours = document.getElementById('hours');
hours.addEventListener('input', () => {
document.getElementById('hoursOut').textContent = hours.value;
});
</script>
</body>
</html>Grouping related checkboxes or radios in a <fieldset> with a <legend> gives the group a name, so a screen reader says “Batch, Morning, radio button” rather than just “Morning”.
Floating labels and input groups
Floating labels start inside the field and move up when you type. Wrap the input and label in form-floating. Two rules matter: the <input> must come before the <label>, and the input needs a placeholder attribute (its text is not shown, but Bootstrap uses it to detect an empty field).
Input groups attach text or buttons to the side of a field: a “+91” country code before a phone number, “@” before a username, or a button after a search box. Wrap them in input-group and put the extra text in input-group-text.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Floating labels and input groups</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container py-4" style="max-width: 480px;">
<div class="form-floating mb-3">
<input type="email" class="form-control" id="floatEmail" placeholder="[email protected]">
<label for="floatEmail">Email address</label>
</div>
<div class="form-floating mb-4">
<input type="password" class="form-control" id="floatPass" placeholder="Password">
<label for="floatPass">Password</label>
</div>
<label for="phone" class="form-label">Mobile number</label>
<div class="input-group mb-3">
<span class="input-group-text">+91</span>
<input type="tel" class="form-control" id="phone" inputmode="numeric" autocomplete="tel-national">
</div>
<label for="coupon" class="form-label">Coupon code</label>
<div class="input-group">
<input type="text" class="form-control" id="coupon">
<button class="btn btn-outline-secondary" type="button">Apply</button>
</div>
</div>
</body>
</html>Validation styles
Bootstrap forms can show green and red states with messages. It builds on the browser’s own validation: attributes such as required, type="email" and minlength decide what is valid. You add novalidate to the form to stop the browser’s default pop-up bubbles, then add the class was-validated to the form when the user submits. The CSS then shows any invalid-feedback message next to fields that fail.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap validation</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container py-4" style="max-width: 480px;">
<form id="joinForm" novalidate>
<div class="mb-3">
<label for="fullName" class="form-label">Full name</label>
<input type="text" class="form-control" id="fullName" required minlength="3">
<div class="invalid-feedback">Please enter at least 3 characters.</div>
</div>
<div class="mb-3">
<label for="mail" class="form-label">Email</label>
<input type="email" class="form-control" id="mail" required>
<div class="invalid-feedback">Please enter a valid email, like [email protected].</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="agree" required>
<label class="form-check-label" for="agree">I agree to the class rules</label>
<div class="invalid-feedback">You must agree before joining.</div>
</div>
<button type="submit" class="btn btn-primary">Join</button>
</form>
<p id="status" class="mt-3" role="status">Press Join with empty fields to see the messages.</p>
</div>
<script>
const form = document.getElementById('joinForm');
form.addEventListener('submit', (event) => {
event.preventDefault();
form.classList.add('was-validated');
const status = document.getElementById('status');
if (form.checkValidity()) {
status.textContent = 'All good! Your request has been sent.';
status.className = 'mt-3 text-success';
} else {
status.textContent = 'Please fix the highlighted fields.';
status.className = 'mt-3 text-danger';
}
});
</script>
</body>
</html>For errors found on the server, such as “This email is already registered”, add is-invalid directly to the input and include an invalid-feedback element after it. is-valid and valid-feedback work the same way for success.
Watch out: Front-end validation only helps the user. Anyone can bypass it, so always validate again on the server, for example in PHP as shown in PHP Forms. Also, never use a placeholder as the only label: it disappears while typing and often has low contrast. For UX rules that make Bootstrap forms easier to complete, read Form Design UX.
