A login page sounds simple but the details matter — validation, accessibility, password visibility, error states. Here's how to build one properly with Bootstrap 5.
Complete Login Page HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login — MyApp</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #f8f9fa; }
.login-card { max-width: 420px; }
.btn-brand { background: #fd4766; border: none; color: #fff; }
.btn-brand:hover { background: #e03355; color: #fff; }
.logo-text { color: #fd4766; font-weight: 800; font-size: 1.4rem; }
.divider { position: relative; text-align: center; margin: 20px 0; }
.divider::before { content: ''; position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #dee2e6; }
.divider span { background: #fff; padding: 0 12px; position: relative; color: #6c757d; font-size: 0.85rem; }
</style>
</head>
<body>
<div class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
<div class="login-card w-100">
<!-- Logo -->
<div class="text-center mb-4">
<div class="logo-text">MyApp</div>
<p class="text-muted small mt-1">Sign in to your account</p>
</div>
<!-- Card -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<!-- Alert (shown on error) -->
<div class="alert alert-danger d-none" id="loginError" role="alert">
Invalid email or password. Please try again.
</div>
<form id="loginForm" novalidate>
<!-- Email -->
<div class="mb-3">
<label for="email" class="form-label fw-semibold small">Email address</label>
<input type="email" class="form-control" id="email"
placeholder="you@example.com" required autocomplete="email">
<div class="invalid-feedback">Please enter a valid email address.</div>
</div>
<!-- Password -->
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<label for="password" class="form-label fw-semibold small mb-0">Password</label>
<a href="/forgot-password" class="text-muted small text-decoration-none"
style="font-size:0.8rem;">Forgot password?</a>
</div>
<div class="input-group">
<input type="password" class="form-control border-end-0" id="password"
placeholder="Enter your password" required autocomplete="current-password">
<button class="btn btn-outline-secondary border-start-0" type="button" id="togglePwd">
👁
</button>
<div class="invalid-feedback">Password is required.</div>
</div>
</div>
<!-- Remember me -->
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="remember">
<label class="form-check-label small" for="remember">Keep me signed in</label>
</div>
</div>
<!-- Submit -->
<button type="submit" class="btn btn-brand w-100 py-2 fw-semibold">
Sign In
</button>
</form>
<!-- Divider -->
<div class="divider"><span>or continue with</span></div>
<!-- Social -->
<div class="d-grid gap-2">
<button class="btn btn-outline-secondary d-flex align-items-center justify-content-center gap-2">
<svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
Continue with Google
</button>
<button class="btn btn-outline-secondary d-flex align-items-center justify-content-center gap-2">
<svg width="18" height="18" viewBox="0 0 24 24" fill="#1877F2"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
Continue with Facebook
</button>
</div>
</div>
</div>
<!-- Sign up link -->
<p class="text-center text-muted small mt-3">
Don't have an account?
<a href="/register" style="color:#fd4766;font-weight:600;">Create one free →</a>
</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Password toggle
document.getElementById('togglePwd').addEventListener('click', function() {
const input = document.getElementById('password')
input.type = input.type === 'password' ? 'text' : 'password'
this.textContent = input.type === 'password' ? '👁' : '🙈'
})
// Form validation
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault()
if (!this.checkValidity()) {
this.classList.add('was-validated')
return
}
// Your login logic here
console.log('Login submitted', {
email: document.getElementById('email').value,
remember: document.getElementById('remember').checked
})
})
</script>
</body>
</html>
What's in This Page
Vertical centering — min-vh-100 d-flex align-items-center on the container. The login card sits in the center of the viewport on any screen size.
Password show/hide — the input-group wraps the password field and toggle button so they look like one element. JavaScript toggles type between password and text.
Bootstrap validation — novalidate stops browser defaults, was-validated triggers Bootstrap's valid/invalid styles after the first submit attempt.
Error alert — a hidden d-none alert div that you show when the server returns an authentication error. Just remove d-none in your fetch callback.
Forgot password link — aligned to the right of the label using d-flex justify-content-between. Small detail but it's the expected UX pattern.
Making the Login Page Dark
Wrap the body content in a dark container:
<body style="background:#0d0d0d;" data-bs-theme="dark">
All Bootstrap components switch automatically. Change the card shadow to something visible on dark:
.card { box-shadow: 0 4px 24px rgba(0,0,0,0.4) !important; }
Frequently Asked Questions
Need a Full Bootstrap 5 Admin Dashboard?
Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.
Browse Templates →Use code FIRST30 for 30% off your first purchase.