Tutorials / / 6 min read
How to create a password strength meter with Tailwind CSS: JavaScript and Alpine.js
Build a password strength meter with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. Live strength checks, a colored bar, and a visibility toggle.
Hello everyone! Today we are building a password strength meter that scores a password as the user types. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is a password strength meter?
A password strength meter gives users live feedback while they type, nudging them toward strong passwords. Ours checks length, uppercase and lowercase letters, numbers, and special characters, then reflects the result in a colored bar and a matching label.
Use cases
- Account creation: nudge new users toward strong passwords right at sign-up.
- Password updates: guide people to something stronger when they change or reset a password.
- Corporate policies: enforce complexity requirements on internal tools and dashboards.
- Sensitive platforms: banking, health, and e-commerce accounts deserve extra protection.
The markup
id="password"withtype="password": the input we measure.id="toggle-password"withtype="button": the button that toggles the visibility of the password.id="show-eye"andid="hide-eye": the two icons inside the button; the second one has a class oftext-blue-500and is initially hidden.id="strength-bar": the progress bar, we change its width and color based on the strength of the password.id="strength-text"andid="strength-label": the text that names the current strength, colored to match.
Note: Irrelevant classes and attributes are removed for brevity and you can get the full code on the GitHub repository.
<div>
<!-- Password Input -->
<div>
<label for="password" class="sr-only">Password</label>
<div class="relative">
<input id="password" type="password" />
<button id="toggle-password" type="button">
<span id="show-eye">
<!-- SVG goes here -->
</span>
<span id="hide-eye" class="text-blue-500" style="display: none;">
<!-- SVG goes here -->
</span>
</button>
</div>
</div>
<!-- Password Strength Meter -->
<div>
<div id="strength-bar" style="width: 0;"></div>
</div>
<!-- Password Strength Text -->
<p id="strength-text">
Password strength: <span id="strength-label">weak</span>
</p>
</div>The script
document.addEventListener("DOMContentLoaded", () => {: waits for the DOM, then grabs the input, the toggle button, the two icons, the strength bar, and the strength texts.let showPassword = false;stores the toggle state.function checkStrength(password) {: starts atweak,25%, and red, then tests the password with four regular expressions:hasLowerCase,hasUpperCase,hasNumber, andhasSpecialChar.const passedChecks = [...].filter(Boolean).length;: counts how many of those checks passed.- For passwords of 8 or more characters: 4 passed checks (or 12 or more characters) means
very strong, 3 meansstrong, and 2 meansmedium. Each level gets its own width and color for the bar and the label. passwordInput.addEventListener("input", (event) => {: re-checks the strength on every keystroke.togglePasswordButton.addEventListener("click", () => {: flipsshowPassword, switches the input betweentextandpassword, and swaps the eye icons.
document.addEventListener("DOMContentLoaded", () => {
const passwordInput = document.getElementById("password");
const togglePasswordButton = document.getElementById("toggle-password");
const showEye = document.getElementById("show-eye");
const hideEye = document.getElementById("hide-eye");
const strengthBar = document.getElementById("strength-bar");
const strengthLabel = document.getElementById("strength-label");
const strengthText = document.getElementById("strength-text");
let showPassword = false;
function checkStrength(password) {
let strength = "weak";
let width = "25%";
let color = "red";
const hasLowerCase = /[a-z]/.test(password);
const hasUpperCase = /[A-Z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?':{}|<>]/.test(password);
const passedChecks = [
hasLowerCase,
hasUpperCase,
hasNumber,
hasSpecialChar,
].filter(Boolean).length;
if (password.length >= 8) {
if (passedChecks === 4 || password.length >= 12) {
strength = "very strong";
width = "100%";
color = "#3e88f7";
} else if (passedChecks >= 3) {
strength = "strong";
width = "75%";
color = "#4caf50";
} else if (passedChecks >= 2) {
strength = "medium";
width = "50%";
color = "orange";
}
}
strengthBar.style.width = width;
strengthBar.style.backgroundColor = color;
strengthLabel.textContent = strength;
strengthText.style.color = color;
}
passwordInput.addEventListener("input", (event) => {
checkStrength(event.target.value);
});
togglePasswordButton.addEventListener("click", () => {
showPassword = !showPassword;
passwordInput.type = showPassword ? "text" : "password";
showEye.style.display = showPassword ? "none" : "inline";
hideEye.style.display = showPassword ? "inline" : "none";
});
});The Alpine.js version
Same component, no separate script: the state lives in x-data.
x-data: holdspassword,strength,showPassword, and thecheckStrength()method, which runs the same checks as the vanilla version but stores the result inthis.strength.x-model="password"and@input="checkStrength()": bind the input and re-check the strength on every keystroke.:type="showPassword ? 'text' : 'password'"and@click="showPassword = !showPassword": handle the visibility toggle, withx-showswitching between the two icons.- The meter is a
divwhose:classmaps each strength to a width and color, fromw-1/4 bg-red-500for weak up tow-full bg-blue-500for very strong. - The text uses
x-text="strength"plus a matching:class, so the label and its color stay in sync with the bar.
Note: Irrelevant classes are removed for brevity and you can find them on the GitHub repository.
<div
x-data="{
password: '',
strength: '',
showPassword: false,
checkStrength() {
const password = this.password;
// Reset the strength if password is empty
if (password.length === 0) {
this.strength = '';
return;
}
// Initialize the strength to weak by default
this.strength = 'weak';
// Define conditions for different levels of password strength
const hasLowerCase = /[a-z]/.test(password);
const hasUpperCase = /[A-Z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?':{}|<>]/.test(password);
// Count the number of passed checks
const passedChecks = [hasLowerCase, hasUpperCase, hasNumber, hasSpecialChar].filter(Boolean).length;
// Update strength based on conditions
if (password.length >= 8) {
if (passedChecks === 4 || password.length >= 12) {
this.strength = 'very strong';
} else if (passedChecks >= 3) {
this.strength = 'strong';
} else if (passedChecks >= 2) {
this.strength = 'medium';
}
}
}
}"
class="w-full max-w-lg pt-6 mx-auto mt-12 border-t space-y-4"
>
<!-- Password Input -->
<div>
<label for="password" class="sr-only">Password</label>
<div class="relative">
<input
id="password"
:type="showPassword ? 'text' : 'password'"
x-model="password"
@input="checkStrength()"
/>
<button
@click="showPassword = !showPassword"
type="button"
class="absolute inset-y-0 right-0 flex items-center pr-3"
>
<span x-show="!showPassword">
<!-- SVG goes here -->
</span>
<span x-show="showPassword">
<!-- SVG goes here -->
</span>
</button>
</div>
</div>
<!-- Password Strength Meter -->
<div>
<div
class="h-full transition-all duration-300 ease-out"
:class="{
'w-1/4 bg-red-500': strength === 'weak',
'w-1/2 bg-yellow-500': strength === 'medium',
'w-3/4 bg-green-500': strength === 'strong',
'w-full bg-blue-500': strength === 'very strong'
}"
></div>
</div>
<!-- Password Strength Text -->
<p
:class="{
'text-red-500': strength === 'weak',
'text-yellow-500': strength === 'medium',
'text-green-500': strength === 'strong',
'text-blue-500': strength === 'very strong'
}"
>
Password strength: <span x-text="strength"></span>
</p>
</div>You can try the Alpine version separately: live demo and source code.
Which one should you use?
Alpine packs the whole meter, checks included, into the markup, which is great when it’s already part of your stack. The vanilla version keeps the logic in a plain script and works with zero dependencies.
Conclusion
Same checks, same meter, two flavors: a few regular expressions score the password and the UI reflects it with width and color.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza