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 centeringmin-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 validationnovalidate 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

Use Bootstrap's built-in validation classes. Add needs-validation to the form, required to inputs, and novalidate to prevent browser default validation. In JavaScript call form.checkValidity() and add was-validated to the form to show Bootstrap's valid/invalid states.
Use min-vh-100 d-flex align-items-center on the container: <div class='container min-vh-100 d-flex align-items-center'>. This makes the container full viewport height and centers content vertically with flexbox.
Wrap the password input in an input-group and add a button that toggles the input type between password and text: btn.addEventListener('click', () => { input.type = input.type === 'password' ? 'text' : 'password' }). Update the button icon to match the state.

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.

Related Components