How I Built a Password Strength for Our Signup Page.
Table of contents
Cool logo in the header of the image above don’t you think? Well, that’s Quidate. Quidate simplifies your crypto journey by offering instant cash for your digital assets. It’s the quickest way to turn your crypto holdings into spendable cash.
So here’s the thing, yeah? I was going through the designs that our Product Designer had just finished for some final changes and iterations, and I saw the password strength indicator. “Huh?” I said to myself, “And why is it like that?” Since I had already picked out the tech stack we were going to be using for the project, I gave the okay and went to work.
For this tutorial, I’ll be using a few packages/libraries which I will be listing bellow.
Tech Stack
Nextjs (UI library)
Formik (Form Library)
Yup (Form Validation)
Tailwind (Styling library)
Since we’ll be using Formik I’ll run you through how I mostly setup Formik for any project I’m working on trust me, it’ll be a fun read.
Just like any other framework you’re using, you want to model your components for reusability and also to be extensible. That is what we’ll be doing with our Formik input component. First of all, we’ll be creating a global input component that will be reused for most inputs we’ll be needing.
import React, { RefObject } from "react";
type InputProps = {
refElement?: RefObject<HTMLDivElement>;
label?: string;
className?: string;
containerClassName?: string;
labelClassName?: string;
inputContainerClassName?: string;
child?: React.ReactNode;
errorChild?: React.ReactNode;
inputType: string;
errorMessage?: React.ReactNode;
errorMessageClassName?: string;
value: string;
name: string;
id: string;
disable?: boolean;
placeholder: string;
touched: boolean | undefined;
error: string | undefined;
handleFocus?: () => void;
handleBlur: {
(e: React.FocusEvent<any, Element>): void;
<T = any>(fieldOrEvent: T): T extends string ? (e: any) => void : void;
};
handleChange?: (e: React.ChangeEvent<any>) => void;
handleKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
};
const Input: React.FC<FormTextInputProps> = ({
refElement,
label,
className,
containerClassName,
labelClassName,
inputContainerClassName,
child,
errorMessage,
errorMessageClassName,
inputType = "text",
handleBlur,
name,
value,
errorChild,
handleKeyDown,
handleChange,
id,
touched,
error,
disable,
placeholder,
handleFocus,
...props
}) => {
return (
<div
ref={refElement}
className={`w-full font-inter flex flex-col items-start gap-y-2 ${containerClassName}`}>
{label && (
<label
className={`${formLabelStyle} ${labelClassName}`}
htmlFor={id || name}>
{label}
</label>
)}
<div className={`relative w-full ${inputContainerClassName}`}>
<input
id={name}
name={name}
type={inputType}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
disabled={disable}
placeholder={placeholder}
onKeyDown={handleKeyDown}
value={value}
{...props}
className={` ${
touched && error
? "border-[#BC0016]"
: "border-primary-grey "
} ${value && "border-secondary-blue"} ${className}`}
/>
{child}
</div>
{touched && error ? (
<>
{errorMessage || (
<div className="flex flex-row items-center gap-x-2 text-[red] ease-in duration-200 text-xs">
<p>{error}</p>
</div>
)}
</>
) : null}
{errorChild}
</div>
);
};
export default Input;
The code above is the boilerplate for our global input component, which we’ll be using for our password field. It can also be extended for other input use cases like ‘text’ and ‘number’ fields, but for now, our focus is on the password field. Feel free to extend or modify the code to suit your taste and style preferences.
We’ll start by creating our password constraints, which is just a list of rules put in place for every user to follow when they’re creating a password.
export const passwordConstraintContent: {
id: number;
name: string;
message: string;
}[] = [
{
id: 1,
name: "minLength",
message: "Password must be at least 10 characters",
},
{
id: 2,
name: "lowercase",
message: "Must contain at least one lowercase letter",
},
{
id: 3,
name: "uppercase",
message: "Must contain at least one uppercase letter",
},
{
id: 4,
name: "number",
message: "Must contain at least one number",
},
{
id: 5,
name: "special",
message: "Must contain at least one special character",
},
];
Let’s also create our validation schema using Yup. By the way, you can use any validation schema of your choice. I just like Yup, and I’ve also been using Zod for a while now. I find it very interesting and good for multiple use cases. For the sake of this tutorial, I will be focusing on just the password field because that’s where our interest lies. So, our validation schema will just be specifically for our password field.
import * as yup from "yup";
export const signUpValidationSchema = yup.object().shape({
password: yup
.string()
.required("Password is required")
.test("minLength", "Password must be at least 10 characters", (value) =>
/^.{10,}$/.test(value),
)
.test(
"lowercase",
"Must contain at least one lowercase letter",
(value) => /[a-z]/.test(value),
)
.test(
"uppercase",
"Must contain at least one uppercase letter",
(value) => /[A-Z]/.test(value),
)
.test("number", "Must contain at least one number", (value) =>
/\d/.test(value),
)
.test(
"special",
"Must contain at least one special character",
(value) => /[!@#$%^&*(),.?":{}|<>]/.test(value),
),
});
The code above is our Yup validation schema for our form. It’s as simple as this: for every password strength rule, we’ll write a regular expression for it. For example, if we want to make sure the user has an uppercase character in their password, we’ll write a regex to check for it. Got it? I’m sure you do :)
Pay attention to the fact that each regex we wrote in our validation schema, contains the same unique name in each password constraint we wrote earlier as this will be important in our password strength hook.
Now, let’s create our custom hook for our password strength.
export const useSignUpCriteria = (
setMatchedCriteria: React.Dispatch<React.SetStateAction<string[]>>,
values: {
password: string;
}
) => {
useEffect(() => {
// Update the matched criteria array dynamically
const updatedCriteria = [];
// Check if the password value is empty
if (values?.password?.trim() === "") {
setMatchedCriteria([]);
return;
}
//Add "minLength" to criteria if the value's length exceeds 9
if (/^.{10,}$/.test(values?.password)) {
updatedCriteria.push("minLength");
}
// Add "number" to criteria if the value contains a number
if (/\d/.test(values.password)) {
updatedCriteria.push("number");
}
// Add "lowercase" to criteria if the value contains a lowercase letter
if (/[a-z]/.test(values.password)) {
updatedCriteria.push("lowercase");
}
// Add "uppercase" to criteria if the value contains an uppercase letter
if (/[A-Z]/.test(values.password)) {
updatedCriteria.push("uppercase");
}
// Add "special" to criteria if the value contains a special character
if (/[!@#$%^&*(),.?":{}|<>]/.test(values.password)) {
updatedCriteria.push("special");
}
// Update the matchedCriteria array
setMatchedCriteria(updatedCriteria);
}, [values.password]);
};
I tried to make this custom hook as self-explanatory as possible, but if there’s too much going on and you can’t seem to place your finger on it, that’s not a bad thing. Learning is part of the journey. The code above is a custom hook that takes in two parameters.
setMatchedCriteria: this is an array state of strings (This holds all the constraints that fails when we do the regex validation for the password value)
value: this is the actuall password input value. With our use effect, which runs every time password value changes.
I know what you might be thinking already: that our custom hook does almost the same thing as our Yup validation. Well, yes, it sort of does, but in a slightly different way. Hear me out first. Imagine we had other fields like username and email that we also want to validate. We’d be putting them inside our Yup validation schema as well. Think of it like a 2FA for the password field alone. Formik does its own validation together with other input fields in the form, and our custom hook does the same too. But our custom hook stores these constraints in an array state which we passed as a parameter and we’ll be making use of that state later. :) See, I told you to hear me out.
Moving on we’ll create our form using useFormik
hook where we pass our initialValue
and validationSchema
which we assigned the Yup validation schema we created earlier to it.
"use client";
import { FormikHelpers, useFormik } from "formik";
import React, { useRef, useState } from "react";
import { passwordConstraintContent } from "../../../../../contents";
type Props = {};
const SignUp = (props: Props) => {
const [matchedCriteria, setMatchedCriteria] = useState<string[]>([]);
const [userTriesToSubmit, setUserTriesToSubmit] = useState<boolean>(false);
const refElement = useRef(null);
const initialValue: SignUpFormType = {
password: "",
};
const formik = useFormik({
initialValues: initialValue,
validationSchema: signUpValidationSchema,
onSubmit: (value: SignUpFormType, formikHelper) => {},
});
const { values, handleChange, handleBlur, errors, touched } = formik;
useSignUpCriteria(setMatchedCriteria, formik?.values);
return (
<div>
<form onSubmit={handleSubmit}>
<div className="flex flex-col w-full gap-y-3">
<Input
value={values?.password}
id={values?.password}
name={"password"}
handleChange={handleChange}
handleBlur={handleBlur}
touched={touched?.password}
error={errors?.password}
inputType={"password"}
placeholder="Enter password"
label="Password"
/>
<div className="font-satoshi flex flex-col gap-y-3">
{passwordConstraintContent?.map((child, index) => (
<div
className={`text-sm flex flex-row items-center gap-x-2 ${
matchedCriteria?.includes(child.name)
? "text-primary-green"
: userTriesToSubmit &&
!matchedCriteria?.includes(child.name)
? "text-[red]"
: "text-secondary-grey"
}`}>
<p>{child.message}</p>
</div>
)}
</div>
</div>
</form>
</div>
)
calling the useSignUpCriteria
hook we created earlier we pass in the 2 arguments it takes which are setMatchedCriteria
set state and formik.value
which is our password value. The code beneath the <Input />
component is where we let the user know when their password passes each constraint.
We looped through passwordConstraintContent
which we created earlier as well and do a simple check if the constraint name field is included in ourmatchedCriteria
array state if that returns true, tailwind CSS changes the text color to green else turns it to red.
By combining these elements, we’ve not only improved our application’s security but also created a more user-friendly experience. Remember, strong passwords are a crucial first line of defense in cybersecurity, and tools like this help users create better passwords without sacrificing usability.
I hope this tutorial has been helpful in showcasing how to implement a robust password strengthener. Feel free to adapt and expand upon these concepts in your own projects. Stay secure, and happy coding!
Subscribe to my newsletter
Read articles from Adigun olamide directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Adigun olamide
Adigun olamide
Software developer based in Lagos, Nigeria