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
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
"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.
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.
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.
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 connected | for="email" | Screen readers announce labels |
| Input type correct | type="email" | Triggers email keyboard on mobile |
| Password toggle named | aria-label | Blind users hear "Show password" |
| Autocomplete hints | autocomplete | Password managers work |
| Focus visible | Border + shadow | Keyboard 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 | ~3KB | No framework |
| JS size | ~8 lines | Password toggle only |
| Dependencies | Zero | Vanilla HTML/CSS/JS |
| Layout shift | None | Fixed 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
- Using GET instead of POST. Credentials leak into logs. Non-negotiable security issue.
- No
autocompleteattributes. Breaks password managers. Users hate typing credentials manually. - Labels not connected to inputs. Fails screen readers. Use
forandid. - Password toggle without aria-label. Screen readers can't identify the button's purpose.
- No visible focus state. Keyboard users can't see where they are in the form.
- Revealing whether email exists. "Email not found" tells attackers which emails are registered.
- No rate limiting. Server-side issue, but worth flagging — brute-force happens fast without it.
- Disabling submit without explanation. Users get stuck. Either show why or keep it enabled.
❓ 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.