Secure password input fields
Password fields implement security best practices including proper autocomplete, show/hide toggle, and strength indicators.
- Use type='password' with correct autocomplete attribute
- Provide accessible show/hide toggle for password visibility
- Show password strength indicator with requirements
- Never store or transmit passwords in plain text
- Support password managers with proper input names
Rule Details
Secure password fields balance security with usability while supporting password managers and accessibility.
Code Examples
<!-- Login form -->
<form method="POST" action="/login">
<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
/>
</div>
<div>
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
autocomplete="current-password"
required
minlength="8"
/>
</div>
<button type="submit">Sign In</button>
</form><!-- Registration form -->
<form method="POST" action="/register">
<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
/>
</div>
<div>
<label for="new-password">Create Password</label>
<input
type="password"
id="new-password"
name="password"
autocomplete="new-password"
required
minlength="12"
aria-describedby="password-requirements"
/>
<p id="password-requirements">
At least 12 characters with uppercase, lowercase, and numbers.
</p>
</div>
<button type="submit">Create Account</button>
</form>Why It Matters
Properly implemented password fields improve security by working with password managers, helping users create strong passwords, and providing accessible controls for all users.
Autocomplete Attributes
| Scenario | Attribute | Purpose |
|---|---|---|
| Login form | autocomplete="current-password" | Fill existing password |
| Registration | autocomplete="new-password" | Suggest strong password |
| Username | autocomplete="username" | Associate with password |
| Email login | autocomplete="email" | For email-based auth |
React Password Field with Toggle
import { useState, useId } from 'react'
interface PasswordFieldProps {
label: string
name: string
autocomplete: 'current-password' | 'new-password'
value: string
onChange: (value: string) => void
error?: string
minLength?: number
}
export function PasswordField({
label,
name,
autocomplete,
value,
onChange,
error,
minLength = 8,
}: PasswordFieldProps) {
const [showPassword, setShowPassword] = useState(false)
const inputId = useId()
const errorId = useId()
return (
<div className="password-field">
<label htmlFor={inputId} className="password-label">
{label}
</label>
<div className="password-input-wrapper">
<input
type={showPassword ? 'text' : 'password'}
id={inputId}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
autoComplete={autocomplete}
minLength={minLength}
required
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? errorId : undefined}
className="password-input"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
aria-pressed={showPassword}
className="password-toggle"
>
{showPassword ? (
<EyeOffIcon aria-hidden="true" />
) : (
<EyeIcon aria-hidden="true" />
)}
</button>
</div>
{error && (
<p id={errorId} role="alert" className="password-error">
{error}
</p>
)}
</div>
)
}
function EyeIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" width={20} height={20} {...props}>
<path
fill="currentColor"
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"
/>
</svg>
)
}
function EyeOffIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" width={20} height={20} {...props}>
<path
fill="currentColor"
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"
/>
</svg>
)
}Password Strength Indicator
interface PasswordStrengthProps {
password: string
}
interface StrengthResult {
score: 0 | 1 | 2 | 3 | 4
label: string
requirements: {
met: boolean
text: string
}[]
}
function calculateStrength(password: string): StrengthResult {
const requirements = [
{
met: password.length >= 12,
text: 'At least 12 characters',
},
{
met: /[a-z]/.test(password),
text: 'One lowercase letter',
},
{
met: /[A-Z]/.test(password),
text: 'One uppercase letter',
},
{
met: /[0-9]/.test(password),
text: 'One number',
},
{
met: /[^a-zA-Z0-9]/.test(password),
text: 'One special character',
},
]
const metCount = requirements.filter((r) => r.met).length
const scoreMap: Record<number, { score: StrengthResult['score']; label: string }> = {
0: { score: 0, label: 'Very weak' },
1: { score: 1, label: 'Weak' },
2: { score: 1, label: 'Weak' },
3: { score: 2, label: 'Fair' },
4: { score: 3, label: 'Good' },
5: { score: 4, label: 'Strong' },
}
return {
...scoreMap[metCount],
requirements,
}
}
export function PasswordStrength({ password }: PasswordStrengthProps) {
const strength = calculateStrength(password)
if (!password) return null
return (
<div className="password-strength" aria-live="polite">
<div className="strength-bar">
<div
className={`strength-fill strength-${strength.score}`}
style={{ width: `${(strength.score + 1) * 20}%` }}
/>
</div>
<span className="strength-label">{strength.label}</span>
<ul className="strength-requirements">
{strength.requirements.map((req, index) => (
<li key={index} className={req.met ? 'met' : 'unmet'}>
<span aria-hidden="true">{req.met ? '✓' : '○'}</span>
<span className={req.met ? 'sr-only' : undefined}>
{req.met ? 'Complete: ' : 'Incomplete: '}
</span>
{req.text}
</li>
))}
</ul>
</div>
)
}Complete Registration Form
'use client'
import { useState } from 'react'
import { PasswordField } from './password-field'
import { PasswordStrength } from './password-strength'
export function RegistrationForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [errors, setErrors] = useState<Record<string, string>>({})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const newErrors: Record<string, string> = {}
if (password.length < 12) {
newErrors.password = 'Password must be at least 12 characters'
}
if (password !== confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match'
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// Submit form
// await register({ email, password })
}
return (
<form onSubmit={handleSubmit} className="registration-form">
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
required
/>
</div>
<div className="form-group">
<PasswordField
label="Create Password"
name="password"
autocomplete="new-password"
value={password}
onChange={setPassword}
error={errors.password}
minLength={12}
/>
<PasswordStrength password={password} />
</div>
<div className="form-group">
<PasswordField
label="Confirm Password"
name="confirmPassword"
autocomplete="new-password"
value={confirmPassword}
onChange={setConfirmPassword}
error={errors.confirmPassword}
minLength={12}
/>
</div>
<button type="submit" className="submit-button">
Create Account
</button>
</form>
)
}Styling
.password-field {
margin-bottom: 1rem;
}
.password-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.password-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-input {
width: 100%;
padding: 0.75rem 3rem 0.75rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.password-input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
.password-input[aria-invalid='true'] {
border-color: #dc2626;
}
.password-toggle {
position: absolute;
right: 0.75rem;
padding: 0.25rem;
background: none;
border: none;
color: #666;
cursor: pointer;
}
.password-toggle:hover {
color: #333;
}
.password-toggle:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
border-radius: 2px;
}
.password-error {
margin-top: 0.5rem;
color: #dc2626;
font-size: 0.875rem;
}
/* Strength indicator */
.password-strength {
margin-top: 0.75rem;
}
.strength-bar {
height: 4px;
background: #e5e5e5;
border-radius: 2px;
overflow: hidden;
}
.strength-fill {
height: 100%;
transition: width 0.3s, background-color 0.3s;
}
.strength-0 { background: #dc2626; }
.strength-1 { background: #f97316; }
.strength-2 { background: #eab308; }
.strength-3 { background: #84cc16; }
.strength-4 { background: #22c55e; }
.strength-label {
display: block;
margin-top: 0.25rem;
font-size: 0.75rem;
color: #666;
}
.strength-requirements {
list-style: none;
padding: 0;
margin: 0.5rem 0 0;
font-size: 0.875rem;
}
.strength-requirements li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
}
.strength-requirements .met {
color: #22c55e;
}
.strength-requirements .unmet {
color: #666;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}Security Best Practices
| Practice | Implementation |
|---|---|
| Never log passwords | Exclude from logging, monitoring |
| Hash passwords | Use bcrypt, Argon2, or scrypt |
| Enforce minimum length | At least 12 characters |
| Check against breaches | Use Have I Been Pwned API |
| Rate limit attempts | Prevent brute force attacks |
| Use HTTPS | Encrypt in transit |
Breach Check Integration
// Check password against known breaches (k-anonymity safe)
async function checkPasswordBreach(password: string): Promise<boolean> {
const encoder = new TextEncoder()
const data = encoder.encode(password)
const hashBuffer = await crypto.subtle.digest('SHA-1', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase()
const prefix = hashHex.slice(0, 5)
const suffix = hashHex.slice(5)
// Only send first 5 chars (k-anonymity)
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`)
const text = await response.text()
// Check if suffix is in response
return text.includes(suffix)
}Exceptions
- A weaker form control is only acceptable when the business requirement and compensating controls are documented explicitly.
- If the flow is already transport-insecure, inaccessible, or externally embedded in a way that changes the threat model, fix that stronger issue first.
- False positives are common on demo, sandbox, or intentionally constrained flows, but they should still be bounded and clearly labeled.
Verification
Automated Checks
- Test show/hide toggle with keyboard (Enter and Space)
- Test form submission with validation errors
- Test in 1Password, LastPass, Bitwarden
Manual Checks
- Verify autocomplete attributes work with password managers
- Check screen reader announces toggle state
- Verify strength indicator updates in real-time
- Check focus management on error state
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Verify password fields use type='password', have proper autocomplete values, and include accessible show/hide toggles.
Fix
Auto-fix issues
Implement password fields with autocomplete='new-password' or 'current-password', accessible toggle buttons, and optional strength meters.
Explain
Learn more
Explain security and UX best practices for password fields including autocomplete attributes and accessible reveal functionality.
Review
Code review
Review server config, headers, forms, and integration points related to Secure password input fields. Flag exact responses, cookies, or browser behaviors that violate the rule, and verify them against the effective production-like response.