The pattern checks four rules and a length in one pass, using lookaheads. Each (?=…) stands at the start of the string and asks whether something matches ahead, without moving forward, so all four checks look at the same whole password:
(?=.*[a-z]) somewhere, a lowercase letter
(?=.*[A-Z]) somewhere, an uppercase letter
(?=.*\d) somewhere, a digit
(?=.*[^A-Za-z\d]) somewhere, a character that is neither: a symbol or a space
.{12,}$ and 12 or more characters in all
What it accepts
The tester below checks one password per line with g and m; for a single value, drop both flags.
Correct-Horse-9 matches
correcthorsebattery no: no uppercase, digit or symbol
Sh0rt!pw no: 8 characters
NoDigitsHere!! no: no digit
ALLCAPS-12345! no: no lowercase
lower-case-1234 no: no uppercase
Tr0ub4dor&3xyz matches
Spaces count 1A matches: a space counts as a symbol
Note what “symbol” means here: anything that isn’t an ASCII letter or digit, so a space or an é counts. . stops at line breaks, which suits a password field. .{12,} counts UTF-16 code units, so an emoji counts as two; add the u flag to count it as one.
Better messages with separate checks
One regex can only say yes or no. Checking each rule on its own tells people what’s missing:
const RULES = [
[/[a-z]/, 'a lowercase letter'],
[/[A-Z]/, 'an uppercase letter'],
[/\d/, 'a digit'],
[/[^A-Za-z\d]/, 'a symbol'],
];
function missing(password) {
const needs = RULES.filter(([rule]) => !rule.test(password)).map(([, need]) => need);
if (password.length < 12) needs.unshift('at least 12 characters');
return needs; // [] when the password passes
}
Should you have these rules at all?
NIST’s guidelines for passwords, SP 800-63B, advise against composition rules like these. They push people toward predictable patterns such as Password1!, and they don’t stop the passwords attackers try first. What helps is length, and a check against lists of passwords already leaked in breaches. Tr0ub4dor&3xyz passes this regex; a long phrase of ordinary words such as correct horse battery staple fails it and is far harder to guess.
If a policy requires the rules, the regex above implements them. If you set the policy, require a decent length, allow every character including spaces, and reject known breached passwords.