In today’s digital landscape, robust password security is more critical than ever. To address this need, I present a comprehensive and highly efficient code solution that will help you create a password creation component with ease.
First things first, I will include here the entire code, after that you can read the explanation, if you want.
PasswordCreation.tsx
import { useState, useRef } from 'react';
import styled from 'styled-components';
import CheckListItem from './CheckListItem';
import TogglePasswordButton from './TogglePasswordButton';
import PasswordStrengthIndicator from './PasswordStrengthIndicator';
import PasswordGenerator from './PasswordGenerator';
const PasswordField = styled.input`
display: block;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
`;
const FormField = styled.div`
padding-bottom: 15px;
max-width: 197px;
display: flex;
flex-direction: column;
gap: 5px 0;
.error {
color: red;
margin: 0;
}
`
const CheckList = styled.ul`
list-style: none;
padding: 0;
`;
const SubmitButton = styled.input`
background: green;
border-radius: 5px;
border: 0;
padding: 10px;
align-self: flex-end;
color: #fff;
cursor: pointer;
&[disabled] {
background: #ccc;
pointer-events: none;
}
`;
const InputWrapper = styled.div`
display: flex;
`
const StyledPasswordStrengthIndicator = styled.div<{strengthColor: string}>`
margin-top: 10px;
height: 10px;
border-radius: 5px;
background-color: ${({strengthColor}) => strengthColor};
`;
const PasswordCreation = () => {
const [passwordData, setPasswordData] = useState({
password: '',
confirmation: '',
isConfirmed: true
});
const {password, confirmation, isConfirmed} = passwordData;
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
const [isPasswordVisible, setIsPasswordVisible] = useState(false)
const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] = useState(false)
const patterns = [
{ label: 'Minimum 8 characters', pattern: '.{8,}' },
{ label: 'A lowercase letter', pattern: '[a-z]+' },
{ label: 'An uppercase letter', pattern: '[A-Z]+' },
{ label: 'A number', pattern: '[0-9]+' },
{ label: 'No whitespace', pattern: '^[^ ]+$' },
{ label: 'At least one special symbol', pattern: '[!*&#@$]' },
];
const regexp = new RegExp(
`^${patterns.map(({pattern}) => `(?=.*${pattern})`).join('')}.*$`
);
const checkListData = patterns.map((pattern) => ({
data: pattern.label,
pattern: pattern.pattern,
}));
const isValid = (pattern:string) => new RegExp(pattern).test(password);
const confirmationInput = useRef<null | HTMLInputElement>(null);
const checkIfPasswordIdentical = (event: {target: HTMLInputElement}) => {
const inputValue = event.target.value;
const isPasswordStrongEnough = regexp.test(password);
const isPasswordConfirmed = inputValue === password;
setPasswordData((prevData) => ({
...prevData,
confirmation: inputValue,
isConfirmed: isPasswordConfirmed,
}));
setIsSubmitDisabled(!(isPasswordStrongEnough && isPasswordConfirmed));
}
const validateForm = (event: { target: HTMLInputElement }) => {
const inputValue = event.target.value;
const isPasswordStrongEnough = regexp.test(inputValue);
const isPasswordConfirmed = inputValue === confirmation;
setPasswordData(prevData => ({
...prevData,
password: inputValue,
isConfirmed: isPasswordConfirmed,
}));
setIsSubmitDisabled(!(isPasswordStrongEnough && isPasswordConfirmed));
}
return (
<section>
<h2>Create a new password</h2>
<form>
<FormField>
<label htmlFor="password">New password</label>
<InputWrapper>
<PasswordField onChange={validateForm} type={isPasswordVisible ? 'text' : 'password'} id="password" />
<TogglePasswordButton onClick={() => setIsPasswordVisible(!isPasswordVisible)} visible={isPasswordVisible} />
</InputWrapper>
</FormField>
<FormField>
<label htmlFor="confirmPassword">Confirm password</label>
<InputWrapper>
<PasswordField onChange={checkIfPasswordIdentical} className={isConfirmed ? 'valid': 'invalid'} ref={confirmationInput} type={isConfirmPasswordVisible ? 'text' : 'password'} id="confirmPassword" />
<TogglePasswordButton onClick={() => setIsConfirmPasswordVisible(!isConfirmPasswordVisible)} visible={isConfirmPasswordVisible}/>
</InputWrapper>
{!isConfirmed && <p className="error">Password confirmation failed</p>}
<PasswordStrengthIndicator patterns={patterns} isValid={isValid}/>
</FormField>
<FormField>
<SubmitButton type='submit' value="Create" disabled={isSubmitDisabled} data-testid="create-button" />
</FormField>
<PasswordGenerator />
</form>
<h3>Password must contain the following:</h3>
<CheckList>
{checkListData.map(({data, pattern}, i) => {
return (
<CheckListItem key={i} msg={data} valid={isValid(pattern)} />
)})
}
</CheckList>
</section>
);
}
export default PasswordCreation;
PasswordStrengthIndicator.tsx
import React from 'react';
import styled from 'styled-components';
interface patternsObject {
label: string;
pattern: string;
}
interface PasswordStrengthIndicatorProps {
patterns: patternsObject[];
isValid: (pattern: string) => boolean;
}
const Indicator = styled.div<{ strengthColor: string }>`
margin-top: 10px;
height: 10px;
border-radius: 5px;
background-color: ${(props) => props.strengthColor};
`;
const StatusText = styled.span<{ strengthColor: string }>`
color: ${(props) => props.strengthColor};
`;
const PasswordStrengthIndicator = ({patterns, isValid} :PasswordStrengthIndicatorProps ) => {
const calculatePasswordStrength = () => {
let strength = -1;
patterns.forEach((pattern) => {
if (isValid(pattern.pattern)) {
strength += 1;
}
});
const strengthColors = ['#ff6347', '#ffa511', '#ffa500', '#ffd700', '#799900', '#00ff00'];
const statusTexts = ['Very Weak', 'Moderately Weak', 'Weak', 'Moderate', 'Strong', 'Very Strong'];
const strengthColor = strengthColors[strength] || '#808080';
const statusText = statusTexts[strength] || 'Invalid';
return { strengthColor, statusText };
};
const { strengthColor, statusText } = calculatePasswordStrength();
return (
<>
<Indicator strengthColor={strengthColor} data-testid="password-strength-indicator" />
<StatusText strengthColor={strengthColor} data-testid="status-text">{statusText}</StatusText>
</>
);
};
export default PasswordStrengthIndicator;
TogglePasswordButton.tsx
import React from 'react';
import styled from 'styled-components';
const Button = styled.button<{visible: boolean}>`
cursor: pointer;
border-radius: 5px;
border:1px solid #ccc;
width: 40px;
&:before {
content: '\\1F441';
text-decoration: ${({ visible }) => (visible ? 'none' : 'line-through')};
font-size: 14px;
}
`;
interface TogglePasswordButtonProps {
visible: boolean;
onClick: () => void;
}
const TogglePasswordButton = ({
visible,
onClick
}:TogglePasswordButtonProps) => (
<Button type="button" visible={visible} onClick={onClick} data-testid="toggle-visibility" />
);
export default TogglePasswordButton;
CheckListItem.tsx
import styled from 'styled-components';
const CheckListItem = styled.li`
&::before {
display: inline-block;
width: 20px;
}
&.invalid {
color: red;
&::before {
content: '\\2BBE';
}
}
&.valid {
color: green;
&::before {
content: '\\2713';
}
}
`
type CheckListProps = {
msg: string;
valid: boolean;
}
const CheckList = ({msg, valid}: CheckListProps) => {
return (
<CheckListItem className={valid ? 'valid' : 'invalid'}>{msg}</CheckListItem>
);
}
export default CheckList;
PasswordGenerator.tsx
import React, { useState } from 'react';
import styled from 'styled-components';
const PopupContainer = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
`;
const GeneratorButton = styled.button`
width: 197px
`
const PasswordGenerator = () => {
const [isPopupVisible, setIsPopupVisible] = useState(false);
const [newPassword, setNewPassword] = useState('');
const generatePassword = () => {
const requiredCharacters = {
lowercase: 'abcdefghijklmnopqrstuvwxyz',
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
number: '0123456789',
special: '!@#$%^&*'
};
const passwordLength = 10;
const passwordCharacters = Object.values(requiredCharacters).join('');
let generatedPassword = '';
// Ensure at least one character from each required character set
Object.values(requiredCharacters).forEach((characterSet) => {
generatedPassword += getRandomCharacter(characterSet);
});
//Generate remaining characters
for (let i = Object.keys(requiredCharacters).length; i < passwordLength; i++) {
const randomCharacterSet = passwordCharacters[Math.floor(Math.random() * passwordCharacters.length)];
generatedPassword += getRandomCharacter(randomCharacterSet);
}
setNewPassword(generatedPassword);
setIsPopupVisible(true);
navigator.clipboard.writeText(generatedPassword)
};
const getRandomCharacter = (characterSet: string) => {
const randomIndex = Math.floor(Math.random() * characterSet.length);
return characterSet.charAt(randomIndex);
};
const handlePopupClose = () => {
setIsPopupVisible(false);
};
return (
<>
<GeneratorButton type="button" onClick={generatePassword}>Generate Password</GeneratorButton>
{isPopupVisible && (
<PopupContainer data-testid="popup-container">
<div>Generated Password: <span data-testid="generated-password-text">{newPassword}</span></div>
<p>This password was copied to your clipboard</p>
<button type="button" onClick={handlePopupClose}>Close</button>
</PopupContainer>
)}
</>
);
};
export default PasswordGenerator;
PasswordCreation.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import PasswordCreation from './PasswordCreation';
import CheckList from './CheckListItem';
import PasswordGenerator from './PasswordGenerator';
import PasswordStrengthIndicator from './PasswordStrengthIndicator';
describe('PasswordCreation', () => {
test('renders PasswordCreation component with all subcomponents', () => {
render(<PasswordCreation />);
// PasswordField
const passwordField = screen.getByLabelText('New password');
expect(passwordField).toBeInTheDocument();
// ConfirmationPasswordField
const confirmationPasswordField = screen.getByLabelText('Confirm password');
expect(confirmationPasswordField).toBeInTheDocument();
// TogglePasswordButtons
const togglePasswordButtons = screen.getAllByTestId('toggle-visibility');
expect(togglePasswordButtons.length).toBe(2);
// SubmitButton
const submitButton = screen.getByRole('button', { name: 'Create' });
expect(submitButton).toBeInTheDocument();
// PasswordStrengthIndicator
const passwordStrengthIndicator = screen.getByTestId('password-strength-indicator');
expect(passwordStrengthIndicator).toBeInTheDocument();
// CheckListItems
const checkListItems = screen.getAllByRole('listitem');
expect(checkListItems.length).toBeGreaterThan(0);
});
test('should update password value', () => {
render(<PasswordCreation />);
const passwordField = screen.getByLabelText('New password') as HTMLInputElement;
const passwordValue = 'testpassword';
fireEvent.change(passwordField, { target: { value: passwordValue } });
expect(passwordField.value).toBe(passwordValue);
});
test('should update confirmation value', () => {
render(<PasswordCreation />);
const confirmPasswordField = screen.getByLabelText('Confirm password') as HTMLInputElement;
const confirmationValue = 'testpassword';
fireEvent.change(confirmPasswordField, { target: { value: confirmationValue } });
expect(confirmPasswordField.value).toBe(confirmationValue);
});
test('validates password and confirmation match', () => {
render(<PasswordCreation />);
const passwordField = screen.getByLabelText('New password');
const confirmationPasswordField = screen.getByLabelText('Confirm password');
fireEvent.change(passwordField, { target: { value: 'password123' } });
fireEvent.change(confirmationPasswordField, { target: { value: 'password123' } });
const errorElement = screen.queryByText('Password confirmation failed');
expect(errorElement).not.toBeInTheDocument();
});
});
describe('CheckList', () => {
test('renders CheckList component with valid status', () => {
render(<CheckList msg="Agree to Terms" valid={true} />);
const checkListItem = screen.getByText('Agree to Terms');
expect(checkListItem).toBeInTheDocument();
expect(checkListItem).toHaveClass('valid');
expect(checkListItem).toHaveStyle({ color: 'green' });
});
test('renders CheckList component with invalid status', () => {
render(<CheckList msg="Agree to Terms" valid={false} />);
const checkListItem = screen.getByText('Agree to Terms');
expect(checkListItem).toBeInTheDocument();
expect(checkListItem).toHaveClass('invalid');
expect(checkListItem).toHaveStyle({ color: 'red' });
});
});
describe('PasswordGenerator', () => {
test('renders the PasswordGenerator component', () => {
render(<PasswordGenerator />);
const generateButton = screen.getByText('Generate Password');
expect(generateButton).toBeInTheDocument();
});
test('generates a password and shows the popup on button click', () => {
// Mock the writeText method of the clipboard API
const writeTextMock = jest.fn();
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: writeTextMock,
},
});
render(<PasswordGenerator />);
const generateButton = screen.getByText('Generate Password');
fireEvent.click(generateButton);
const generatedPassword = screen.getByText(/Generated Password:/i);
const copyConfirmation = screen.getByText(/This password was copied to your clipboard/i);
const closeButton = screen.getByRole('button', { name: 'Close' });
expect(generatedPassword).toBeInTheDocument();
expect(copyConfirmation).toBeInTheDocument();
expect(closeButton).toBeInTheDocument();
});
});
describe('PasswordStrengthIndicator', () => {
const patterns = [
{ label: 'Pattern 1', pattern: 'pattern1' },
{ label: 'Pattern 2', pattern: 'pattern2' },
{ label: 'Pattern 3', pattern: 'pattern3' },
];
test('renders the PasswordStrengthIndicator component', () => {
render(<PasswordStrengthIndicator patterns={patterns} isValid={() => true} />);
const indicator = screen.getByTestId('password-strength-indicator');
const statusText = screen.getByTestId('status-text');
expect(indicator).toBeInTheDocument();
expect(statusText).toBeInTheDocument();
});
test('displays the correct strength color based on the pattern validity', () => {
const mockGetComputedStyle = jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
getPropertyValue: () => 'rgb(128, 128, 128)',
} as unknown as CSSStyleDeclaration));
const isValid = jest.fn().mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(true);
render(<PasswordStrengthIndicator patterns={patterns} isValid={isValid} />);
const indicator = screen.getByTestId('password-strength-indicator');
expect(indicator).toHaveStyle('background-color: rgb(128, 128, 128)');
expect(isValid).toHaveBeenCalledTimes(3);
expect(isValid).toHaveBeenNthCalledWith(1, 'pattern1');
expect(isValid).toHaveBeenNthCalledWith(2, 'pattern2');
expect(isValid).toHaveBeenNthCalledWith(3, 'pattern3');
mockGetComputedStyle.mockRestore();
});
});
This component is a big one so I spread it out on multiple files to make some order in chaos. This is how the file system looks like for it:

The main file is the PasswordCreation.tsx that contains the whole password creation form. The TogglePasswordButton is just a See/Hide password functionality separated in an individual tsx file. The PasswordStrengthIndicator is just a visual extra to show if you password is weak, moderate or strong(there are more statuses actually). Also this is shown visually with colours. The CheckListItem is detailing what requirements are met or still not met. The PasswordGenerator is the cherry on top, if the user does not want to struggle with creating a new password, a password generator will be available that will create a password that meets all requirements and it’s status is Very strong. I also included a test file(Jest and react test library).

Also in case you want to restrict user to not copy paste the password to the password confirmation field, you could use something like this:
confirmationInput.addEventListener("paste", evt => evt.preventDefault();
);
Also the Password Generator works likes this:

Let me explain

First step is to create a state that holds the password field value, the password confirmation field value and the comparison status between those two values. If the 2 values are identical that the isConfirmed property is true. After this I destructure that object to write shorter code when referencing the passwordData properties.
The isSubmitDisabled is changed when the password met all requirements and the value is identical with the confirmation field’s value.
I created for both password inputs a separate state, of course you could also do one state for it and put both inside as properties and then destructure it. The isPasswordVisible and the isConfirmPasswordVisible hold the information about the input’s value visibility.
The patterns variable actually has the checklist with the requirements and the actual regular expression pattern for each of them. This way we can check the password value separately for each criteria. But then again we want to also check if the password is valid entirely so I also made a regular expression with all criteria all together.
The regexp variable in end will look like this:

I used positive lookaheads for each criteria, because what it does for each the search starts from the beginning of the string and it is not captured, it just checks if exists and then goes to the other one and checks again. The .* before each criteria means that the actual pattern can start from anywhere, does not matter what is before it or after it.
Eventually we map through that checkListData object that we just created:

The isValid function actually just checks if the pattern attached to an item meets the criteria in the checklist:

Validating our password

When we type in the password input, we always check 2 things: the overall validity of the password(if it meets all criteria from the checklist) and if the password is confirmed in the confirmation input(the value matches the password value. We update the passwordData state and enable/disable the submit button depending if both validation and confirmation are ok.
Confirming the password

This is actually almost the same as the validation function, with the difference that we compare the input’s value that we are interacting with, with the value of the other input.
The checklist component

This one is really short, it just get’s one by one the text(msg) as prop and the isValid(valid). After that it just creates this <li> item with the class name valid or invalid(green or red).
The password visibility toggler

This component get’s 2 props, the visible and the onClick prop. Both values are coming from the parent component, no calculation or function is done in this small component.

Not much is happening here, instead of putting the setter for the state inside the component I just left it as a prop and also added the value on it as a prop.
How about the password strength indicator?

I created here 6 colours for 6 validation criteria and also 6 status text for it. I start the initial count from -1 and initially if the strength is not a valid index in the strengthColors then we render a grey colour with the Invalid status text.
We iterate through the patterns array with the criteria’s and for those that are valid, we add 1 to that count and than use that count to choose from the 2 arrays(strenghtColors and statusTexts) an item from that index(the count is the index). Easy-peasy, right?
The extra in the end – the password generator component

First we need to create for each required character the list of options from where we can choose a random character. So we have lower and uppercase letters, numbers and special characters. The passwordLength I’ve put to 10 but you can choose 8 as it was initially if you want. Also we need an overall string containing all characters, I will explain later why.
The main engine of the password generator

First we generate an array item from each character set and then we pick a random character from each array item and we add it to the generatedPassword string variable.

The random character generation is just based on how many characters are in that string, that will be the maximum limit of that random number. After this we just use this number for the index of that character(charAt).
The second function uses the joined string, stored in the passwordCharacters where we do the same thing and get a random character but now from any type of character from those 4 types. This will serve as filling to make the password a 10 character password. The reason behind it is, that at the begining we generated only 1 character from each type(lowercase, uppercase, number, special character) and then we needed 6 more random characters of any type.
That’s all enjoy and please share, comment, like and subscribe for more.