DEV Community

Cover image for Building a Password Checker with Vanilla JS (No Frameworks!)
chittaranjan nivargi
chittaranjan nivargi

Posted on

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

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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('');
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Deployment

Local Development:

# No build step!
python -m http.server 8000
# Visit localhost:8000
Enter fullscreen mode Exit fullscreen mode

Production:

git push origin main
# Vercel auto-deploys
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode
  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);
});
Enter fullscreen mode Exit fullscreen mode
  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);
}
Enter fullscreen mode Exit fullscreen mode

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!


Top comments (0)