Login Series Adding a Password Strength Meter in React | Part 2c

This post expands upon the password input components post Password Input Components where we separate password fields from the base forms to scale our feature. Extract the github Login Feature Part 2b to pick up where we left off in our series.

Once password inputs were properly abstracted, adding a password strength indicator became obvious and straightforward.

The key decision was where this logic should live.

  • ❌ Not in the login form
  • ❌ Not in a shared password utility that runs everywhere
  • ✅ Inside the new password creation flow

This post documents how I added a password strength feature without breaking component boundaries.


Strength ≠ Validation

Before writing code, it helped to separate concerns:

Concept Purpose
Validation Is the password acceptable?
Strength How good is the password?
Matching Do both inputs agree?

Strength is feedback, not enforcement.


The Rules (Simple, Explainable)

I intentionally avoided overcomplicated scoring.

Strength is based on:

  • Length
  • Uppercase letters
  • Numbers
  • Symbols

These same signals are already useful for validation.


Step 1: Strength Utility (Pure Logic)

utils/passwordStrength.js

export function getPasswordStrength(password) {
  let score = 0;

  if (password.length >= 8) score++;
  if (/[A-Z]/.test(password)) score++;
  if (/\d/.test(password)) score++;
  if (/[^A-Za-z0-9]/.test(password)) score++;

  return {
    score,
    label: ["Weak", "Fair", "Good", "Strong", "Very Strong"][score],
  };
}

This function:

  • Has no UI knowledge
  • Is easy to test
  • Can evolve independently

Step 2: Strength Meter UI

components/forms/PasswordStrengthMeter.jsx

export default function PasswordStrengthMeter({ strength }) {
  const colors = ["bg-red-400", "bg-orange-400", "bg-yellow-400", "bg-green-400", "bg-emerald-500"];

  return (
    <div className="mt-2">
      <div className="flex gap-1">
        {[0, 1, 2, 3].map((i) => (
          <div
            key={i}
            className={`h-1 flex-1 rounded
              ${i < strength.score ? colors[strength.score] : "bg-gray-200"}
            `}
          />
        ))}
      </div>

      <p className="mt-1 text-xs text-gray-400">
        Strength: <span className="font-medium">{strength.label}</span>
      </p>
    </div>
  );
}

This component is:

  • Stateless
  • Reusable
  • Visual-only

Step 3: Integrate Into ConfirmPasswordFields

Strength applies only to new password creation.

import { getPasswordStrength } from "../utils/passwordStrength";
import PasswordStrength from "./PasswordStrength";

Inside the component:

const strength = getPasswordStrength(password);

Render it under the new password field:

<PasswordStrength strength={strength} />

Open the ConfirmPasswordFields.jsx file to include our new component. The final file will look like this:

//ConfirmPasswordFields.jsx
import { useState } from "react";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline";

import PasswordRules from "./PasswordRules";
import { getPasswordRules } from "../utils/passwordRules";

import PasswordStrengthMeter from "./PasswordStrengthMeter";
import { getPasswordStrength } from "../utils/passwordStrength";

export default function ConfirmPasswordFields({ password, setPassword, confirm, setConfirm, showConfirm = true }) {
  const [showPassword, setShowPassword] = useState(false);
  const [showConfirmPassword, setShowConfirmPassword] = useState(false);
  const [touched, setTouched] = useState(false);

  const rules = getPasswordRules(password, confirm);
  const strength = getPasswordStrength(password);

  const passwordsMatch = password && confirm && password === confirm;

  return (
    <>
      {/* Password */}
      <div>
        <label className="block mb-1 text-sm font-medium text-gray-100">Password</label>

        <div className="relative">
          <input
            type={showPassword ? "text" : "password"}
            value={password}
            autoComplete="on"
            onFocus={() => setTouched(true)}
            onChange={(e) => setPassword(e.target.value)}
            className="block w-full rounded-md bg-white/5 px-3 py-1.5 pr-10
              text-base text-white outline outline-1 -outline-offset-1 outline-white/10
              placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2
              focus:outline-indigo-500 sm:text-sm/6"
          />

          <button
            type="button"
            onClick={() => setShowPassword((v) => !v)}
            className="absolute inset-y-0 right-0 flex items-center pr-3 bg-transparent
              text-gray-400 hover:text-gray-200"
          >
            {showPassword ? <EyeSlashIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
          </button>
        </div>
        <PasswordStrengthMeter strength={strength} />
      </div>
      {/* Confirm Password */}
      {showConfirm && (
        <div className="mt-4">
          <label className="block mb-1 text-sm font-medium text-gray-100">Confirm Password</label>

          <div className="relative">
            <input
              type={showConfirmPassword ? "text" : "password"}
              value={confirm}
              autoComplete="on"
              onChange={(e) => setConfirm(e.target.value)}
              className={`block w-full rounded-md bg-white/5 px-3 py-1.5 pr-10
                text-base text-white outline outline-1 -outline-offset-1 outline-white/10
                placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2
                sm:text-sm/6
                ${confirm && !passwordsMatch ? "outline-red-400 focus:outline-red-400" : "focus:outline-indigo-500"}`}
            />
            <button
              type="button"
              onClick={() => setShowConfirmPassword((v) => !v)}
              className="absolute inset-y-0 right-0 flex items-center pr-3 bg-transparent
                text-gray-400 hover:text-gray-200"
            >
              {showConfirmPassword ? <EyeSlashIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
            </button>
          </div>

          {password && <PasswordRules rules={rules} />}
        </div>
      )}
    </>
  );
}

Important: This is not rendered for login or current-password fields.


Step 4: Signup Form Enforcement (Optional)

Strength does not block input by default.

If you want to enforce minimum strength:

//SigninForm.jsx

const canSubmit = passwordsMatch && strength.score >= 3 && firstName && lastName && email;

const handleSubmit = async (e) => {
  e.preventDefault();
  if (canSubmit) {
    //do something
  }
};

This allows:

  • UX feedback without hard rejection
  • Progressive enhancement later

Why This Architecture Works

  • Strength logic is isolated
  • UI is composable
  • No state duplication
  • Login remains untouched
  • Reset and signup benefit automatically

This feature was added without refactoring existing forms — a sign the abstraction was correct.


UX Philosophy

I intentionally avoided:

  • Aggressive red warnings
  • Blocking keystrokes
  • Overly strict rules

Password strength should guide, not punish.

src/
  ├─ app/
  │   ├─ App.jsx
  │   └─ Router.jsx

  └─ features/
      └─ auth/
        ├─ components/
        |    ├─ elements/
        |    |  ├─ AccountDropdown.jsx
        |    |  └─ AuthBackground.jsx
        |    |  └─ DarkModeButton.jsx
        |    ├─ forms/
        |    |  ├─ ConfirmPasswordFields.jsx  <- Adjust
        |    |  ├─ LoginForm.jsx
        |    |  ├─ PasswordField.jsx
        |    |  ├─ PasswordRules.jsx
        |    |  ├─ PasswordStrengthMeter.jsx  <- new component we will create
        |    |  └─ SignupForm.jsx
        |    ├─ layout/
        |      ├─ AuthLayout.jsx
        |      ├─ AuthFacingHeader.jsx
        |      ├─ HeaderTopLevel.jsx
        |      └─ AuthFacingFooter.jsx
        |    ├─ utils/
        |      ├─ passwordRules.jsx
        |      └─ passwordStrength.jsx <- new component we will create
        ├─ pages/
        │  ├─ ConfirmEmailPage.jsx
        │  ├─ ConfirmResetPage.jsx
        │  ├─ LoginPage.jsx
        │  ├─ NotFoundPage.jsx
        │  ├─ ProfilePage.jsx
        │  ├─ RequestResetPage.jsx
        │  └─ SignupPage.jsx
  └─ main.jsx

Final Thoughts

Adding password strength late in development usually causes pain. In this case, it didn’t — because the password inputs were already designed around clear responsibilities. That’s the payoff of good component boundaries. This post is pushed to our github Part 2c repo branch Post Password Strength UI.

The next post in this series UI Tailwind part 2d will cover the remaining pages, forms and links in greater detail and will finalize the UI components of the login feature. These pages and associated forms are covering this login application flow in production systems. We approached building components in the same way as the login/signup.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • React Login Series - Reusing the Feature in Another App | Part 7
  • React Login Series - Login Feature Complete Test Suite | Part 6b
  • What Git Never Explains -- Stacked Branches & Merge Order
  • Why Branch Discipline in Git Isn’t Optional (And How It Saved This Auth Refactor)
  • React Login Series - Testing the Login Feature (RTL + Jest) | Part 6