Tutorial

How To Build a Password Strength Meter in React

Updated on April 9, 2020
Default avatar

By Glad Chinda

How To Build a Password Strength Meter in React

Introduction

Passwords are commonly used for user authentication in most web applications. Because of this, it is important that passwords are stored in a safe way. Over the years, techniques such as one-way password hashing have been employed to hide the real representation of passwords being stored in a database.

Although password hashing is a great step toward securing password, the user still poses a major challenge to password security: a user who uses a common word as a password makes the effort of hashing fruitless, since a brute-force attack can quickly crack such passwords.

To address this, many web applications today insist on users having strong passwords, either by ensuring a minimum password length or some combination of alphanumeric characters and symbols in the password. To measure password strength, Dropbox developed an algorithm for a realistic password strength estimator inspired by password crackers. This algorithm is packaged in a JavaScript library called zxcvbn. In addition, the package contains a dictionary of commonly used English words, names, and passwords.

In this tutorial, we will create a form with fields for full name, email, and password using the React JavaScript framework. We will perform some lightweight form validation and also use the zxcvbn library to estimate the strength of the password in the form while providing visual feedback.

Check out this CodeSandbox demo of what you will create by the end of this tutorial.

Prerequisites

Before you begin, ensure that you have a recent version of Node installed on your system.

To follow this tutorial, you will need the following:

  • A recent version of Node installed on your machine. For more information on how to install this, select your distribution from the How To Install Node.js collection.
  • yarn installed to run all of your NPM scripts and to install dependencies for the project. You can follow this Yarn installation guide to install yarn on your system.

Step 1 — Setting Up the Application

This tutorial will use the create-react-app package to generate your new React application. Run the following command to install create-react-app on your system if you have not installed it already:

  1. npm install -g create-react-app

Once the installation is complete, start a new React application using the following command:

  1. create-react-app react-password-strength

This command names it react-password-strength, but you can name it whatever you’d like.

Note: If you are using npm version 5.2 or higher, it ships with an additional npx binary. Using the npx binary, you don’t need to install create-react-app globally on your system. You can start a new React application with this command: npx create-react-app react-password-strength.

Next, you will install the dependencies needed for the application. Run the following command to install the required dependencies:

  1. yarn add zxcvbn isemail prop-types node-sass bootstrap

This command installs the following dependencies:

  • zxcvbn - Aforementioned password-strength estimation library.
  • isemail - E-mail validation library.
  • prop-types - Runtime checking of intended types of properties passed to components.
  • node-sass - Used to compilie Sass files to CSS.

As you might have noticed, you installed the bootstrap package as a dependency for the application to get some default styling. To include Bootstrap in the application, edit the src/index.js file and add the following line before every other import statement:

src/index.js
import 'bootstrap/dist/css/bootstrap.min.css';

Finally, start your application:

  1. yarn start

The application is now started and development can begin. Notice that a browser tab has been opened for you with the live reloading functionality. This will keep in sync with changes in the application as you develop.

At this point, your application view will look like the following screenshot:

Initial View

Step 2 — Building the Components

This application will use a form for full name, email, and password. It will also perform some lightweight form validation on the fields. In this step, you will create the following React components:

  • FormField - Wraps a form input field with its attributes and change event handler.

  • EmailField - Wraps the email FormField and adds email validation logic to it.

  • PasswordField - Wraps the password FormField and adds the password validation logic to it. Also attaches the password strength meter and some other visual cues to the field.

  • JoinForm - The fictitious Join Support Team form that houses the form fields.

Create a components directory inside the src directory of the application to house all of the components.

The FormField Component

Create a new file FormField.js in the src/components directory and add the following code snippet to it:

src/components/FormField.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';

class FormField extends Component {

  // initialize state
  state = { value: '', dirty: false, errors: [] }

  hasChanged = e => {
    e.preventDefault();

    // destructure props - assign default dummy functions to validator and onStateChanged props
    const { label, required = false, validator = f => f, onStateChanged = f => f } = this.props;

    const value = e.target.value;
    const isEmpty = value.length === 0;
    const requiredMissing = this.state.dirty && required && isEmpty;

    let errors = [];

    if (requiredMissing) {
      // if required and is empty, add required error to state
      errors = [ ...errors, `${label} is required` ];
    } else if ('function' === typeof validator) {
      try {
        validator(value);
      } catch (e) {
        // if validator throws error, add validation error to state
        errors = [ ...errors, e.message ];
      }
    }

    // update state and call the onStateChanged callback fn after the update
    // dirty is only changed to true and remains true on and after the first state update
    this.setState(({ dirty = false }) => ({ value, errors, dirty: !dirty || dirty }), () => onStateChanged(this.state));
  }

  render() {
    const { value, dirty, errors } = this.state;
    const { type, label, fieldId, placeholder, children } = this.props;

    const hasErrors = errors.length > 0;
    const controlClass = ['form-control', dirty ? hasErrors ? 'is-invalid' : 'is-valid' : '' ].join(' ').trim();

    return (
      <Fragment>
        <div className="form-group px-3 pb-2">
          <div className="d-flex flex-row justify-content-between align-items-center">
            <label htmlFor={fieldId} className="control-label">{label}</label>
            {/** Render the first error if there are any errors **/}
            { hasErrors && <div className="error form-hint font-weight-bold text-right m-0 mb-2">{ errors[0] }</div> }
          </div>
          {/** Render the children nodes passed to component **/}
          {children}
          <input type={type} className={controlClass} id={fieldId} placeholder={placeholder} value={value} onChange={this.hasChanged} />
        </div>
      </Fragment>
    );
  }

}

FormField.propTypes = {
  type: PropTypes.oneOf(["text", "password"]).isRequired,
  label: PropTypes.string.isRequired,
  fieldId: PropTypes.string.isRequired,
  placeholder: PropTypes.string.isRequired,
  required: PropTypes.bool,
  children: PropTypes.node,
  validator: PropTypes.func,
  onStateChanged: PropTypes.func
};

export default FormField;

We are doing a handful of stuff in this component. Let’s break it down a little bit:

Input State: First, you initialized state for the form field component to keep track of the current value of the input field, the dirty status of the field, and any existing validation errors. A field becomes dirty the moment its value first changes and remains dirty.

Handle Input Change: Next, you added the hasChanged(e) event handler to update the state value to the current input value on every change to the input. In the handler, you also resolve the dirty state of the field. You check if the field is a required field based on props, and add a validation error to the state errors array if the value is empty.

However, if the field is not a required field or is required but not empty, then you delegate to the validation function passed in the optional validator prop, calling it with the current input value, and adding the thrown validation error to the state errors array (if there is any error).

Finally, you update the state and pass a callback function to be called after the update. The callback function calls the function passed in the optional onStateChanged prop, passing the updated state as its argument. This will become handy for propagating state changes outside the component.

Rendering and Props: Here you are rendering the input field and its label. You also conditionally render the first error in the state errors array (if there are any errors). Notice how you dynamically set the classes for the input field to show validation status using built-in classes from Bootstrap. You also render any children nodes contained in the component.

As seen in the component’s propTypes, the required props for this component are type ('text' or 'password'), label, placeholder, and fieldId. The remaining components are optional.

The EmailField Component

Create a new file EmailField.js in the src/components directory and add the following code snippet to it:

src/components/EmailField.js
import React from 'react';
import PropTypes from 'prop-types';
import { validate } from 'isemail';

import FormField from './FormField';

const EmailField = props => {

  // prevent passing type and validator props from this component to the rendered form field component
  const { type, validator, ...restProps } = props;

  // validateEmail function using the validate() method of the isemail package
  const validateEmail = value => {
    if (!validate(value)) throw new Error('Email is invalid');
  };

  // pass the validateEmail to the validator prop
  return <FormField type="text" validator={validateEmail} {...restProps} />
};

EmailField.propTypes = {
  label: PropTypes.string.isRequired,
  fieldId: PropTypes.string.isRequired,
  placeholder: PropTypes.string.isRequired,
  required: PropTypes.bool,
  children: PropTypes.node,
  onStateChanged: PropTypes.func
};

export default EmailField;

In the EmailField component, you are rendering a FormField component and passing an email validation function to the validator prop. You are using the validate() method of the isemail package for the email validation.

You may also notice that all other props except the type and validator props are transferred from the EmailField component to the FormField component.

The PasswordField Component

Create a new file PasswordField.js in the src/components directory and add the following code snippet to it:

src/components/PasswordField.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import zxcvbn from 'zxcvbn';

import FormField from './FormField';

class PasswordField extends Component {

  constructor(props) {

    super(props);
    const { minStrength = 3, thresholdLength = 7 } = props;

    // set default minStrength to 3 if not a number or not specified
    // minStrength must be a a number between 0 - 4

    this.minStrength = typeof minStrength === 'number'
      ? Math.max( Math.min(minStrength, 4), 0 )
      : 3;

    // set default thresholdLength to 7 if not a number or not specified
    // thresholdLength must be a minimum value of 7

    this.thresholdLength = typeof thresholdLength === 'number'
      ? Math.max(thresholdLength, 7)
      : 7;

    // initialize internal component state
    this.state = { password: '', strength: 0 };
  };

  stateChanged = state => {

    // update the internal state using the updated state from the form field

    this.setState({
      password: state.value,
      strength: zxcvbn(state.value).score
    }, () => this.props.onStateChanged(state));

  };

  validatePasswordStrong = value => {
    // ensure password is long enough
    if (value.length <= this.thresholdLength) throw new Error("Password is short");

    // ensure password is strong enough using the zxcvbn library
    if (zxcvbn(value).score < this.minStrength) throw new Error("Password is weak");
  };

  render() {
    const { type, validator, onStateChanged, children, ...restProps } = this.props;
    const { password, strength } = this.state;

    const passwordLength = password.length;
    const passwordStrong = strength >= this.minStrength;
    const passwordLong = passwordLength > this.thresholdLength;

    // dynamically set the password length counter class
    const counterClass = ['badge badge-pill', passwordLong ? passwordStrong ? 'badge-success' : 'badge-warning' : 'badge-danger'].join(' ').trim();

    // password strength meter is only visible when password is not empty
    const strengthClass = ['strength-meter mt-2', passwordLength > 0 ? 'visible' : 'invisible'].join(' ').trim();

    return (
      <Fragment>
        <div className="position-relative">
          {/** Pass the validation and stateChanged functions as props to the form field **/}
          <FormField type="password" validator={this.validatePasswordStrong} onStateChanged={this.stateChanged} {...restProps}>
            <span className="d-block form-hint">To conform with our Strong Password policy, you are required to use a sufficiently strong password. Password must be more than 7 characters.</span>
            {children}
            {/** Render the password strength meter **/}
            <div className={strengthClass}>
              <div className="strength-meter-fill" data-strength={strength}></div>
            </div>
          </FormField>
          <div className="position-absolute password-count mx-3">
            {/** Render the password length counter indicator **/}
            <span className={counterClass}>{ passwordLength ? passwordLong ? `${this.thresholdLength}+` : passwordLength : '' }</span>
          </div>
        </div>
      </Fragment>
    );
  }

}

PasswordField.propTypes = {
  label: PropTypes.string.isRequired,
  fieldId: PropTypes.string.isRequired,
  placeholder: PropTypes.string.isRequired,
  required: PropTypes.bool,
  children: PropTypes.node,
  onStateChanged: PropTypes.func,
  minStrength: PropTypes.number,
  thresholdLength: PropTypes.number
};

export default PasswordField;

This component is using the zxcvbn JavaScript password strength estimator package. The package exports a zxcvbn() function that takes a password string as its first argument and returns an object with several properties for the password strength estimation. In this tutorial, we would be concerned only with the score property, which is an integer from 0 - 4, which can be useful for implementing a visual strength bar.

Here is a breakdown of what is happening in the PasswordField component:

Initialization: In the constructor(), you created two instance properties, thresholdLangth and minStrength, from their corresponding prop passed to the component. The thresholdLength is the minimum password length before it can be considered sufficiently long. It defaults to 7 and cannot be lower. The minStrength is the minimum zxcvbn score before the password is considered to be strong enough. Its value ranges from 0-4. It defaults to 3 if not specified.

You also initialized the internal state of the password field to store the current password and password strength.

Handling Password Changes: You defined a password validation function that will be passed to the validator prop of the underlying FormField component. The function ensures that the password length is longer than the thresholdLength and also has a minimum zxcvbn() score of the specified minStrength.

You also defined a stateChanged() function, which will be passed to the onStateChanged prop of the FormField component. This function retrieves the updated state of the FormField component and uses it to compute and update the new internal state of the PasswordField component.

The callback function will be called after the internal state update. The callback function calls the function passed in the optional onStateChanged prop of the PasswordField component, passing the updated FormField state as its argument.

Rendering and Props: Here you rendered the underlying FormField component alongside some elements for input hint, password strength meter, and password length counter.

The password strength meter indicates the strength of the current password based on the state and is configured to be dynamically invisible if the password length is 0. The meter will indicate different colors for different strength levels.

The password length counter indicates when the password is long enough. It shows the password length if the password is not longer than the thresholdLength, otherwise it shows the thresholdLength followed by a plus(+).

The PasswordField component accepts two additional optional fields, minStrength and thresholdLength, as defined in the component’s propTypes.

The JoinForm Component

Create a new file JoinForm.js in the src/components directory and add the following code snippet to it:

src/components/JoinForm.js
import React, { Component } from 'react';

import FormField from './FormField';
import EmailField from './EmailField';
import PasswordField from './PasswordField';

class JoinForm extends Component {

  // initialize state to hold validity of form fields
  state = { fullname: false, email: false, password: false }

  // higher-order function that returns a state change watch function
  // sets the corresponding state property to true if the form field has no errors
  fieldStateChanged = field => state => this.setState({ [field]: state.errors.length === 0 });

  // state change watch functions for each field
  emailChanged = this.fieldStateChanged('email');
  fullnameChanged = this.fieldStateChanged('fullname');
  passwordChanged = this.fieldStateChanged('password');

  render() {
    const { fullname, email, password } = this.state;
    const formValidated = fullname && email && password;

    // validation function for the fullname
    // ensures that fullname contains at least two names separated with a space
    const validateFullname = value => {
      const regex = /^[a-z]{2,}(\s[a-z]{2,})+$/i;
      if (!regex.test(value)) throw new Error('Fullname is invalid');
    };

    return (
      <div className="form-container d-table-cell position-relative align-middle">
        <form action="/" method="POST" noValidate>

          <div className="d-flex flex-row justify-content-between align-items-center px-3 mb-5">
            <legend className="form-label mb-0">Support Team</legend>
            {/** Show the form button only if all fields are valid **/}
            { formValidated && <button type="button" className="btn btn-primary text-uppercase px-3 py-2">Join</button> }
          </div>

          <div className="py-5 border-gray border-top border-bottom">
            {/** Render the fullname form field passing the name validation fn **/}
            <FormField type="text" fieldId="fullname" label="Full Name" placeholder="Enter Full Name" validator={validateFullname} onStateChanged={this.fullnameChanged} required />

            {/** Render the email field component **/}
            <EmailField fieldId="email" label="Email" placeholder="Enter Email Address" onStateChanged={this.emailChanged} required />

            {/** Render the password field component using thresholdLength of 7 and minStrength of 3 **/}
            <PasswordField fieldId="password" label="Password" placeholder="Enter Password" onStateChanged={this.passwordChanged} thresholdLength={7} minStrength={3} required />
          </div>

        </form>
      </div>
    );
  }

}

export default JoinForm;

The JoinForm component wraps the form field components that make up our form. We initialized state to hold the validity of the three form fields: fullname, email, and password. They are all false, or invalid, initially.

We also defined state change watch functions for each field to update the form state accordingly. The watch function checks if there are no errors in a field and updates the form internal state for that field to true, or valid. These watch functions are then assigned to the onStateChanged prop of each form field component to monitor state changes.

Finally, the form is rendered. Notice that you added a validation function to the fullname field to ensure that at least two names, separated by a space and containing only alphabet characters, are provided.

The App Component

Up until this point, the browser still renders the boilerplate React application. Now you will modify the App.js file in the src directory to render the JoinForm inside the AppComponent.

The App.js file will look like the following snippet:

src/App.js
import React from 'react';
import JoinForm from './components/JoinForm';
import './App.css';

function App() {
  return (
    <div className="main-container d-table position-absolute m-auto">
      <JoinForm />
    </div>
  );
}

export default App;

Step 3 — Styling with Sass

You are one step away from the final look and feel of your application. At the moment, everything may seem a little out of place. In this step, you will go ahead and define some style rules to style the form.

In order to take advantage of powerful Sass variables, nesting, and loops, we have previously installed the dependency of node-sass. You are using Sass to generate a CSS file that browsers can understand.

There are two things you will need to change to utilize Sass in your application after installing the dependency:

  • Rename the file src/App.css to src/App.scss.
  • Edit the import line in src/App.js to reference the renamed file.

After renaming the src/App.css file, update your src/App.js file to the following:

src/App.js
import './App.scss';

Save and close the file.

Next, replace the existing content in the App.scss file with the following code to format the application:

src/App.scss
/** Declare some variables **/
$primary: #007bff;

// Password strength meter color for the different levels
$strength-colors: (darkred, orangered, orange, yellowgreen, green);

// Gap width between strength meter bars
$strength-gap: 6px;

body {
  font-size: 62.5%;
}

.main-container {
  width: 400px;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.form-container {
  bottom: 100px;
}

legend.form-label {
  font-size: 1.5rem;
  color: desaturate(darken($primary, 10%), 60%);
}

.control-label {
  font-size: 0.8rem;
  font-weight: bold;
  color: desaturate(darken($primary, 10%), 80%);
}

.form-control {
  font-size: 1rem;
}

.form-hint {
  font-size: 0.6rem;
  line-height: 1.4;
  margin: -5px auto 5px;
  color: #999;

  &.error {
    color: #C00;
    font-size: 0.8rem;
  }
}

button.btn {
  letter-spacing: 1px;
  font-size: 0.8rem;
  font-weight: 600;
}

.password-count {
  bottom: 16px;
  right: 10px;
  font-size: 1rem;
}

.strength-meter {
  position: relative;
  height: 3px;
  background: #DDD;
  margin: 7px 0;
  border-radius: 2px;

  // Dynamically create the gap effect
  &:before,
  &:after {
    content: '';
    height: inherit;
    background: transparent;
    display: block;
    border-color: #FFF;
    border-style: solid;
    border-width: 0 $strength-gap 0;
    position: absolute;
    width: calc(20% + #{$strength-gap});
    z-index: 10;
  }

  // Dynamically create the gap effect
  &:before {
    left: calc(20% - #{($strength-gap / 2)});
  }

  // Dynamically create the gap effect
  &:after {
    right: calc(20% - #{($strength-gap / 2)});
  }
}

.strength-meter-fill {
  background: transparent;
  height: inherit;
  position: absolute;
  width: 0;
  border-radius: inherit;
  transition: width 0.5s ease-in-out, background 0.25s;

  // Dynamically generate strength meter color styles
  @for $i from 1 through 5 {
    &[data-strength='#{$i - 1}'] {
      width: (20% * $i);
      background: nth($strength-colors, $i);
    }
  }
}

You have succeeded in adding the styles required by your application. Notice the use of generated CSS content in the .strength-meter:before and .strength-meter:after pseudo-elements to add gaps to the password strength meter.

You also used the Sass @for directive to dynamically generate fill colors for the strength meter at different password strength levels.

The final app screen will look like this:

Final View

With validation errors, the screen will look like this:

Final View with Errors

Without any errors, when all fields are valid, the screen will look like this:

Final View without Errors

Conclusion

In this tutorial, you created a password strength meter based on the zxcvbn JavaScript library in your React application. For a detailed usage guide and documentation of the zxcvbn library, see the zxcvbn repository on GitHub. For a complete code sample of this tutorial, checkout the password-strength-react-demo repository on GitHub. You can also get a live demo of this tutorial on Code Sandbox.

If you are interested in the AngularJS version of this article, you can take a look at: Password Strength Meter in AngularJS.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Glad Chinda

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel