What is a Bootstrap form?
Bootstrap doesn’t ship a form widget the way it ships a modal. What it gives you is a set of classes that sit on top of plain HTML controls, plus grid layouts and validation states. Classes such as form-control, form-select and form-check make text inputs, selects and checkboxes look the same in every browser without any custom CSS.
They build on Bootstrap’s Reboot form styles and keep controls consistent across browsers and devices (Bootstrap docs, 5.3). Eight of them do most of the work: form-control, form-label, form-select, form-check, form-switch, form-range, form-text and form-floating.
They come with the Bootstrap framework and attach to ordinary form markup, so a basic form needs no custom CSS.
The form element itself gets no default styling. Almost every control does get display: block and width: 100%, which is why fields stack vertically until a layout class says otherwise (Bootstrap docs, 5.3).
Set the right type on each input too (email, number and so on), so the browser can apply email verification and number selection.
Bootstrap’s job ends at the styling. Getting the data submitted and stored is up to the backend.
Which Bootstrap version the classes belong to
Old snippets are everywhere. Bootstrap 3 still accounts for 28.2% of the websites that use Bootstrap, roughly 4.1% of all websites (W3Techs, 28 October 2026), so markup from that generation keeps turning up next to current examples.
This article uses Bootstrap 5.3, the line the docs mark as latest, with 5.3.8 current when this was written. Bootstrap 6 Alpha 1 arrived on October 8, 2026 and rewrites several form patterns:
.form-select is gone, and .form-control goes on select elements instead- Validation is opt-in through
data-bs-validate rather than .was-validated - Input groups no longer use
.has-validation - Checkboxes and radios take
.check or .radio directly on the input
The alpha also raises the browser floor to Chrome and Edge 130, Firefox 132 and Safari 18, where v5 reaches back to Chrome and Firefox 60 and Safari 12 (Bootstrap blog, October 8, 2026).
The maintainers call it an alpha that can still change before beta, and they plan a 5.4.0 release before announcing an end-of-life date for Bootstrap 5. For production forms I’d stay on 5.3 for now.
How to build a basic Bootstrap form
Load the Bootstrap 5.3 stylesheet, add a form element, and give each field a wrapper where a .form-label label and a .form-control input share matching for and id values. That’s a working form.
- Load the Bootstrap 5.3 CSS file from a CDN or npm. Form styling needs no JavaScript from the bundle.
- Wrap the fields in a
form element and put each field in a div with .mb-3. - Pair every
label that carries .form-label with its input through for and id. - Add
.form-control to text inputs and textareas, and .form-select to selects. - Put hint text in
.form-text and point to it with aria-describedby on the input. - Group checkboxes in
.form-check, then finish with a submit button that has btn btn-primary.
<form>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" aria-describedby="emailHelp">
<div id="emailHelp" class="form-text">We only use it for the weekly digest.</div>
</div>
<div class="mb-3">
<label for="plan" class="form-label">Plan</label>
<select class="form-select" id="plan">
<option selected>Choose a plan</option>
<option value="1">Free</option>
<option value="2">Team</option>
</select>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="terms">
<label class="form-check-label" for="terms">I accept the terms</label>
</div>
<button type="submit" class="btn btn-primary">Subscribe</button>
</form>
Spacing comes from margin utilities. Bootstrap recommends sticking to margin-bottom and using a single direction across the whole form (Bootstrap docs, 5.3).
The button is an easy place to slip. Inside a form it submits by default, so one without a type can send the form by accident.
The submit button here uses btn-primary, and any of the button variants Bootstrap offers works inside a form.
Form control classes and the elements they style
Text-like inputs and textareas get .form-control. Selects take .form-select, checkboxes, radios and switches share .form-check, and sliders use .form-range.
| Control | HTML element | Class | Detail |
|---|
| Text, email, password, textarea | input, textarea | .form-control | Full width, block display |
| Select menu | select | .form-select | Replaces .custom-select from v4 |
| Checkbox, radio, switch | input | .form-check, .form-check-input, .form-check-label (plus .form-switch) | .form-check-inline puts options on one row |
| Range slider | input type range | .form-range | Replaces .custom-range from v4 |
| File and color | input type file or color | .form-control (plus .form-control-color) | Color input needs type="color" |
Text-like controls accept the standard HTML input types, including email, password, number and date. More detail on them sits in the guide to Bootstrap input fields.
Height changes with .form-control-sm or .form-control-lg, and selects use .form-select-sm and .form-select-lg.
Put the disabled attribute on a control, or on a fieldset to disable everything inside it. Button-like links such as a.btn stay focusable in a disabled fieldset, so they need tabindex="-1" and aria-disabled (Bootstrap docs, 5.3). The docs write aria-disabled="disabled", but ARIA only defines true and false, so use aria-disabled="true".
The readonly attribute keeps the value visible and blocks edits. Swap .form-control for .form-control-plaintext to drop the field styling and keep the margin and padding.
Switches have one dated quirk. Safari 17.4 and later supports a native switch attribute that adds haptic feedback on iOS, and Bootstrap’s custom-styled switches look the same with or without it (Bootstrap docs, 5.3).
Input groups for icons and buttons
An input group attaches text or a button to the side of a field, as long as both sit inside .input-group. Icons count as text here.
.input-group-text styles a prefix or suffix such as @ or $- Buttons go straight into the group, no wrapper needed
- Resizing the whole group takes
.input-group-sm or .input-group-lg - Several inputs and several addons can share one group
Bootstrap 5 removed the v4 wrappers .input-group-prepend and .input-group-append, so old markup needs flattening. Icons from the Bootstrap icon set fit inside .input-group-text without extra classes.
Validation styles also only work on input groups that hold a single input (Bootstrap docs, 5.3), which catches people out.
Which layout fits: vertical, horizontal, inline or floating labels?
Most forms are fine with the default vertical stack. Horizontal grids suit short labels on wide screens, inline rows work for compact filters, and floating labels help when a short form is short on vertical space.
| Layout | Key classes | Best for | Watch out for |
|---|
| Vertical | .mb-3 per field | Login, short forms | Tall on long forms |
| Horizontal | .row, .col-sm-*, .col-form-label | Settings, admin screens | More markup per field |
| Inline | .row-cols-lg-auto, .g-3, .col-12 | Search, filters | Hidden labels still needed |
| Floating labels | .form-floating | Short mobile forms | Placeholder required, input first |
Vertical form
Vertical is the default, and it needs nothing beyond .mb-3.
There’s no grid markup and no breakpoints to manage, which keeps the class count low. The catch is length. A form with many fields turns into one long column.
Horizontal form
- Put
.row on each form group - Give the label
.col-sm-2 and .col-form-label, and wrap the control in .col-sm-10 - Use
.col-form-label-sm or .col-form-label-lg so the label matches .form-control-sm or .form-control-lg - Wrap radio groups in a
fieldset with .row, and use a legend with .col-form-label
The columns come from the grid system, so the usual breakpoints apply. Below the chosen one, the labels sit above the controls.
Labels line up neatly with their fields, though you pay for that with more markup than a vertical form.
Inline form
Inline forms in v5 are a grid arrangement, not a class, because .form-inline was removed.
Putting row row-cols-lg-auto g-3 align-items-center on the form lines the fields up in one row from the lg breakpoint up, and each field sits in a .col-12 wrapper. Hide the labels with .visually-hidden instead of deleting them, because every control still needs a name for assistive technology.
Below lg, the .col-12 wrappers stack the fields, which keeps the row usable on a small viewport. Compact and easy to scan. But long labels and many fields wrap into a second row.
Floating labels
Floating labels had a rough moment in 2025. Bootstrap 5.3.5 (April 4, 2025) was a hotfix for an Autoprefixer regression that left them permanently floated in Firefox (Bootstrap blog, 2025).
The labels are CSS-only and detect an empty field through :placeholder-shown, which sets strict rules:
- a non-empty
placeholder on every input (it stays invisible) - the input before the label in the markup, because the CSS uses a sibling selector
- an explicit
height on textareas, never the rows attribute - selects work but always show the label floated, and selects with
size or multiple are not supported
Those constraints come from the Bootstrap 5.3 docs. Nielsen Norman Group credits the pattern with saving space on mobile and keeping the label visible while people type, and rates it better than using a placeholder as the label (NN/g, 2014, reviewed 2018).
It still ranks second to a label placed outside the field when screen space allows. Fields holding text look less noticeable and can pass for prefilled, and some assistive technology reads placeholders unreliably. NN/g points to Warby Parker’s site as its example of the pattern done well.
Bootstrap’s own accessibility note lands in the same place: visible label text is generally the best approach (Bootstrap docs, 5.3). So the two sources agree.
My rule of thumb is floating labels for a short form like a login, and labels above the fields for anything longer or rarely used.
Contact, login and registration forms: what each one needs
These forms ask for different things. Login is two fields in a vertical stack. Contact puts a name-and-email row above a textarea, and registration wants a two-column grid with a required terms checkbox and validation.
Login form
A vertical stack with two fields works, plus a remember-me checkbox. The submit button goes full width.
Each field gets its own .mb-3 wrapper. Use an email or username input and a type="password" input, with matching autocomplete values so password managers can fill them. A .form-check handles remember me, and a .d-grid wrapper stretches the button.
Floating labels fit here because the form is short. More variations live in the guide to building a login form in Bootstrap.
Contact form
A contact form pairs name and email in one row and gives the message a full-width textarea.
<form class="row g-3 needs-validation" novalidate>
<div class="col-md-6">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="col-md-6">
<label for="mail" class="form-label">Email</label>
<input type="email" class="form-control" id="mail" required>
</div>
<div class="col-12">
<label for="msg" class="form-label">Message</label>
<textarea class="form-control" id="msg" rows="5" required></textarea>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Send</button>
</div>
</form>
The novalidate attribute turns off the browser’s default error bubbles so custom styles can take over, and required marks the fields that must be filled.
For a fuller build, see this walkthrough of a contact form in Bootstrap.
Registration form
- First name and last name as
.col-md-6 pairs - Email at full width
- Password and confirm password side by side
- A required terms checkbox in
.form-check - An input group, if the form needs a username prefix or a show-password button
Registration forms collect the most fields, so clear validation feedback matters most here.
How Bootstrap form validation works
Bootstrap doesn’t do the checking itself. The browser handles that through the HTML5 constraint validation API, and Bootstrap paints the result on inputs, textareas, selects and checks (Bootstrap docs, 5.3).
On submit, a script adds .was-validated to the form, which turns the browser’s :valid and :invalid states into green and red styling. .valid-feedback or .invalid-feedback elements supply the text.
Which route you take depends mostly on how much control you want over the error messages.
Browser default validation
Add required or a type such as email and stop there. No novalidate, no script, no .was-validated.
That means zero JavaScript, and the docs recommend it, alongside server-side classes, as the accessible alternative to custom styles. The downside is that the bubbles look different in every browser and cannot be styled with CSS, although the wording can still be changed with setCustomValidity.
W3C’s Understanding page for WCAG 2.2 success criterion 3.3.1 covers native validation and points out its drawbacks. Depending on the browser, messages vanish, ignore zoom, read generically, and expose only the first error per submission, so developers have to judge whether it is accessibility supported (W3C Understanding docs, updated June 2026).
Custom styles with .was-validated
Custom styles need novalidate on the form and a hook class for the script to find. Each control also gets feedback elements beside it.
document.querySelectorAll('.needs-validation').forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
})
})
The handler is a few lines of JavaScript that blocks submission when checkValidity() returns false and always adds the class, so valid fields turn green too.
The CSS never mentions .needs-validation. It is only the selector the starter script looks for.
For dynamic submissions through Ajax, remove .was-validated after the response to reset the form’s appearance.
You get full control over messages and tooltips, and over both states. The same docs page opens with a warning, though, that custom validation styles and tooltips are not exposed to assistive technologies.
Server-side classes
.is-invalid and .is-valid work without a .was-validated parent.invalid-feedback is supported alongside them- The backend renders the classes after its own checks
Point aria-describedby at the feedback id on every invalid field. The attribute takes several ids, so a field can reference an addon or hint and its error together (Bootstrap docs, 5.3).
This is the other route the docs treat as accessible. The cost is a round trip for every error.
Feedback messages and tooltips
For plain text under the control, .valid-feedback and .invalid-feedback do the job. .valid-tooltip and .invalid-tooltip float a styled bubble instead, and the parent needs position: relative (the .position-relative utility does it).
Bootstrap 5.3.0 added local CSS variables for validation colors, --bs-form-valid-color and --bs-form-invalid-color among them, and they adapt to dark mode.
The v6 alpha moves the styling to the :user-invalid pseudo-class, which activates only after a user interacts with a field (Bootstrap migration guide, v6.0.0-alpha.1).
Making a Bootstrap form accessible
Bootstrap handles the visual layer and none of the error wiring. Every field still needs an accessible name and a linked hint or error message, and anything offering a choice needs a fieldset. For a broader checklist, see this guide to building accessible forms.
The label numbers are not great. WebAIM found missing form input labels on 51% of the top million home pages in February 2026, up from 48.2% a year earlier.
The 240,869 pages detected with Bootstrap averaged 63.3 errors against 56.1 for the whole sample. WebAIM adds that extra errors cannot always be pinned on the technology (WebAIM Million, 2026).
Names, labels and groups
A label is the simplest source of an accessible name. Where none fits, Bootstrap lists a .visually-hidden label, aria-labelledby, a title or aria-label, and warns that assistive technology can fall back to the placeholder when none exists.
Radio and checkbox sets belong in a fieldset with a legend, because user agents often read the legend before each option’s label (W3C technique H71, updated January 2026). The technique is optional when every option’s label already says it all.
WCAG 3.3.2 requires labels or instructions wherever input is requested, while correct association is judged separately under 1.3.1 (W3C Understanding docs). That makes it a Level A rule.
Errors and required fields
Orange’s accessibility guidelines, in a form article built on Boosted, its Bootstrap-based library, walk through the error routine step by step (Orange, updated June 2026):
- Set
aria-invalid="true" on each field in error, next to .is-invalid - Write the message in plain text that names the field and the fix, never color alone
- Link it with
aria-describedby or aria-labelledby - Move focus to the first field in error
Orange also keeps the submit button active in all circumstances and calls a disabled-until-valid button a bad idea.
The required attribute tells assistive technology a field is mandatory, and the visual marker cannot rely on color alone. An asterisk needs a sentence at the top of the form that explains it.
ARIA and native elements
Use the native element first. WebAIM’s 2026 data shows pages with ARIA averaged 59.1 detected errors, against 42.0 without it, and the pages were also more complex, so ARIA is not necessarily the cause.
A label element needs no ARIA attributes at all, which leaves aria-describedby for hints and errors.
Why is a Bootstrap form not working?
Most broken Bootstrap forms come down to a handful of mistakes. A missing novalidate, a script that never runs, .was-validated hard-coded in the markup, feedback that isn’t a sibling placed after its control, or an input group without .has-validation.
| Symptom | Cause | Fix |
|---|
| Browser bubbles, no Bootstrap styling | Form lacks novalidate | Add it, because the browser blocks submission before the script runs |
| Nothing happens on submit | Script missing, or its selector matches no form | Load it after the markup and match the hook class |
| Every empty required field is red on load | .was-validated is in the HTML | Add it only inside the submit handler |
| Feedback text never shows | Feedback is not a sibling that follows its control | Place it after the control inside the same parent, because the CSS uses the sibling selector |
| Addon corners or feedback look off | Input group has no .has-validation | Add it, and with floating labels show the feedback through JavaScript |
A few smaller traps come from the docs. Validation icons on selects appear only with .form-select, not .form-control, and in v5 they skip selects with multiple.
- A label placed before its control cannot be styled by the validation CSS without custom JavaScript
- Bootstrap 3 snippets using
.has-error or .help-block style nothing, because v5 keys off .was-validated, :valid, :invalid and the .is-* classes
Rules that span fields, such as a password match, sit outside what required and type checks cover. Use setCustomValidity or the server-side classes.
What changed in Bootstrap forms from version 4 to version 5
Bootstrap 5 dropped the v4 layout and custom control classes in favor of margin utilities, the grid and one set of consolidated components. It also made .form-label mandatory.
| Bootstrap 4 | Bootstrap 5 | Change |
|---|
.form-group, .form-row, .form-inline | .mb-3, .row with .g-*, .row-cols-* | Layout moved to utilities and the grid |
.custom-control (checkbox, radio, switch) | .form-check, .form-check.form-switch | Native and custom merged |
.custom-select, .custom-range, .form-control-range | .form-select, .form-range | One class per control |
.custom-file, .form-control-file | .form-control on input type="file" | Native variants dropped |
.input-group-prepend, .input-group-append | Direct children of .input-group | Wrappers removed |
- Every
label now needs .form-label .form-text no longer sets display, so the HTML element decides inline or block- Controls use
min-height instead of a fixed height where possible - Floating labels are new in 5.0, promoted from a v4-era example
- Support for Internet Explorer 10 and 11 is gone
Those changes come from the official v5 migration notes. Treat them as a checklist when upgrading old markup.
What the 5.x releases added
Version 5.2.0 refined the border radius on form controls and added the .form-check-reverse modifier. Then 5.3.0 moved form controls, checks and switches to CSS variables for color modes, and validation colors became color-mode aware (Bootstrap migration docs).
Bootstrap 4 reached end of life on January 1, 2023, a date the team announced in its July 19, 2022 post for v4.6.2. Old v4 forms still render, but they no longer get fixes.
Bootstrap Form FAQ
Does a Bootstrap form need jQuery?
No. Bootstrap 5.0 dropped jQuery and rewrote its plugins in plain JavaScript.
Form styling is pure CSS, and the validation starter script relies on standard browser methods such as checkValidity().
Can Bootstrap forms work in React, Vue, Rails or Django?
Yes. Wrapper libraries render the same Bootstrap classes: React-Bootstrap v2 for React, BootstrapVueNext for Vue, the bootstrap_form gem for Rails and django-bootstrap5 for Django.
Match the wrapper’s version to your Bootstrap version, because v4 and v5 class names differ.
How do you customize Bootstrap form styles?
Set Sass variables such as $input-btn-padding-y and $input-btn-padding-x, which buttons and form controls share. Place them after the functions import and before the variables import.
For runtime tweaks, the 5.3 validation colors also respond to CSS variables like –bs-form-invalid-color.
Do Bootstrap forms support dark mode?
Yes, from Bootstrap 5.3. Set data-bs-theme=”dark” on the html element or on any wrapper, a single form included, and the controls and validation colors follow it (Bootstrap docs, 5.3).
Is there a drag-and-drop Bootstrap form builder?
Not in the official docs. They offer markup examples for each control and layout, validation states included, but no visual builder.
Third-party generators exist. Check their class names against 5.3, since older ones can emit v3 or v4 classes.
What Happens After a Bootstrap Form Is Submitted
Once submitted, the field values go to the URL in the form’s action attribute and Bootstrap does nothing more. Validation belongs to server code, along with storage and whatever response comes back.
OWASP’s Input Validation Cheat Sheet calls client-side checks bypassable and gives them one job, immediate feedback.
- Validate every field on the server
- Return failures with
.is-invalid and aria-describedby - Add the
.was-validated script last
That order follows what each layer guarantees: only the server check holds when a request skips the browser. OWASP also lists drop-down values among the inputs that need an exact membership check.
This holds for Bootstrap 5.3.8, verified on October 28, 2026. A stable Bootstrap 6 release would change the client hooks and leave the server-side rule untouched.