Forms & Input

Copy. Paste. Ship.

Home › Code Snippets › HTML › Login Form

Part of our complete guide: HTML & Form Snippets →

Form Component · HTML + CSS + JS

Login Form

Complete sign-in form with email, password, show/hide toggle, remember me, social buttons, and validation. Fully responsive.

Last updated: October 2026 · 8 min read

A login form has two inputs — email and password — plus a submit button. The password input toggles between type="password" and type="text" with a small eye button. Always use method="POST" — never GET, which puts credentials in the URL.

What you get: Full login card with email, password with toggle, "Forgot password" and "Remember me" row, primary submit button, social sign-in options, and client-side validation. All accessible and mobile-friendly.

Why login forms matter more than they look

The login form is where users decide whether they trust your app. A confusing or broken login screen — even a small thing like the password toggle not working — makes users abandon. Worse, security issues in login forms (like using GET, or storing plain text passwords, or not rate-limiting) are among the most common audit failures in web applications.

The version below implements the standard security and accessibility patterns: method="POST", proper autocomplete attributes so password managers work, label-input connections, and a show/hide toggle that respects accessibility guidelines. None of this is fancy, but each one is something an audit will flag if it's missing.

The styling matches the rest of your site — dark background, blue accent, rounded inputs. Social buttons are included but inert — you'd wire those up to your OAuth provider. The form is entirely client-side; you'd send the POST to your backend to actually authenticate.

🎬 Live Login Form

Try the password toggle, the validation, and the submit button.

Complete code

<form class="login-card" method="POST" action="/api/login">
  <div class="login-logo">
    <div class="login-logo-icon">✨</div>
    <div class="login-title">Welcome back</div>
    <div class="login-subtitle">Sign in to your account</div>
  </div>

  <div class="form-group">
    <label class="form-label" for="email">Email</label>
    <div class="form-input-wrap">
      <input
        type="email" id="email" name="email"
        class="form-input" placeholder="you@example.com"
        autocomplete="email" required>
    </div>
  </div>

  <div class="form-group">
    <label class="form-label" for="password">Password</label>
    <div class="form-input-wrap">
      <input
        type="password" id="password" name="password"
        class="form-input" placeholder="Enter your password"
        autocomplete="current-password" required minlength="6">
      <button type="button" class="password-toggle"
              id="passwordToggle" aria-label="Show password">👁️</button>
    </div>
  </div>

  <div class="form-row">
    <label class="form-checkbox">
      <input type="checkbox" name="remember">
      <span>Remember me</span>
    </label>
    <a href="/forgot-password" class="form-link">Forgot password?</a>
  </div>

  <button type="submit" class="form-submit">Sign in</button>

  <div class="form-divider">or continue with</div>

  <div class="social-buttons">
    <button type="button" class="social-btn">G Google</button>
    <button type="button" class="social-btn">🐙 GitHub</button>
  </div>

  <div class="form-footer">
    Don't have an account? <a href="/signup" class="form-link">Sign up</a>
  </div>
</form>

<style>
.login-card {
  width: 100%; max-width: 380px;
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 1rem;
  padding: 2rem;
  box-shadow: 0 20px 50px rgba(0,0,0,0.3);
}

.login-logo { text-align: center; margin-bottom: 1.5rem; }
.login-logo-icon {
  width: 48px; height: 48px;
  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
  border-radius: 12px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 24px; margin-bottom: 0.75rem;
}
.login-title { color: #fff; font-weight: 800; font-size: 1.35rem; margin-bottom: 0.25rem; }
.login-subtitle { color: #94a3b8; font-size: 0.82rem; }

.form-group { margin-bottom: 1rem; }
.form-label {
  display: block; color: #cbd5e1;
  font-size: 0.78rem; font-weight: 600;
  margin-bottom: 0.4rem;
}
.form-input-wrap { position: relative; }
.form-input {
  width: 100%;
  background: #0f172a;
  border: 1px solid #334155;
  color: #e2e8f0;
  padding: 0.7rem 0.9rem; padding-right: 2.5rem;
  border-radius: 0.5rem;
  font-size: 0.88rem; font-family: inherit;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.form-input:focus {
  outline: none;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.form-input::placeholder { color: #64748b; }

.password-toggle {
  position: absolute; right: 0.5rem; top: 50%;
  transform: translateY(-50%);
  background: transparent; border: none;
  color: #64748b; cursor: pointer;
  padding: 0.35rem; border-radius: 0.375rem;
  font-size: 0.85rem; font-family: inherit;
}
.password-toggle:hover { color: #cbd5e1; }

.form-row {
  display: flex; justify-content: space-between;
  align-items: center; margin-bottom: 1.25rem;
  font-size: 0.78rem;
}
.form-checkbox {
  display: flex; align-items: center; gap: 0.5rem;
  color: #cbd5e1; cursor: pointer;
}
.form-checkbox input {
  width: 14px; height: 14px;
  accent-color: #3b82f6; cursor: pointer;
}
.form-link { color: #60a5fa; text-decoration: none; font-weight: 600; }
.form-link:hover { text-decoration: underline; }

.form-submit {
  width: 100%; background: #3b82f6; color: #fff;
  border: none; padding: 0.75rem; border-radius: 0.5rem;
  font-weight: 700; font-size: 0.88rem;
  cursor: pointer; font-family: inherit;
  transition: background 0.2s;
}
.form-submit:hover { background: #2563eb; }
.form-submit:disabled { background: #475569; cursor: not-allowed; }

.form-divider {
  display: flex; align-items: center; gap: 0.75rem;
  margin: 1.25rem 0; color: #64748b;
  font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.05em;
}
.form-divider::before, .form-divider::after {
  content: ''; flex: 1; height: 1px; background: #334155;
}

.social-buttons { display: flex; gap: 0.5rem; }
.social-btn {
  flex: 1; background: #0f172a;
  border: 1px solid #334155; color: #cbd5e1;
  padding: 0.6rem; border-radius: 0.5rem;
  cursor: pointer; font-family: inherit;
  font-size: 0.82rem; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
  gap: 0.4rem; transition: all 0.2s;
}
.social-btn:hover { background: #334155; border-color: #475569; color: #fff; }

.form-footer {
  text-align: center; margin-top: 1.5rem;
  color: #94a3b8; font-size: 0.8rem;
}
</style>

<script>
const passwordInput = document.getElementById('password');
const passwordToggle = document.getElementById('passwordToggle');

passwordToggle.addEventListener('click', () => {
  const isPassword = passwordInput.type === 'password';
  passwordInput.type = isPassword ? 'text' : 'password';
  passwordToggle.textContent = isPassword ? '🙈' : '👁️';
  passwordToggle.setAttribute('aria-label',
    isPassword ? 'Hide password' : 'Show password');
});
</script>

How the login form works

1. Method POST, never GET

The form uses method="POST". GET puts the email and password in the URL — visible in browser history, server logs, and Referer headers. POST sends them in the request body, which is never logged by default. This is non-negotiable for any credential form.

2. autocomplete attributes make password managers work

The email input uses autocomplete="email", and the password uses autocomplete="current-password". These attributes tell password managers (1Password, Bitwarden, browser autofill) which field is which. Without them, autofill often gets confused and users have to type credentials manually.

3. The password toggle

The eye button switches type="password" to type="text", revealing the password. It also updates its own aria-label so screen readers announce "Show password" / "Hide password" correctly. This pattern is WCAG 2.1 AA compliant — the button has an accessible name, and it doesn't auto-hide (users control visibility).

4. HTML5 validation, not JS-only

The email input uses type="email" — the browser validates format automatically. The password uses required minlength="6". Both prevent form submission if invalid. This is instant feedback without JavaScript. Server-side validation is still required for security.

5. Labels are connected to inputs

Every <label> has a for attribute matching the input's id. This means clicking the label focuses the input, and screen readers announce the label when the input is focused. This is the most basic accessibility requirement, and it's still missed on 30% of forms in production.

6. Social buttons are type="button"

The Google and GitHub buttons use type="button", not type="submit". Without this, clicking them would submit the form (and their click handlers would run, but the form would also POST — confusing behavior). Setting type="button" makes them inert until you wire up OAuth.

🔬 Edge cases

The problems that break login forms in production.

Using GET instead of POST

Credentials end up in browser history, server logs, and referrer headers. Anyone with access to the logs sees plaintext passwords. Fix: always method="POST". Server should also reject GET requests to the login endpoint.

No autocomplete attributes

Password managers don't know which field is which. Users type credentials manually or reuse weak passwords. Fix: add autocomplete="email" and autocomplete="current-password". For signup, use new-password to trigger a password generator.

No rate limiting

Attackers can brute-force passwords in seconds. Fix: rate limit login attempts on the server (e.g., 5 failed attempts per IP per minute). Optionally add a CAPTCHA after repeated failures. This is server-side work — nothing on the frontend can enforce it.

Disabling the submit button without reason

If the button is disabled and there's no clear reason, users are stuck. Either show a message ("Fill in both fields") or keep the button enabled and validate on submit. Avoid silently disabling.

Revealing whether email exists

"Email not found" vs "Wrong password" tells attackers which emails are registered. Fix: always show "Invalid email or password" regardless of which is wrong. This is a standard security pattern.

No CSRF token

Without CSRF protection, a malicious site can submit the login form on behalf of a logged-in user. Fix: include a CSRF token in the form (server-side generated). This is the server's responsibility, not the form's, but the form must include the hidden input.

Autofocusing the wrong field

If autofocus is on the password field, screen readers skip the email label. Fix: autofocus the email field, or don't autofocus at all. Keyboard users expect to tab through from the start.

Password toggle changes the form layout

If the toggle button is inside the input, long passwords push the eye icon off-screen. Fix: padding-right: 2.5rem on the input, so the text never overlaps the toggle button.

♿ Accessibility

Login forms fail accessibility more often than any other component. Here's what this one does right.

Requirement Implementation Why
Labels connectedfor="email"Screen readers announce labels
Input type correcttype="email"Triggers email keyboard on mobile
Password toggle namedaria-labelBlind users hear "Show password"
Autocomplete hintsautocompletePassword managers work
Focus visibleBorder + shadowKeyboard users see where they are

The one thing most forms miss: error messages connected via aria-describedby. When validation fails, the input should have aria-describedby="email-error" pointing to the error message element. Screen readers then announce the error when the input is focused.

⚡ Performance

Aspect Value Notes
CSS size~3KBNo framework
JS size~8 linesPassword toggle only
DependenciesZeroVanilla HTML/CSS/JS
Layout shiftNoneFixed max-width, no images

Login page speed matters more than you think. Slow login pages cause 20-40% abandonment. The form itself is tiny — no images, no heavy CSS. The slowness usually comes from loading full site assets (navbars, hero images, tracking scripts) on the login page. Consider using a stripped-down layout for login.

⚠️ Common mistakes

❓ Frequently asked questions

How do I create a login form in HTML?

Use a <form> element with <input type='email'> for the email and <input type='password'> for the password. Add a submit button. Wrap everything in a styled container. Add validation attributes like required and minlength.

How do I add a show/hide password toggle?

Use a button next to the password input that toggles the input's type attribute between 'password' and 'text'. This requires a few lines of JavaScript. There's no pure CSS method that works reliably.

Should I use client-side or server-side validation?

Both. Client-side (HTML5 required, pattern, type attributes) gives instant feedback. Server-side validation is mandatory for security — clients can bypass any client-side validation.

How do I make a login form accessible?

Label every input with a <label>. Use autocomplete attributes ('email', 'current-password'). Add error messages linked via aria-describedby. Support keyboard navigation with tab. Never disable the submit button without explaining why.

What is the difference between GET and POST for login forms?

Always use POST for login. GET puts credentials in the URL, which is logged in browser history and server logs. POST sends them in the request body, which is never logged by default.

Should I include a remember me checkbox?

Only if you implement it securely. A 'remember me' checkbox should create a long-lived token, not store the password. For most sites, offering this without the security work is worse than not offering it.

How do I add 'Forgot password' functionality?

Link to a separate reset page where users enter their email. Send a time-limited token via email. Never reveal whether an email exists in your system — show 'If an account exists, we sent a link' regardless.

What should the login button say?

'Sign in' (not 'Login' or 'Submit'). Match the language to the page title — if the page says 'Sign in', the button should too. 'Submit' is generic and unfriendly.

🔗 Related snippets

🌐 External resources

← All HTML Snippets