Building a Password Checker with Vanilla JS (No Frameworks!)



This content originally appeared on DEV Community and was authored by chittaranjan nivargi

I got locked out of the GST portal for the third time because my password “didn’t meet requirements.”

No explanation. Just “Invalid password.”
After wasting 30 minutes trying different passwords, I decided to build a tool that would tell me exactly what was wrong with my password for different government portals.
Result: PasswordChecker.in – Built in 2 weekends with vanilla JavaScript. No React. No Vue. No frameworks.
Here’s how I did it.

Why No Frameworks?

Speed. I wanted the tool to be:

  • Fast to load (< 50KB total)
  • Fast to build (2 weekends)
  • Fast to run (real-time password checking)

Adding React would add 130KB+ before I wrote a single line of code. For a single-page tool, that’s overkill.

The Tech Stack

  • Frontend: Vanilla JavaScript + Tailwind CSS
  • Styling: Tailwind CDN (for rapid prototyping)
  • API: Have I Been Pwned (k-Anonymity model)
  • Hosting: Vercel
  • Source Control: GitHub

Total bundle size: ~48KB (including Tailwind)
Feature #1: Real-Time Password Strength Checker
The Requirements
Different Indian government portals have different password requirements:

const portalRequirements = {
  uidai: {
    minLength: 8,
    requiresUppercase: true,
    requiresLowercase: true,
    requiresNumber: true,
    requiresSpecial: true,
  },
  gstn: {
    minLength: 10,
    requiresUppercase: true,
    requiresLowercase: true,
    requiresNumber: true,
    requiresSpecial: true,
    maxLength: 15,
  },
  incomeTax: {
    minLength: 12,
    requiresUppercase: true,
    requiresLowercase: true,
    requiresNumber: true,
    requiresSpecial: true,
    maxLength: 14,
  },
};

The Core Logic

function checkPasswordStrength(password) {
  const checks = {
    length: password.length >= 8,
    uppercase: /[A-Z]/.test(password),
    lowercase: /[a-z]/.test(password),
    number: /[0-9]/.test(password),
    special: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password),
  };

  // Calculate score (0-100)
  let score = 0;
  if (checks.length) score += 20;
  if (checks.uppercase) score += 20;
  if (checks.lowercase) score += 20;
  if (checks.number) score += 20;
  if (checks.special) score += 20;

  // Additional points for length
  if (password.length >= 12) score += 10;
  if (password.length >= 16) score += 10;

  return {
    score: Math.min(score, 100),
    checks,
    strength: getStrengthLabel(score),
  };
}

function getStrengthLabel(score) {
  if (score >= 80) return 'Very Strong';
  if (score >= 60) return 'Strong';
  if (score >= 40) return 'Moderate';
  if (score >= 20) return 'Weak';
  return 'Very Weak';
}

Real-Time Updates

const passwordInput = document.getElementById('password');
const strengthMeter = document.getElementById('strength-meter');

passwordInput.addEventListener('input', (e) => {
  const password = e.target.value;
  const result = checkPasswordStrength(password);

  // Update UI
  updateStrengthMeter(result);
  updateChecklist(result.checks);
  checkPortalCompliance(password);
});

Feature #2: Password Generator

Users needed passwords that were:

Strong (meets requirements)
Memorable (not random gibberish)
Portal-specific (works for UIDAI, GST, etc.)

The Memorable Password Algorithm
Instead of generating xK9!mP2@, generate Quick7Tiger$42

function generateReadablePassword(requirements) {
  const adjectives = ['Quick', 'Brave', 'Smart', 'Cool', 'Swift'];
  const nouns = ['Tiger', 'Eagle', 'Wolf', 'Lion', 'Hawk'];
  const specials = ['@', '#', '$', '%', '&', '*'];

  const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
  const number1 = Math.floor(Math.random() * 9) + 1;
  const noun = nouns[Math.floor(Math.random() * nouns.length)];
  const special = specials[Math.floor(Math.random() * specials.length)];
  const number2 = Math.floor(Math.random() * 90) + 10;

  let password = `${adjective}${number1}${noun}${special}${number2}`;

  // Adjust for portal requirements
  if (requirements.minLength > password.length) {
    password += Math.random().toString(36).substring(2, requirements.minLength - password.length + 2);
  }

  return password;
}

Output: Passwords like Smart5Wolf@73 – easy to remember, meets all requirements.

Feature #3: Data Breach Checker

This was the most complex feature. I needed to check if a password appeared in data breaches without sending the password to any server.
The k-Anonymity Model
Have I Been Pwned (HIBP) uses k-Anonymity:

  • Hash the password (SHA-1)
  • Send only the first 5 characters of the hash
  • Server returns all breached passwords matching those 5 chars
  • Client checks if full hash is in the list
async function checkPasswordBreach(password) {
  // Hash password
  const hash = await sha1(password);
  const prefix = hash.substring(0, 5);
  const suffix = hash.substring(5).toUpperCase();

  // Query HIBP API (only send first 5 chars)
  const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
  const data = await response.text();

  // Check if full hash is in response
  const hashes = data.split('\n');
  for (let line of hashes) {
    const [hashSuffix, count] = line.split(':');
    if (hashSuffix === suffix) {
      return {
        found: true,
        count: parseInt(count),
      };
    }
  }

  return { found: false };
}

// SHA-1 implementation
async function sha1(str) {
  const buffer = new TextEncoder().encode(str);
  const hash = await crypto.subtle.digest('SHA-1', buffer);
  const hashArray = Array.from(new Uint8Array(hash));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

Privacy: Your password never leaves your browser. Only the first 5 characters of its hash are sent.

Feature #4: Multi-Portal Compatibility Test

Test one password against 10 portals simultaneously.

function testPortalCompatibility(password) {
  const portals = [
    { name: 'UIDAI', requirements: portalRequirements.uidai },
    { name: 'GSTN', requirements: portalRequirements.gstn },
    { name: 'Income Tax', requirements: portalRequirements.incomeTax },
    // ... more portals
  ];

  return portals.map(portal => ({
    name: portal.name,
    compatible: checkRequirements(password, portal.requirements),
    issues: getIssues(password, portal.requirements),
  }));
}

function checkRequirements(password, requirements) {
  if (password.length < requirements.minLength) return false;
  if (requirements.maxLength && password.length > requirements.maxLength) return false;
  if (requirements.requiresUppercase && !/[A-Z]/.test(password)) return false;
  if (requirements.requiresLowercase && !/[a-z]/.test(password)) return false;
  if (requirements.requiresNumber && !/[0-9]/.test(password)) return false;
  if (requirements.requiresSpecial && !/[!@#$%^&*]/.test(password)) return false;

  return true;
}

Deployment

Local Development:

# No build step!
python -m http.server 8000
# Visit localhost:8000

Production:

git push origin main
# Vercel auto-deploys

No webpack. No babel. No build process.

Performance Optimizations

  1. Debounced Input Don’t check password on every keystroke:
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

const debouncedCheck = debounce((password) => {
  checkPasswordStrength(password);
}, 300);

passwordInput.addEventListener('input', (e) => {
  debouncedCheck(e.target.value);
});
  1. Lazy Load Breach Check Only check breaches when user clicks “Check for Breaches”:
// Don't auto-check (expensive API call)
breachCheckButton.addEventListener('click', async () => {
  showLoader();
  const result = await checkPasswordBreach(password);
  hideLoader();
  displayResult(result);
});
  1. Cache API Responses
const breachCache = new Map();

async function checkPasswordBreach(password) {
  const hash = await sha1(password);
  const prefix = hash.substring(0, 5);

  // Check cache first
  if (breachCache.has(prefix)) {
    return checkHash(breachCache.get(prefix), hash);
  }

  // Fetch and cache
  const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
  const data = await response.text();
  breachCache.set(prefix, data);

  return checkHash(data, hash);
}

Lessons Learned

1. Vanilla JS is Fast Enough

For most tools, you don’t need a framework. Vanilla JS is:

  • Faster to load
  • Easier to debug
  • Fewer dependencies
  • More portable

2. Real-Time Feedback is Critical

Users type fast. Your UI must keep up. Debouncing is your friend.

3. Privacy Matters

For a password tool, client-side only was non-negotiable. No backend. No database. No tracking.

Users can inspect the source and verify nothing is sent to any server (except the HIBP API with anonymized data).

4. Government Standards are Inconsistent

Every portal has different requirements. No standardization. This makes users’ lives harder—and creates opportunities for tools like mine.

What’s Next

Planned Features:

  • Browser extension (auto-fill government portals)
  • Bulk password checking (CSV upload)
  • API for developers
  • Dark mode

Tech Debt:

  • Add proper testing (currently manually tested)
  • Consider Web Components for reusability
  • Add service worker for offline support

Try It Yourself

Live: passwordchecker.in

Source: (Will open-source if there’s interest—comment below!)

Conclusion

You don’t need React to build useful tools. Sometimes vanilla JavaScript is the right choice.

Key Takeaways:

  • Keep it simple
  • Client-side for privacy
  • Real-time feedback
  • Focus on UX

Built in 2 weekends. 500+ users in the first week. Zero frameworks.

Questions? Ask in the comments!


This content originally appeared on DEV Community and was authored by chittaranjan nivargi