Bootstrap 5 Forms: Validation, Layouts, and Custom Styles
Forms are the workhorses of the web. Contact forms, login pages, checkout flows, newsletter sign-ups — virtually every meaningful interaction a user has with your site involves a form. Bootstrap 5 gives you a surprisingly powerful toolkit to build them well, but most developers only scratch the surface, defaulting to a plain form-control class and calling it a day.
In this guide we go deeper. We’ll cover how Bootstrap 5 forms are structured, how to implement client-side validation the right way, how to control layout with the grid system and utility classes, and how to push beyond the defaults with custom styles that still stay on-brand. Every section includes working code examples you can drop straight into a project — whether you’re building from scratch or customising a premium template like Canvas Template.
Key Takeaways
Key Takeaways
- Bootstrap 5 dropped jQuery, making form validation lighter and easier to control with vanilla JS.
- Use
.was-validatedon the<form>element to trigger native HTML5 validation feedback styles. - The Bootstrap grid system (col-md-*, row, g-*) gives precise control over multi-column form layouts.
- Floating labels, input groups, and custom checkboxes are all built-in — no third-party plugins needed.
- SASS variables and CSS custom properties let you restyle every form component without overriding compiled CSS.
- Always pair visual validation with
aria-describedbyfor accessible error messaging. - Canvas Template ships with pre-built form sections you can adapt rather than building from zero.
Bootstrap 5 Form Fundamentals: Structure and Base Classes
Before diving into validation or custom styles, it pays to understand the core building blocks Bootstrap 5 provides. The framework organises form markup around a handful of consistent class patterns.
The most important wrapper is .mb-3 (or a .form-group-equivalent pattern using utility spacing) around each field pair. Each input gets .form-control, selects get .form-select, and labels get .form-label. Checkboxes and radios use .form-check as their wrapper.
<form>
<div class="mb-3">
<label for="emailInput" class="form-label">Email address</label>
<input type="email" class="form-control" id="emailInput"
placeholder="name@example.com">
<div class="form-text">We'll never share your email with anyone.</div>
</div>
<div class="mb-3">
<label for="roleSelect" class="form-label">Role</label>
<select class="form-select" id="roleSelect">
<option selected>Choose...</option>
<option value="dev">Developer</option>
<option value="design">Designer</option>
</select>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="agreeCheck">
<label class="form-check-label" for="agreeCheck">I agree to terms</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
A key Bootstrap 5 change: .form-group was removed. Spacing between fields is now handled entirely by margin utilities like .mb-3. This is more flexible but can catch developers migrating from Bootstrap 4 off guard.
Sizing variants — .form-control-sm and .form-control-lg — let you scale inputs to match your design density. Pair these with .btn-sm or .btn-lg for a consistent feel across a form section.
Form Layouts: Grid, Inline, and Horizontal
One of Bootstrap 5’s strongest form features is how cleanly it integrates with the grid system. You can go from a single-column stacked layout to a responsive multi-column form with a few extra classes and no custom CSS.
Multi-column grid layout — wrap fields in a .row and use .col-* classes on the field wrappers:
<form>
<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" required>
</div>
<div class="col-md-6">
<label for="lastName" class="form-label">Last name</label>
<input type="text" class="form-control" id="lastName" required>
</div>
<div class="col-12">
<label for="address" class="form-label">Address</label>
<input type="text" class="form-control" id="address"
placeholder="1234 Main St">
</div>
<div class="col-md-6">
<label for="city" class="form-label">City</label>
<input type="text" class="form-control" id="city">
</div>
<div class="col-md-4">
<label for="country" class="form-label">Country</label>
<select class="form-select" id="country">
<option selected disabled>Choose...</option>
<option>United Kingdom</option>
<option>United States</option>
</select>
</div>
<div class="col-md-2">
<label for="zip" class="form-label">Zip</label>
<input type="text" class="form-control" id="zip">
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Save address</button>
</div>
</div>
</form>
Notice the g-3 gutter class on the row — this controls the gap between columns and rows simultaneously without manual padding hacks. If you want more control over horizontal vs vertical gutters, use gx-* and gy-* separately.
Inline / horizontal forms are achieved using flexbox utilities. For a compact search-bar style layout, .row.row-cols-lg-auto combined with .g-3.align-items-center works well. If you’re new to Bootstrap 5’s alignment system, our guide on Bootstrap 5 Flexbox: Alignment Utilities That Actually Work covers the full set of options in depth.
Bootstrap Form Validation: Native HTML5 + Bootstrap Styles
Bootstrap 5 ships with its own validation UI layer that hooks into the browser’s native HTML5 Constraint Validation API. The trick is adding .was-validated to the <form> element on submit, which activates Bootstrap’s :valid and :invalid pseudo-class styles.
<form class="needs-validation" novalidate>
<div class="mb-3">
<label for="valEmail" class="form-label">Email</label>
<input type="email" class="form-control" id="valEmail" required>
<div class="valid-feedback">Looks good!</div>
<div class="invalid-feedback">Please enter a valid email address.</div>
</div>
<div class="mb-3">
<label for="valPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="valPassword"
minlength="8" required>
<div class="invalid-feedback">Password must be at least 8 characters.</div>
</div>
<button class="btn btn-primary" type="submit">Sign up</button>
</form>
<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>
The novalidate attribute on the form suppresses the browser’s default balloon tooltips, letting Bootstrap’s styled feedback divs take over. The JavaScript adds .was-validated on submit, which triggers the CSS to show either the green .valid-feedback or red .invalid-feedback message depending on the field’s validity state.
Server-side validation feedback is handled differently — you add .is-valid or .is-invalid directly to the input element on page render. This is the right approach when you need to reflect errors returned from a backend after form submission.
<input type="text" class="form-control is-invalid" id="username"
value="taken_user" aria-describedby="usernameError">
<div id="usernameError" class="invalid-feedback">
That username is already taken.
</div>
Note the aria-describedby linking the input to the error message — this is essential for screen reader users. For a complete walkthrough of accessible form patterns, see our post on How to Make a Bootstrap 5 Website Accessible (WCAG 2.1 AA).
Input Groups and Floating Labels
Two Bootstrap 5 features that add significant polish to forms without any custom CSS: input groups and floating labels.
Input groups let you attach prepended or appended text, icons, or buttons to an input:
<div class="input-group mb-3">
<span class="input-group-text">@</span>
<input type="text" class="form-control" placeholder="Username"
aria-label="Username">
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Search..."
aria-label="Search">
<button class="btn btn-outline-secondary" type="button">Go</button>
</div>
<div class="input-group mb-3">
<span class="input-group-text">£</span>
<input type="number" class="form-control" aria-label="Amount">
<span class="input-group-text">.00</span>
</div>
Floating labels are a modern UX pattern where the label starts as a placeholder and animates upward when the user focuses the field. Bootstrap 5 ships these natively:
<div class="form-floating mb-3">
<input type="email" class="form-control" id="floatEmail"
placeholder="name@example.com">
<label for="floatEmail">Email address</label>
</div>
<div class="form-floating mb-3">
<textarea class="form-control" placeholder="Leave a message"
id="floatMessage" style="height: 120px"></textarea>
<label for="floatMessage">Message</label>
</div>
One constraint: the placeholder attribute is required on the input for floating labels to work correctly. Bootstrap uses it internally for the CSS :placeholder-shown selector to determine label position. The placeholder value itself is never visible to users.
Custom Bootstrap Input Styles: SASS, CSS Variables, and Manual Overrides
Default Bootstrap inputs are fine for prototyping, but production projects almost always need brand-specific styling. There are three approaches, each with different trade-offs:
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| SASS variable overrides | Full theme customisation at build time | Clean, DRY, no specificity battles | Requires SASS build pipeline |
| CSS custom properties | Runtime theming, dark mode | Works without SASS, supports dynamic switching | Bootstrap 5 coverage is partial (improving) |
| Plain CSS overrides | Quick one-off tweaks | Simple, no tooling needed | Specificity issues, harder to maintain |
SASS variable overrides — create a _custom.scss file and import it before Bootstrap:
// _custom.scss
$input-border-radius: 0.5rem;
$input-border-color: #d1d5db;
$input-focus-border-color: #6366f1;
$input-focus-box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25);
$input-padding-y: 0.65rem;
$input-padding-x: 1rem;
$form-label-font-weight: 500;
$form-label-font-size: 0.875rem;
// main.scss
@import "custom";
@import "bootstrap";
CSS custom property overrides — useful if you’re using the compiled Bootstrap CDN and don’t have a build step:
:root {
--bs-border-radius: 0.5rem;
}
.form-control {
--bs-body-color: #111827;
border-color: #d1d5db;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-control:focus {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
outline: none;
}
.form-label {
font-weight: 500;
font-size: 0.875rem;
color: #374151;
}
If you’re using Canvas Template, the SASS architecture is already set up — you can drop your variable overrides into the provided _user-variables.scss partial and get a fully restyled form system without touching Bootstrap’s source files.
For dark mode-compatible forms, you’ll want to ensure your custom colour values are referenced through CSS variables rather than hardcoded hex values. Our post on How to Add Dark Mode to Any Bootstrap 5 HTML Template explains the pattern in detail.
Range Sliders, Switches, and File Inputs
Beyond text inputs, Bootstrap 5 brings consistent styling to several input types that browsers notoriously render inconsistently.
Range input:
<label for="priceRange" class="form-label">
Budget: <span id="rangeValue">500</span>
</label>
<input type="range" class="form-range" id="priceRange"
min="0" max="1000" step="50" value="500"
oninput="document.getElementById('rangeValue').textContent = this.value">
Toggle switch (renders a checkbox as an iOS-style switch):
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="notificationsSwitch" checked>
<label class="form-check-label" for="notificationsSwitch">
Email notifications
</label>
</div>
File input:
<div class="mb-3">
<label for="avatarUpload" class="form-label">Profile photo</label>
<input class="form-control" type="file" id="avatarUpload"
accept="image/png, image/jpeg">
</div>
Bootstrap 5’s file input finally looks consistent across browsers without any JavaScript plugins — something Bootstrap 4 couldn’t achieve cleanly. The role=”switch” attribute on toggle checkboxes also ensures screen readers announce the correct component semantics.
Form Accessibility Best Practices
A well-structured Bootstrap form is almost accessible by default — but almost isn’t good enough. Here are the gaps you need to close manually.
Every input must have a visible label. Never rely solely on placeholder text — it disappears when users start typing and has insufficient contrast in most browsers. If your design calls for a label-free layout, use .visually-hidden on the label element to keep it in the DOM for screen readers:
<div class="input-group">
<label for="searchField" class="visually-hidden">Search the site</label>
<input type="search" class="form-control" id="searchField"
placeholder="Search...">
<button class="btn btn-primary" type="submit">Search</button>
</div>
Error messages need programmatic association. Use aria-describedby on the input pointing to the ID of the error message container. When Bootstrap’s .invalid-feedback is revealed, the screen reader will announce it automatically because of this linkage.
Required fields should have both the HTML required attribute (for validation) and a visible indicator in the label for sighted users. A common pattern:
<label for="reqName" class="form-label">
Full name <span aria-hidden="true" class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="reqName" required
aria-required="true">
The aria-hidden="true" on the asterisk prevents screen readers from reading “asterisk” aloud — the aria-required="true" on the input covers the required state semantically instead.
Putting It All Together: A Complete Contact Form
Here’s a production-ready contact form that combines everything covered in this guide — grid layout, floating labels, input groups, validation, and accessibility attributes:
<section class="py-5 bg-light">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-7">
<h2 class="mb-4">Get in touch</h2>
<form class="needs-validation" novalidate>
<div class="row g-3">
<div class="col-md-6">
<div class="form-floating">
<input type="text" class="form-control" id="contactFirst"
placeholder="First" required
aria-describedby="contactFirstError">
<label for="contactFirst">First name *</label>
<div id="contactFirstError" class="invalid-feedback">
Please enter your first name.
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-floating">
<input type="text" class="form-control" id="contactLast"
placeholder="Last" required
aria-describedby="contactLastError">
<label for="contactLast">Last name *</label>
<div id="contactLastError" class="invalid-feedback">
Please enter your last name.
</div>
</div>
</div>
<div class="col-12">
<div class="input-group">
<span class="input-group-text">@</span>
<div class="form-floating flex-grow-1">
<input type="email" class="form-control" id="contactEmail"
placeholder="email" required
aria-describedby="contactEmailError">
<label for="contactEmail">Email address *</label>
</div>
</div>
<div id="contactEmailError" class="invalid-feedback d-block"
style="display:none!important">
Please enter a valid email.
</div>
</div>
<div class="col-12">
<div class="form-floating">
<textarea class="form-control" id="contactMessage"
placeholder="Message" style="height:140px"
required minlength="20"
aria-describedby="contactMessageError"></textarea>
<label for="contactMessage">Message *</label>
<div id="contactMessageError" class="invalid-feedback">
Please enter a message (20 characters minimum).
</div>
</div>
</div>
<div class="col-12 form-check ms-1">
<input class="form-check-input" type="checkbox"
id="privacyCheck" required
aria-describedby="privacyError">
<label class="form-check-label" for="privacyCheck">
I agree to the privacy policy *
</label>
<div id="privacyError" class="invalid-feedback">
You must accept the privacy policy.
</div>
</div>
<div class="col-12">
<button class="btn btn-primary btn-lg w-100" type="submit">
Send message
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
<script>
(() => {
'use strict';
document.querySelectorAll('.needs-validation').forEach(form => {
form.addEventListener('submit', e => {
if (!form.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
form.classList.add('was-validated');
});
});
})();
</script>
This is exactly the type of form section that ships pre-built in Canvas Template — ready to style, rearrange, and connect to your backend. If you’re building a portfolio site and need more context around how these sections fit into a larger page structure, our tutorial on Building a Portfolio Website With Bootstrap 5 (Step-by-Step) walks through the full page-building process.
If you’d prefer an AI-assisted approach to generating these layouts quickly, CanvasBuilder can scaffold full form sections — including validation logic and responsive grid layouts — from a plain-text description.
Frequently Asked Questions
Does Bootstrap 5 form validation work without JavaScript?
Partially. Browser-native HTML5 validation (the required, type, minlength attributes) works without any JS and will prevent form submission when constraints aren’t met. However, Bootstrap’s styled .valid-feedback and .invalid-feedback messages only appear when the .was-validated class is added to the form element, which requires the small JavaScript snippet shown above. Without it, users get the browser’s default validation bubbles instead of Bootstrap’s styled messages.
How do I reset validation state after a successful AJAX form submission?
Remove the .was-validated class from the form element and reset the form values using the native form.reset() method. This clears all :valid and :invalid pseudo-class states along with the field values:
form.classList.remove('was-validated');
form.reset();
Can I use Bootstrap 5 form validation with React or Vue?
Yes, but you’ll typically manage the validation state yourself rather than using Bootstrap’s JS. Add is-valid or is-invalid classes to inputs dynamically based on your component’s state, and conditionally render the .valid-feedback or .invalid-feedback elements. This server-side-style approach works cleanly in any component-based framework without needing Bootstrap’s vanilla JS snippet at all.
What’s the difference between form-control and form-select in Bootstrap 5?
.form-control is intended for text-based inputs: type="text", type="email", type="password", textarea, and so on. .form-select is specifically for <select> elements and includes the custom dropdown arrow styling and consistent cross-browser appearance. Applying .form-control to a <select> will work in Bootstrap 5 but won’t give you the custom arrow — use .form-select for dropdowns.
How do I style Bootstrap 5 form inputs for dark mode?
Bootstrap 5.3+ introduced data-bs-theme="dark" which you can apply to a wrapping element or the <html> tag, and form components will update their colours automatically using CSS custom properties. For earlier versions or more granular control, you’ll need to override variables like $input-bg, $input-color, and $input-border-color through SASS or define dark-mode-specific CSS rules under a [data-theme="dark"] selector. Our guide on adding dark mode to Bootstrap 5 templates covers this in full.
Ready to Build Better Bootstrap 5 Forms?
Bootstrap 5 forms are capable of far more than most templates show out of the box. If you want a head start with professionally designed form sections, contact pages, login layouts, and checkout flows — all built on clean, validated Bootstrap 5 code — Canvas Template has everything ready to customise.
Prefer to describe what you want and let AI handle the scaffolding? CanvasBuilder generates full Bootstrap 5 page sections — including forms with validation — in seconds.
Skip the setup — build it free
Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.
Canvas Team
Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.
More from the Canvas Blog