What does the HTML for a basic Bootstrap login form look like?
Short. You can read the whole thing on one screen, and most of it is labels and inputs.
Getting a working form onto the page goes like this:
- Load Bootstrap’s stylesheet in the head and add the viewport meta tag.
- Wrap the page content in a
.container. - Pair every input with a label. The label gets
form-label and a for attribute, and the input gets form-control and a matching id. - Add the remember me checkbox, the submit button and the forgot password link.
<form action="/login" method="post">
<div class="mb-3">
<label for="loginEmail" class="form-label">Email address</label>
<input type="email" class="form-control" id="loginEmail" name="email" required>
</div>
<div class="mb-3">
<label for="loginPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="loginPassword" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="rememberMe" name="remember">
<label class="form-check-label" for="rememberMe">Remember me</label>
</div>
<button type="submit" class="btn btn-primary w-100">Sign in</button>
<p class="mt-3 mb-0 text-center"><a href="/forgot-password">Forgot your password?</a></p>
</form>
form-label styles the label text and adds the space between label and field.- Full width, padding, a border and a focus ring all come from
form-control. - The checkbox and its label stay on one line thanks to
form-check. btn btn-primary styles the submit button, and w-100 stretches it across the form.- Spacing between the field groups is just
mb-3, a margin utility.
Bootstrap’s form layout documentation calls margin utilities the easiest way to group labels, controls and feedback. It also advises giving every button an explicit type, because a button inside a form defaults to submit.
Phones need the viewport meta tag in the head, or mobile browsers render the page at desktop width. The tag is <meta name="viewport" content="width=device-width, initial-scale=1">.
Fields beyond email and password (selects, textareas, file inputs) follow the same label and control pattern. Bootstrap’s wider set of form controls covers them.
How to center a Bootstrap login form and keep it responsive
Put the form in a column inside a row and add justify-content-center to the row. Then set the column width per breakpoint. Vertical centering needs a flex container with align-items-center and min-vh-100 around everything.
Horizontal centering with the grid
As of Bootstrap v5.3.8, the default .container caps its width at 540px, 720px, 960px, 1140px and 1320px from the sm, md, lg, xl and xxl breakpoints (Bootstrap containers documentation).
Inside that container, Bootstrap’s twelve-column grid system sizes the login column.
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<!-- login form here -->
</div>
</div>
</div>
On phones col-12 keeps the form full width. It narrows through col-sm-8 and col-md-6 as the screen grows, then settles at a third of the container with col-lg-4 on desktops.
Breakpoint classes work mobile-first. A class such as col-md-6 applies from 768px upward, and smaller screens fall back to the nearest smaller class.
Vertical centering with flex utilities
Use min-vh-100, not vh-100.
The vh-100 class fixes the height at the full viewport, so a form taller than a short landscape screen overflows. min-vh-100 sets a minimum instead, and the wrapper grows when the content needs more room.
<main class="d-flex align-items-center min-vh-100">
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<!-- login form here -->
</div>
</div>
</div>
</main>
The wrapper becomes a flex container through d-flex, and align-items-center centers the container vertically. min-vh-100 gives it at least the full viewport height.
Test the result in Chrome DevTools device mode at 575px, 576px, 768px and 992px. Bootstrap’s max-width queries use values such as 575.98px, so the layout flips exactly at those thresholds.
Bootstrap login form layouts and when each one fits
Where the form lives decides the layout. A standalone login page works with a centered card or a split-screen. A sign-in button on content pages calls for a modal. Floating labels suit a form with two fields, and stacked labels stay the default once fields carry helper text.
| Layout | Key classes | Best for | Limitation |
|---|
| Stacked labels | form-label, form-control | Forms with helper text | Uses more vertical space |
| Floating labels | form-floating | Short two-field forms | Placeholder required, input before label |
| Centered card | card, card-body, shadow | Standalone login pages | Extra nesting inside the grid |
| Split-screen | row, col-lg-6, d-none d-lg-block | Branded app entry pages | Image column disappears below lg |
| Modal | modal, data-bs-toggle | Secondary sign-in point | Needs the JS bundle, no autofocus |
Floating labels
Bootstrap’s floating labels documentation has some picky rules for the markup, and the element order is the one that trips people up first.
- The input comes first and the label second, because the CSS relies on a sibling selector.
- Every input needs a non-empty placeholder. The CSS-only effect reads
:placeholder-shown to detect an empty field, and the placeholder text itself stays hidden. - In input groups, validation feedback goes outside
.form-floating but inside the .input-group, and it has to be shown with JavaScript.
<div class="form-floating mb-3">
<input type="email" class="form-control" id="floatingEmail" placeholder="name@example.com">
<label for="floatingEmail">Email address</label>
</div>
For a login form with two fields, I’d take floating labels over stacked ones. Once helper text sits under the inputs, standard labels read better.
Modal login form
A modal suits sites where signing in is a secondary action, such as a button in the navbar. Bootstrap’s modal component handles the backdrop and closes on the Escape key by default.
- Modals are built with HTML, CSS and JavaScript, so the Bootstrap JS bundle has to be loaded.
- The trigger button carries
data-bs-toggle="modal" and data-bs-target="#loginModal". - The
autofocus attribute has no effect inside a modal, so focus the email field when the modal opens. - Only one modal can be open at a time. A sign-in modal can hand over to a password reset modal through the same toggle attributes.
const loginModal = document.getElementById('loginModal')
const loginEmail = document.getElementById('loginEmail')
loginModal.addEventListener('shown.bs.modal', () => {
loginEmail.focus()
})
Split-screen and card layouts
A centered card puts the form inside Bootstrap’s card component, using card, card-body and a shadow utility, on a flex-centered page.
Split-screen layouts give the form half of a two-column row and a branded image the rest. Hide the image below lg with d-none d-lg-block, otherwise phones scroll past a photo before they reach the fields.
Bootstrap’s examples collection includes a Sign-in page, described as a custom form layout for a simple sign in form.
How does validation work in a Bootstrap login form?
Bootstrap leans on HTML5 attributes like required and type="email". Add novalidate to the form, then let a short script put the was-validated class on it at submit. That class switches on the green and red field styles and reveals the matching feedback messages.
Browser defaults
Browser defaults need no script. Remove novalidate and the browser shows its own popup on submit.
Wording and styling differ by browser and operating system, and CSS cannot restyle those popups. The text can be changed in JavaScript with setCustomValidity. All modern browsers implement the constraint validation API behind it (Bootstrap validation documentation, v5.3.8).
Custom styles with was-validated
Add novalidate to the form so the browser drops its popups, then let a short JavaScript handler call checkValidity on submit.
<form class="needs-validation" novalidate>
<div class="mb-3">
<label for="loginEmail" class="form-label">Email address</label>
<input type="email" class="form-control" id="loginEmail" required>
<div class="invalid-feedback">Enter a valid email address.</div>
</div>
<div class="mb-3">
<label for="loginPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="loginPassword" required>
<div class="invalid-feedback">Enter your password.</div>
</div>
<button type="submit" class="btn btn-primary w-100">Sign in</button>
</form>
<script>
document.querySelectorAll('.needs-validation').forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
})
})
</script>
Bootstrap scopes the :valid and :invalid styles to the was-validated parent class. Without it, a required field with no value looks invalid on page load.
After a dynamic submit with Ajax, remove was-validated from the form to reset its appearance.
Server-side classes
When the server decides a login failed, it marks fields with is-invalid and is-valid instead. These classes need no was-validated parent, and invalid-feedback works with them.
<input type="password" class="form-control is-invalid" id="loginPassword"
name="password" aria-describedby="loginFeedback" required>
<div id="loginFeedback" class="invalid-feedback">
The email or password is incorrect.
</div>
Mark both fields is-invalid and show one message, so the response does not reveal which credential was wrong. Bootstrap’s server-side example links each message to its field through aria-describedby.
Accessibility fixes the default validation styles need
Bootstrap’s validation documentation states that its client-side custom validation styles and tooltips are not exposed to assistive technologies. Red borders and feedback text reach sighted users only. A Bootstrap form needs an announcement, aria-invalid and correct autocomplete tokens layered on top of the classes.
The same page disagrees with itself on the remedy. The notice at the top recommends server-side validation or browser defaults, while the server-side section says Bootstrap recommends client-side validation (v5.3.8 documentation).
For a login form I’d keep the custom styles and patch the gaps yourself.
Announce errors to screen readers
Add an alert region to the form and fill it from the submit handler. The ARIA alert role makes screen readers read new text without moving focus.
<form class="needs-validation" novalidate>
<div id="formStatus" class="visually-hidden" role="alert"></div>
<div class="mb-3">
<label for="loginEmail" class="form-label">Email address</label>
<input type="email" class="form-control" id="loginEmail"
aria-describedby="emailFeedback" required>
<div id="emailFeedback" class="invalid-feedback">Enter a valid email address.</div>
</div>
<!-- the password field follows the same pattern -->
</form>
form.addEventListener('submit', event => {
const status = document.getElementById('formStatus')
status.textContent = ''
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
form.querySelectorAll(':invalid').forEach(field => field.setAttribute('aria-invalid', 'true'))
status.textContent = 'Check the email and password fields.'
form.querySelector(':invalid').focus()
}
form.classList.add('was-validated')
})
Here aria-invalid marks the failing fields and the alert region reads out a one-line summary. Focus then jumps to the first invalid field.
Clear the region at the start of each submit. A repeated identical message is not announced again.
Autocomplete tokens and password managers
WCAG 2.2 success criterion 3.3.8 (Level AA) bars cognitive function tests in authentication unless an alternative or an assisting mechanism exists. Password manager autofill and copy and paste both count as mechanisms, according to the W3C understanding document.
<input type="email" class="form-control" id="loginEmail" name="email"
autocomplete="username" required>
<input type="password" class="form-control" id="loginPassword" name="password"
autocomplete="current-password" required>
The same W3C document adds that a login form meeting criterion 1.3.5 (Input Purpose), with accessible names on its controls, lets browsers and password managers fill the fields.
A script that cancels paste on the password field fails the criterion for anyone who relies on a password manager. Scripts that stop the browser from populating fields fail it too. And a one-time-code input made of six boxes, where pasting fills only the first, shows up in the W3C document as a problem case.
Labels, focus and contrast
Every input keeps a real label element. A placeholder disappears as soon as the user types, which is why Bootstrap’s floating labels still require the label in the markup.
Wider guidance on accessible forms covers focus order, grouping and error text.
Bootstrap 5 raised its color contrast ratio from 3:1 to 4.5:1 for the default theme (v5.0.0 migration notes). Override btn-primary with a brand color and that guarantee is gone, so recheck the button text.
How to add a show and hide password toggle
Place the password input and a button inside an input group, then switch the input’s type between password and text on click. Give the button type="button" and an accessible name, or it submits the form and screen readers announce an unnamed control.
<div class="mb-3">
<label for="loginPassword" class="form-label">Password</label>
<div class="input-group">
<input type="password" class="form-control" id="loginPassword" name="password" required>
<button type="button" class="btn btn-outline-secondary" id="togglePassword"
aria-label="Show password">
<i class="bi bi-eye" aria-hidden="true"></i>
</button>
</div>
</div>
const passwordField = document.getElementById('loginPassword')
const toggleButton = document.getElementById('togglePassword')
toggleButton.addEventListener('click', () => {
const show = passwordField.type === 'password'
passwordField.type = show ? 'text' : 'password'
toggleButton.setAttribute('aria-label', show ? 'Hide password' : 'Show password')
toggleButton.firstElementChild.className = show ? 'bi bi-eye-slash' : 'bi bi-eye'
})
The eye and eye-slash glyphs come from Bootstrap Icons, loaded through its own stylesheet.
If the field also shows validation errors, add has-validation to the input group and place invalid-feedback inside it. Bootstrap added the class in v4.6.0 and v5.0 to fix the missing border radius on input groups with feedback (v5 migration guide).
For the announcement, either change the label with the state or keep one fixed label and use aria-pressed. Do both and a screen reader says “Hide password, pressed”.
Edge is the odd one out here. Microsoft Edge adds a native reveal button to password inputs, so Edge users see two eye icons next to a custom toggle.
Microsoft’s Edge documentation gives the removal rule, and users or enterprises can disable the native button globally from Edge 87 onward.
input[type="password"]::-ms-reveal {
display: none;
}
The same documentation advises weighing the native button before removing it, because it hides itself when focus leaves the field, when a script modifies the field and when a password manager autofills it. Keep the custom toggle when the design needs one icon style everywhere, and hide the native button to avoid the duplicate.
What changes between Bootstrap 4 and Bootstrap 5 login forms?
Bootstrap 4 login markup breaks in a handful of places. form-group and form-row are gone, custom-control became form-check, ml-* and mr-* turned into ms-* and me-*, and data attributes gained a bs prefix.
| Bootstrap 4 class | Bootstrap 5 replacement | Where it shows in a login form |
|---|
| form-group | mb-3 on a wrapper div | Spacing between email and password |
| custom-control custom-checkbox | form-check | Remember me checkbox |
| ml-2, mr-2 | ms-2, me-2 | Spacing around links and icons |
| input-group-append | Button as a direct child of input-group | Password toggle |
| data-toggle, data-target | data-bs-toggle, data-bs-target | Modal trigger button |
form-row and form-inline give way to grid classes such as row g-3btn-block is replaced by a d-grid wrappersr-only is now visually-hidden- labels now require the
form-label class
Bootstrap announced in July 2022 that Bootstrap 4 reaches end of life on January 1, 2023, with 4.6.2 as one of the last releases. Start new login forms on Bootstrap 5 and convert older snippets with the table above.
Removed classes fail silently. A form-group wrapper left in Bootstrap 5 markup loads no styles, so the fields lose their spacing without any console error.
Bootstrap 5.0.0 also dropped jQuery. Modal scripts copied from older tutorials call jQuery methods that no longer exist.
// Bootstrap 4 with jQuery
$('#loginModal').modal('show')
// Bootstrap 5
bootstrap.Modal.getOrCreateInstance('#loginModal').show()
Security limits of a Bootstrap login form
Bootstrap styles the form and nothing more. It does not check passwords, keep sessions, stop bots or encrypt traffic, so every protection comes from the server and the transport layer.
Start with the checks. was-validated and checkValidity run in the browser, where anyone can skip them with developer tools or a direct request, so the server repeats every check.
Serve the login page and every authenticated page over TLS only (OWASP Authentication Cheat Sheet), and submit with method="post", since OWASP’s CSRF guidance rules out GET for requests that change state.
For CSRF, render a hidden input whose value the server generates. OWASP says the token should exist before authentication too, to stop login CSRF.
Failures should look identical. OWASP asks for the same response whether the ID or password was wrong, the account does not exist, or the account is locked or disabled. Registration and password reset follow the same rule.
None of this lives in the HTML. The backend that receives the POST decides who gets a session.
Use the framework’s built-in CSRF protection before writing custom token code. OWASP recommends built-in implementations first, and notes that cross-site scripting defeats every CSRF defense.
Bot checks cost money past a point. Google Cloud’s documentation, which now files reCAPTCHA under Fraud Defense, lists assessments as free up to 10,000 per month, and the Premium tier charges a flat $8 from there up to 100,000. Even the free allowance requires a billing account on the Google Cloud project.
Puzzle challenges that go beyond recognizing objects count as cognitive function tests under WCAG 3.3.8, so they need an alternative path. Object-recognition CAPTCHAs are excepted at Level AA, but not at Level AAA (3.3.9). Score-based reCAPTCHA keys run without a visible puzzle.
WebAuthn, the W3C standard developed with the FIDO Alliance, is the technology behind passkeys. A passkey flow lets the form shrink to an email field and a sign-in button, and moves the work to credential storage and recovery on the server.
Read together, the sources point one way. Keep the form friendly to password managers, prefer score-based bot checks over puzzles, and put every decision on the server.
You can skip the password form altogether when users never type a password, like these cases:
- apps with single sign-on only, where an identity provider’s hosted page collects credentials
- passkey-first products
Bootstrap Login Form FAQ
What separates a login form from a registration form?
Registration creates an account. A login form only verifies one that already exists. Registration adds a confirm password field and uses autocomplete=”new-password”, where login uses current-password.
Can I use a free third-party template for a login page?
Yes. Check which Bootstrap version it targets and what the license says, and read any scripts you didn’t write before they go live.
A template supplies markup only, so authentication still needs your own server code.
How do I change the colors of the form?
Override Sass variables after importing Bootstrap’s functions file and before its variables file, because every Bootstrap variable carries the !default flag and keeps any value assigned earlier (Bootstrap Sass documentation).
Since v5.3.0, validation colors also respond to CSS variables such as –bs-form-invalid-color. Bootswatch publishes ready-made themes built on the same Sass variables.
How do I add social login buttons?
Stack outline buttons below the submit button, each linking to a server route that starts the provider’s OAuth flow. Bootstrap only styles the link, using d-grid gap-2 for full-width stacking and Bootstrap Icons for the brand glyphs.
Follow each provider’s branding rules for the button wording.
How do I put icons inside the email and password fields?
Place a span with the input-group-text class before the input, inside an input group, and put a Bootstrap Icons glyph such as bi-envelope or bi-lock in it.
Mark the icon aria-hidden. The label already names the field.
Does the form work without a PHP or Node backend?
No. The form only sends a POST request, so a server route has to read the fields, check the credentials and start the session.
Set action to that route and give every input a name attribute, since the server reads values by name. Express needs express.urlencoded to parse the body, while PHP exposes it in $_POST.
Should the submit button stay disabled until the fields are valid?
Usually no. A disabled button cannot take focus and gives no reason for the block, so keyboard and screen reader users get stuck without an explanation.
Keep it enabled and let was-validated show the errors on submit. If the design demands a disabled state, set the button’s disabled property from checkValidity inside an input listener.
What to Build on the Server First
A Bootstrap login form is ready for production once the server covers transport and request forgery first, and credentials last. The order matters because each layer protects the next.
- HTTPS with a POST route
- CSRF token on the form
- Credential check with one generic failure message
The token travels inside the form, so it needs a secured connection before it protects anything. A credential check on an unsecured, forgeable request protects little.
The generic failure message has a price. OWASP’s Authentication Cheat Sheet calls the confusion for legitimate users a UX matter and leaves the final call to how critical the application and its data are.
Verified against the Bootstrap v5.3.8 documentation on October 29, 2026: its validation page still says custom styles are not accessible while the team works on a solution. A release that fixes that retires the extra ARIA announcements.