tutorial // Oct 14, 2022

How to Build a Password Strength Input with HTML, CSS, and JavaScript

How to write a simple algorithm for calculating password strength and displaying that strength as a progress bar in your UI.

How to Build a Password Strength Input with HTML, CSS, and JavaScript

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Setting up a base component

To get started, in the project we just created, we want to open up the file at /ui/pages/index/index.js and replace its contents with the following:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

Next, we want to modify this to include the HTML we'll need to render our password input and strength indicator.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    strength: 'bad',
  },
  render: ({ state }) => {
    return `
      <div class="password-strength">
        <label for="password">Password</label>
        <input type="password" name="password" placeholder="Password" autocomplete="off" />
        <div class="strength-indicator">
          <div class="strength-bar">
            <div class="strength ${state.strength}"></div>
          </div>
          <p>${state.strength}</p>
        </div>
      </div>
    `;
  },
});

export default Index;

Two things happening here. First, down in the render() function for our Index component, we're rendering the HTML for our password input and strength indicator (a simple progress bar with a label to the right describing the current strength). Second, we're making use of our component's state feature, setting a default value for state.strength to the string 'bad'.

As we'll see, we'll write the logic to update state.strength on the fly as the user's password changes and passes a set of tests we'll define later. As a bit of foreshadowing, we'll have five different levels of strength:

  1. Bad
  2. Okay
  3. Good
  4. Better
  5. Best

Each level will correspond to some test being passed by the user's current input.

Adding CSS

The majority of the CSS for this component is inconsequential. The styling we care about most here is for the strength indicator. This will need to dynamically update based on the current strength level for the user's password. To do it, we're going to utilize the level string (e.g., bad or best) as a CSS class to assign a color and fill percentage for the strength indicator.

Before we do that, let's get some basic styles in place for all of the pieces and then take a look at how we'll handle the different strength levels.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    strength: 'bad',
  },
  css: `
    .password-strength {
      padding: 40px;
    }

    label {
      display: block;
      font-size: 15px;
      margin-bottom: 10px;
    }

    input[type="password"] {
      width: 300px;
      background: #fff;
      border: 1px solid #eee;
      border-radius: 3px;
      box-shadow: inset 0px 0px 2px 2px rgba(0, 0, 0, 0.03);
      padding: 15px;
    }

    .strength-indicator {
      width: 300px;
      margin-top: 20px;
      display: flex;
      align-items: center;
    }

    .strength-indicator .strength-bar {
      height: 15px;
      background: #eee;
      border-radius: 30px;
      width: 100%;
    }

    .strength-indicator .strength-bar .strength {
      height: 15px;
      border-radius: 30px;
      transition: width 0.3s ease-in-out;
    }

    .strength-indicator p {
      margin: 0 0 0 10px;
      font-weight: bold;
      text-transform: uppercase;
      font-size: 12px;
    }
  `,
  render: ({ state }) => {
    return `
      <div class="password-strength">
        <label for="password">Password</label>
        <input type="password" name="password" placeholder="Password" autocomplete="off" />
        <div class="strength-indicator">
          <div class="strength-bar">
            <div class="strength ${state.strength}"></div>
          </div>
          <p>${state.strength}</p>
        </div>
      </div>
    `;
  },
});

export default Index;

Above, we've added in some styling for our password input, the label above it, and the strength indicator bar. Again, we want to think of the strength indicator like a progress bar. So, we start by giving the .strength-bar a background of #eee (light grey) and a height of 15px. We've also added a width so the "empty" bar expands the width of its parent (in this case, the .strength-indicator div which is set to display: flex; and a width of 300px).

In addition, we've added a style for the actual fill for the strength bar (the .strength div), also giving it a height of 15px and a border-radius of 30px. In addition, we've also added a special rule transition: width 0.3s ease-in-out. This is saying that we want to animate changes to this element's width, using an animation timing function of ease-in-out that runs for 300ms. This will give our fill a "smooth" effect as it increases or decreases (versus the blocky default which, while technically correct, isn't as attractive from a UI perspective).

Finally, we add in a style for the <p></p> tag that's added immediately after the .strength-bar which will display the current strength level as plain text.

Important! Adding the plain text here isn't just a design detail, it's also an accessibility detail. Because we're going to use colors to indicate strength, adding the label here ensures that a user who's color blind can also intuit the strength of their password.

Now for the important stuff. Let's add in the styling for the different strength levels:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    strength: 'bad',
  },
  css: `
    .password-strength {
      padding: 40px;
    }

    label {
      display: block;
      font-size: 15px;
      margin-bottom: 10px;
    }

    input[type="password"] {
      width: 300px;
      background: #fff;
      border: 1px solid #eee;
      border-radius: 3px;
      box-shadow: inset 0px 0px 2px 2px rgba(0, 0, 0, 0.03);
      padding: 15px;
    }

    .strength-indicator {
      width: 300px;
      margin-top: 20px;
      display: flex;
      align-items: center;
    }

    .strength-indicator .strength-bar {
      height: 15px;
      background: #eee;
      border-radius: 30px;
      width: 100%;
    }

    .strength-indicator .strength-bar .strength {
      height: 15px;
      border-radius: 30px;
      transition: width 0.3s ease-in-out;
    }

    .strength-indicator p {
      margin: 0 0 0 10px;
      font-weight: bold;
      text-transform: uppercase;
      font-size: 12px;
    }

    .strength-indicator .strength-bar .strength.bad {
      width: 10%;
      background: #DD3B2D;
    }

    .strength-indicator .strength-bar .strength.okay {
      width: 25%;
      background: #F59E1B;
    }

    .strength-indicator .strength-bar .strength.good {
      width: 50%;
      background: #FFCC00;
    }

    .strength-indicator .strength-bar .strength.better {
      width: 75%;
      background: #4285F4;
    }

    .strength-indicator .strength-bar .strength.best {
      width: 100%;
      background: #00D490;
    }
  `,
  render: ({ state }) => {
    return `
      <div class="password-strength">
        <label for="password">Password</label>
        <input type="password" name="password" placeholder="Password" autocomplete="off" />
        <div class="strength-indicator">
          <div class="strength-bar">
            <div class="strength ${state.strength}"></div>
          </div>
          <p>${state.strength}</p>
        </div>
      </div>
    `;
  },
});

export default Index;

While it may look like we've added a lot, all of these styles are identical in terms of intent; the only difference is the values we're setting on each property.

Like we hinted at above, we'll have five different strength levels for our indicator, starting with bad and going up to best. Here, we've written five additional styles, all of which are for the same element .strength-indicator .strength-bar .strength, but, only when that element has an additional class name matching the current strength level (e.g., .strength-indicator .strength-bar .strength.good).

If we look down in our HTML, we can see that the strength level stored in state.strength is added as an additional class on the <div class="strength"></div> element. The idea here is that as our user's input passes the tests for strength, we'll update state.strength with one of our five levels. Down in the render() function, then, we'll be updating the HTML for our component and trigger re-renders. As we do, we'll see our indicator bar grow or shrink relative to the level name it's being passed.

Next, let's wire up that functionality and get this all working.

Calculating strength and assigning a rating

Now for the important part. On our component, we're going to add a new option methods which will contain three different functions (on a Joystick component, methods are just miscellaneous functions we can call from anywhere in our component):

  1. getStrengthPercentage() which will be responsible for taking in the user's current input and evaluating it against a series of tests in the form of regular expressions.
  2. getLevel() which will determine the level string (e.g., 'bad' or 'best') based on the strength percentage returned by getStrengthPercentage().
  3. getStrength() which will call the two above functions together and return the resulting level string.

After we wire these up, we'll see how to connect them to our password input. First, though, let's start by wiring up the getStrengthPercentage() method:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    strength: 'bad',
  },
  methods: {
    getStrengthPercentage: (password = '') => {
      const tests = [
        password?.length >= 8,
        // Includes at least one lowercase letter.
        new RegExp(/([a-z])/).test(password),
        // Includes at least one uppercase letter.
        new RegExp(/([A-Z])/).test(password),
        // Includes at least one number.
        new RegExp(/([0-9])/).test(password),
        // Includes at least one special character.
        new RegExp(/[-._!"`'#%&,:;<>=@{}~\$\(\)\*\+\/\\\?\[\]\^\|]+/).test(password),
      ];

      return (tests?.filter((result) => !!result)?.length / tests?.length) * 100;
    },
  },
  css: `...`,
  render: ({ state }) => {
    return `
      <div class="password-strength">
        <label for="password">Password</label>
        <input type="password" name="password" placeholder="Password" autocomplete="off" />
        <div class="strength-indicator">
          <div class="strength-bar">
            <div class="strength ${state.strength}"></div>
          </div>
          <p>${state.strength}</p>
        </div>
      </div>
    `;
  },
});

export default Index;

Above, we've assigned an object to the property methods on our component's options, and on that object, we've added a function getStrengthPercentage() (we use the name "method" for this as the technical term for a function defined on an object in JavaScript is a method).

To that function, we expect the user's password to be passed in as a string. In order to evaluate that password, we've created an array tests which includes a handful of statements which we expect to return a boolean true or false.

Here's what we're up to:

  1. Starting with the simplest, we've added a check to make sure at the very least, the user's password length is greater than or equal to eight characters in length.
  2. We check to see if the password includes at least 1 lowercase letter.
  3. We check to see if the password includes at least 1 uppercase letter.
  4. We check to see if the password includes at least 1 special character (e.g., @!><).

The "strength" of the user's password then is relative to the number of these tests that return true instead of false. To calculate this, at the bottom of our getStrengthPercentage(), we're returning a statement which first filters are tests array down to only the true items and gets its length and then divides that length by the length of the entire tests array. Finally, we multiply the result of this division by 100 to get our percentage as an integer (i.e., converting 0.50 to 50).

That takes care of the hard part. Now, with our strength percentage available, let's wire up the getLevel() method to get our level string.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    strength: 'bad',
  },
  methods: {
    getStrengthPercentage: (password = '') => { ... },
    getLevel: (percentage = 0) => {
      if (percentage >= 25 && percentage < 50) {
        return 'okay';
      }

      if (percentage >= 50 && percentage < 75) {
        return 'good';
      }

      if (percentage >= 75 && percentage < 90) {
        return 'better';
      }

      if (percentage >= 90) {
        return 'best';
      }

      return 'bad';
    },
  },
  css: `...`,
  render: ({ state }) => {
    return `
      <div class="password-strength">
        <label for="password">Password</label>
        <input type="password" name="password" placeholder="Password" autocomplete="off" />
        <div class="strength-indicator">
          <div class="strength-bar">
            <div class="strength ${state.strength}"></div>
          </div>
          <p>${state.strength}</p>
        </div>
      </div>
    `;
  },
});

export default Index;

This method is pretty straightforward. Here, we're just writing a series of if statements, taking in the percentage we got from getStrengthPercentage() and evaluating it based on a set or ranges corresponding to the levels we want to return. By default, if none of our statements match, we assume that the level is still the default 'bad'.

Just one more method to go: getStrength() which will call the above two methods for us.

Terminal

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    strength: 'bad',
  },
  methods: {
    getStrengthPercentage: (password = '') => { ... },
    getLevel: (percentage = 0) => { ... },
    getStrength: (password = '', component) => {
      const strengthPercentage = component.methods.getStrengthPercentage(password);
      return component.methods.getLevel(strengthPercentage);
    },
  },
  css: `...`,
  render: ({ state }) => {
    return `
      <div class="password-strength">
        <label for="password">Password</label>
        <input type="password" name="password" placeholder="Password" autocomplete="off" />
        <div class="strength-indicator">
          <div class="strength-bar">
            <div class="strength ${state.strength}"></div>
          </div>
          <p>${state.strength}</p>
        </div>
      </div>
    `;
  },
});

export default Index;

Pretty straightforward. Here, we anticipate the password the user has typed in being passed as the first argument and as the second, we anticipate the component instance automatically passed as the last argument to our method by Joystick. From that component instance, we call our getStrengthPercentage() and getLevel() methods via the component.methods object.

There's just one piece left...how does this get called?

Updating assessment on user input

To make all of this work, now, we're going to add an event listener to our [name="password"] input, calling to component.methods.getStrength() whenever our user types into the input.

Terminal

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    strength: 'bad',
  },
  methods: {
    getStrengthPercentage: (password = '') => { ... },
    getLevel: (percentage = 0) => { ... },
    getStrength: (password = '', component) => {
      const strengthPercentage = component.methods.getStrengthPercentage(password);
      return component.methods.getLevel(strengthPercentage);
    },
  },
  css: `...`,
  events: {
    'keyup [name="password"]': (event, component) => {
      component.setState({ strength: component.methods.getStrength(event?.target?.value) });
    },
  },
  render: ({ state }) => {
    return `
      <div class="password-strength">
        <label for="password">Password</label>
        <input type="password" name="password" placeholder="Password" autocomplete="off" />
        <div class="strength-indicator">
          <div class="strength-bar">
            <div class="strength ${state.strength}"></div>
          </div>
          <p>${state.strength}</p>
        </div>
      </div>
    `;
  },
});

export default Index;

This is fairly straightforward. Here, we're adding one last option to our component, events which is assigned an object which defines event listeners for the component. For our needs, we want to listen for a keyup event (meaning a key on the keyboard has popped up after being pressed down) on our password input. When this occurs, we know that the user has updated their password.

Remember, our goal is to ultimately update the state.strength value on our component so our UI updates in response. So, here, whenever the input changes, we call to update state via component.setState(), changing the strength value to the response of calling component.methods.getStrength(), passing in the current input for the user's password (event.target.value).

That's it! Now, if we load up our app in the browser, we should see our strength indicator working as intended.

Wrapping up

In this tutorial, we learned how to build a password input with a strength indicator. We learned how to set up CSS styles for our indicator, making it change conditionally as password strength changed, as well as how to calculate the strength as a percentage and then translate that percentage into a human-friendly string. Finally, we learned how to use event listeners in a Joystick component to determine password strength as our user was typing.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode