Password Rails Components
Password input components with various features for your Ruby on Rails application.
Installation
1. Stimulus Controller Setup
Add the following controllers to your project based on which components you want to use:
Basic Password Controller
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = [
"input",
"toggleIcon",
"strengthBar",
"strengthText",
"lengthCheck",
"lowercaseCheck",
"uppercaseCheck",
"numberCheck",
"confirm",
"confirmToggleIcon",
"matchIndicator",
"matchText",
];
static values = {
strength: { type: Boolean, default: false },
requirements: { type: Boolean, default: false },
confirm: { type: Boolean, default: false },
confirmDelay: { type: Number, default: 300 },
};
connect() {
this.isVisible = false;
this.confirmVisible = false;
if (this.hasInputTarget) {
if (this.strengthValue) {
this.checkStrength();
}
if (this.requirementsValue) {
this.checkRequirements();
}
}
}
disconnect() {
// Clear any pending timeout when the controller is disconnected
if (this.confirmValue) {
clearTimeout(this.checkTimeout);
}
}
toggle() {
this.isVisible = !this.isVisible;
if (this.isVisible) {
this.inputTarget.type = "text";
this.updateIcon(this.toggleIconTarget, true);
} else {
this.inputTarget.type = "password";
this.updateIcon(this.toggleIconTarget, false);
}
}
toggleConfirm() {
if (!this.confirmValue) return;
this.confirmVisible = !this.confirmVisible;
if (this.confirmVisible) {
this.confirmTarget.type = "text";
this.updateIcon(this.confirmToggleIconTarget, true);
} else {
this.confirmTarget.type = "password";
this.updateIcon(this.confirmToggleIconTarget, false);
}
}
// Called on input event
handleInput() {
if (this.strengthValue) {
this.checkStrength();
}
if (this.requirementsValue) {
this.checkRequirements();
}
if (this.confirmValue && this.hasConfirmTarget && this.confirmTarget.value) {
this.checkMatch();
}
}
// Strength checking functionality
checkStrength() {
const password = this.inputTarget.value;
let strength = 0;
const feedback = [];
// Length check
if (password.length >= 8) {
strength += 25;
} else if (password.length > 0) {
feedback.push("At least 8 characters");
}
// Lowercase check
if (/[a-z]/.test(password)) {
strength += 25;
} else if (password.length > 0) {
feedback.push("Include lowercase letter");
}
// Uppercase check
if (/[A-Z]/.test(password)) {
strength += 25;
} else if (password.length > 0) {
feedback.push("Include uppercase letter");
}
// Number or special character check
if (/[0-9!@#$%^&*]/.test(password)) {
strength += 25;
} else if (password.length > 0) {
feedback.push("Include number or special character");
}
this.updateStrengthIndicator(strength);
}
updateStrengthIndicator(strength) {
if (!this.hasStrengthBarTarget) return;
// Update bar width and color
this.strengthBarTarget.style.width = `${strength}%`;
// Update color classes
this.strengthBarTarget.classList.remove(
"bg-red-500",
"bg-yellow-500",
"bg-lime-500",
"bg-green-500",
"dark:bg-red-400",
"dark:bg-yellow-400",
"dark:bg-lime-400",
"dark:bg-green-400",
"bg-neutral-300",
"dark:bg-neutral-600"
);
if (strength === 0) {
this.strengthBarTarget.classList.add("bg-neutral-300", "dark:bg-neutral-600");
} else if (strength <= 25) {
this.strengthBarTarget.classList.add("bg-red-500", "dark:bg-red-400");
} else if (strength <= 50) {
this.strengthBarTarget.classList.add("bg-yellow-500", "dark:bg-yellow-400");
} else if (strength < 100) {
this.strengthBarTarget.classList.add("bg-lime-500", "dark:bg-lime-400");
} else {
this.strengthBarTarget.classList.add("bg-green-500", "dark:bg-green-400");
}
// Update text
if (this.hasStrengthTextTarget) {
if (strength === 0) {
this.strengthTextTarget.textContent = "";
this.strengthTextTarget.classList.add("hidden");
} else {
this.strengthTextTarget.classList.remove("hidden");
// Remove all text color classes
this.strengthTextTarget.classList.remove(
"text-red-600",
"text-yellow-600",
"text-lime-600",
"text-green-600",
"dark:text-red-400",
"dark:text-yellow-400",
"dark:text-lime-400",
"dark:text-green-400"
);
// Update text content and color classes
if (strength <= 25) {
this.strengthTextTarget.textContent = "Weak";
this.strengthTextTarget.classList.add("text-red-600", "dark:text-red-400");
} else if (strength <= 50) {
this.strengthTextTarget.textContent = "Fair";
this.strengthTextTarget.classList.add("text-yellow-600", "dark:text-yellow-400");
} else if (strength < 100) {
this.strengthTextTarget.textContent = "Good";
this.strengthTextTarget.classList.add("text-lime-600", "dark:text-lime-400");
} else {
this.strengthTextTarget.textContent = "Strong";
this.strengthTextTarget.classList.add("text-green-600", "dark:text-green-400");
}
}
}
}
// Requirements checking functionality
checkRequirements() {
const password = this.inputTarget.value;
// Check length
this.updateRequirement(this.lengthCheckTarget, password.length >= 8);
// Check lowercase
this.updateRequirement(this.lowercaseCheckTarget, /[a-z]/.test(password));
// Check uppercase
this.updateRequirement(this.uppercaseCheckTarget, /[A-Z]/.test(password));
// Check number or special character
this.updateRequirement(this.numberCheckTarget, /[0-9!@#$%^&*]/.test(password));
}
updateRequirement(target, met) {
if (!target) return;
const icons = target.querySelectorAll("svg");
const uncheckedIcon = icons[0]; // First SVG is the unchecked circle
const checkedIcon = icons[1]; // Second SVG is the checked circle
const text = target.querySelector("span");
if (met) {
// Requirement is met - show checked icon
target.classList.remove("text-neutral-500", "dark:text-neutral-400");
target.classList.add("text-green-600", "dark:text-green-500");
// Toggle icons
if (uncheckedIcon) uncheckedIcon.classList.add("hidden");
if (checkedIcon) checkedIcon.classList.remove("hidden");
if (text) text.classList.add("line-through");
} else {
// Requirement is not met - show unchecked icon
target.classList.remove("text-green-600", "dark:text-green-500");
target.classList.add("text-neutral-500", "dark:text-neutral-400");
// Toggle icons
if (uncheckedIcon) uncheckedIcon.classList.remove("hidden");
if (checkedIcon) checkedIcon.classList.add("hidden");
if (text) text.classList.remove("line-through");
}
}
// Confirmation matching functionality
checkMatch() {
// Clear any existing timeout
clearTimeout(this.checkTimeout);
// Set a new timeout to check after the delay
this.checkTimeout = setTimeout(() => {
this.performCheck();
}, this.confirmDelayValue);
}
performCheck() {
const password = this.inputTarget.value;
const confirm = this.confirmTarget.value;
if (!confirm) {
this.hideMatchIndicator();
return;
}
if (password === confirm) {
this.showMatch();
} else {
this.showMismatch();
}
}
showMatch() {
if (this.hasMatchIndicatorTarget) {
this.matchIndicatorTarget.classList.remove("hidden", "text-red-600", "dark:text-red-400");
this.matchIndicatorTarget.classList.add("text-green-600", "dark:text-green-400");
// Update icon to checkmark
const iconElement = this.matchIndicatorTarget.querySelector("svg");
if (iconElement) {
iconElement.innerHTML = `
<g fill="currentColor">
<path d="M5.25,11.75h-.002c-.177,0-.344-.081-.454-.219L1.794,7.531c-.202-.252-.161-.62,.092-.821,.253-.202,.621-.161,.821,.092l2.544,3.18L10.411,2.549c.204-.251,.571-.29,.822-.086,.251,.204,.29,.572,.086,.822l-5.611,8.246c-.111,.138-.278,.219-.458,.219Z" fill="currentColor"></path>
</g>
`;
}
}
if (this.hasMatchTextTarget) {
this.matchTextTarget.textContent = "Passwords match";
}
// Update confirm input border
this.confirmTarget.classList.remove("input-error");
this.confirmTarget.classList.add("input-success");
}
showMismatch() {
if (this.hasMatchIndicatorTarget) {
this.matchIndicatorTarget.classList.remove("hidden", "text-green-600", "dark:text-green-400");
this.matchIndicatorTarget.classList.add("text-red-600", "dark:text-red-400");
// Update icon to X mark
const iconElement = this.matchIndicatorTarget.querySelector("svg");
if (iconElement) {
iconElement.innerHTML = `
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M10 4L4 10M4 4l6 6"></path>
</g>
`;
}
}
if (this.hasMatchTextTarget) {
this.matchTextTarget.textContent = "Passwords do not match";
}
// Update confirm input border
this.confirmTarget.classList.remove("input-success");
this.confirmTarget.classList.add("input-error");
}
hideMatchIndicator() {
if (this.hasMatchIndicatorTarget) {
this.matchIndicatorTarget.classList.add("hidden");
}
// Reset confirm input border
if (this.hasConfirmTarget) {
this.confirmTarget.classList.remove("input-success", "input-error");
}
}
// Shared icon update functionality
updateIcon(target, visible) {
if (!target) return;
if (visible) {
// Show eye-off icon when password is visible
target.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M4.8077 13.1923C3.4687 12.267 2.56488 11.0325 2.04418 10.1133C1.65178 9.42061 1.65178 8.57951 2.04418 7.88681C2.99118 6.21511 5.2055 3.50009 8.9999 3.50009C10.708 3.50009 12.0959 4.0503 13.1921 4.8078"></path> <path d="M15.327 6.9151C15.578 7.2579 15.7869 7.58889 15.9556 7.88669C16.348 8.57939 16.348 9.42049 15.9556 10.1132C15.0086 11.7849 12.7943 14.4999 8.99994 14.4999C8.59234 14.4999 8.20304 14.4686 7.83154 14.4106"></path> <path d="M7.05551 10.9446C6.55781 10.4469 6.25 9.7594 6.25 9C6.25 7.4812 7.4812 6.25 9 6.25C9.7594 6.25 10.4469 6.55779 10.9445 7.05539"></path> <path d="M2 16L16 2"></path></g></svg>
`;
} else {
// Show eye icon when password is hidden
target.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.75C10.5188 11.75 11.75 10.5188 11.75 9C11.75 7.48122 10.5188 6.25 9 6.25C7.48122 6.25 6.25 7.48122 6.25 9C6.25 10.5188 7.48122 11.75 9 11.75Z"></path> <path d="M15.9557 7.88669C16.3481 8.57939 16.3481 9.42049 15.9557 10.1132C15.0087 11.7849 12.7944 14.4999 9 14.4999C5.2056 14.4999 2.9912 11.7849 2.0443 10.1132C1.6519 9.42049 1.6519 8.57939 2.0443 7.88669C2.9913 6.21499 5.2056 3.5 9 3.5C12.7944 3.5 15.0088 6.21499 15.9557 7.88669Z"></path></g></svg>
`;
}
}
}
2. Custom CSS
Here are the custom CSS classes used for styling form components. Copy these into your CSS file if you haven't already:
/* Forms */
label,
.label {
@apply text-sm leading-6 font-medium text-neutral-700;
@apply dark:text-neutral-100;
}
.form-input[disabled] {
@apply cursor-not-allowed bg-neutral-200;
}
/* non-input elements (like the Stripe card form) can be styled to look like an input */
div.form-control {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-width: 1px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
}
.form-control {
@apply block w-full rounded-lg bg-white border-0 px-3 py-2 text-base leading-6 text-neutral-900 shadow-sm ring-1 ring-neutral-300 outline-hidden ring-inset placeholder:text-neutral-500 focus:ring-2 focus:ring-neutral-600 dark:bg-neutral-700 dark:text-white dark:placeholder-neutral-300 dark:ring-neutral-600 dark:focus:ring-neutral-500;
}
@media (min-width: 640px) {
.form-control {
font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
}
}
.form-control[disabled] {
@apply cursor-not-allowed bg-neutral-100 dark:bg-neutral-600;
}
.form-control.error {
@apply border-red-400 ring-red-300 focus:ring-red-500 dark:border-red-600 dark:ring-red-500;
}
select:not([multiple]) {
@apply w-full appearance-none rounded-lg border-0 bg-white px-3 py-2 text-base leading-6 text-neutral-900 shadow-sm ring-1 ring-neutral-300 outline-hidden ring-inset focus:ring-2 focus:ring-neutral-600;
/* Custom dropdown arrow */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23737373' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.75rem center;
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
padding-right: 2.5rem;
}
@media (min-width: 640px) {
select:not([multiple]) {
font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
}
}
/* Dark mode styling for single select */
.dark {
select:not([multiple]) {
@apply dark:bg-neutral-700 dark:text-white dark:ring-neutral-600 dark:focus:ring-neutral-500;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23A1A1AA' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
}
}
select:not([multiple])[disabled] {
@apply cursor-not-allowed bg-neutral-100 opacity-75 ring-neutral-200 dark:bg-neutral-600 dark:ring-neutral-500;
}
select[multiple] {
@apply w-full rounded-lg rounded-r-none border-0 bg-white px-3 py-2.5 text-base leading-6 text-neutral-900 shadow-sm outline-1 -outline-offset-1 outline-neutral-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-neutral-600 dark:outline-neutral-600;
min-height: 120px;
}
select[multiple] option {
@apply rounded-md;
}
@media (min-width: 640px) {
select[multiple] {
font-size: 0.875rem; /* text-sm equivalent (14px) for larger screens */
}
}
/* Dark mode styling for multiple select */
.dark {
select[multiple] {
@apply dark:bg-neutral-700 dark:text-white dark:ring-neutral-600 dark:focus:ring-neutral-500;
}
}
select[multiple][disabled] {
@apply cursor-not-allowed bg-neutral-100 opacity-75 ring-neutral-200 dark:bg-neutral-600 dark:ring-neutral-500;
}
option {
@apply bg-white px-3 py-2 text-sm text-neutral-900 dark:bg-neutral-700 dark:text-neutral-100;
}
option:checked {
@apply bg-neutral-100 dark:bg-neutral-600;
}
option:hover {
@apply bg-neutral-50 dark:bg-neutral-600;
}
.caret {
@apply pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-neutral-800;
}
[type="checkbox"] {
@apply size-4 cursor-pointer appearance-none rounded-sm border border-neutral-300 bg-white checked:border-neutral-700 checked:bg-neutral-800 indeterminate:border-neutral-700 indeterminate:bg-neutral-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:border-neutral-300 disabled:bg-neutral-100 disabled:checked:bg-neutral-100 dark:border-white/20 dark:bg-neutral-800 dark:checked:border-white/20 dark:checked:bg-neutral-900 dark:indeterminate:border-neutral-500 dark:indeterminate:bg-neutral-600 dark:focus-visible:outline-neutral-200 dark:disabled:border-neutral-500 dark:disabled:bg-neutral-400 dark:disabled:checked:bg-neutral-500 forced-colors:appearance-auto;
}
[type="checkbox"]:checked {
@apply text-white dark:text-neutral-800;
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type="checkbox"]:indeterminate {
@apply border-neutral-300 bg-neutral-800 text-white dark:border-neutral-600 dark:!bg-neutral-700 dark:text-neutral-800;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3e%3cg fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' %3e%3cline x1='10.75' y1='6' x2='1.25' y2='6'%3e%3c/line%3e%3c/g%3e%3c/svg%3e");
background-size: 75% 75%;
background-position: center;
background-repeat: no-repeat;
}
[type="checkbox"]:disabled {
@apply cursor-not-allowed border-neutral-300 bg-neutral-300 text-neutral-400 opacity-75 hover:text-neutral-300 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:text-neutral-500;
}
[type="checkbox"]:disabled:checked {
@apply border-neutral-300 dark:border-neutral-600 dark:bg-neutral-600;
}
[type="checkbox"]:indeterminate:hover {
@apply bg-neutral-800 dark:border-neutral-600 dark:!bg-neutral-700;
}
[type="radio"] {
@apply size-4 cursor-pointer appearance-none rounded-full border border-neutral-300 bg-white checked:border-neutral-700 checked:bg-neutral-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:border-neutral-300 disabled:bg-neutral-100 disabled:checked:bg-neutral-100 dark:border-white/20 dark:bg-neutral-800 dark:checked:border-white/20 dark:checked:bg-neutral-900 dark:focus-visible:outline-neutral-200 dark:disabled:border-neutral-500 dark:disabled:bg-neutral-400 dark:disabled:checked:bg-neutral-500 forced-colors:appearance-auto;
}
[type="radio"]:checked {
@apply text-white dark:text-neutral-800;
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type="radio"]:disabled {
@apply cursor-not-allowed border-neutral-300 bg-neutral-300 text-neutral-400 opacity-75 hover:text-neutral-300 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:text-neutral-500;
}
[type="radio"]:disabled:checked {
@apply border-neutral-300 dark:border-neutral-600 dark:bg-neutral-600;
}
/* Datalist styling */
input[list] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* Replace default datalist arrow in WebKit browsers */
input[list].replace-default-datalist-arrow::-webkit-calendar-picker-indicator {
display: none !important;
-webkit-appearance: none !important;
}
input[list].replace-default-datalist-arrow {
padding-right: 2.5rem;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23737373' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.75rem center;
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
}
/* Dark mode datalist arrow */
.dark {
input[list].replace-default-datalist-arrow {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23A1A1AA' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
}
}
Examples
Basic Password
A simple password input with a show/hide toggle.
<div class="w-full max-w-sm">
<label for="basic_password" class="label mb-1.5 text-sm">Password</label>
<div class="relative" data-controller="password">
<input type="password"
id="basic_password"
name="password"
class="form-control !pr-12"
placeholder="Enter your password..."
value=""
data-password-target="input">
<button type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors"
data-action="click->password#toggle"
aria-label="Toggle password visibility">
<span data-password-target="toggleIcon">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.75C10.5188 11.75 11.75 10.5188 11.75 9C11.75 7.48122 10.5188 6.25 9 6.25C7.48122 6.25 6.25 7.48122 6.25 9C6.25 10.5188 7.48122 11.75 9 11.75Z"></path> <path d="M15.9557 7.88669C16.3481 8.57939 16.3481 9.42049 15.9557 10.1132C15.0087 11.7849 12.7944 14.4999 9 14.4999C5.2056 14.4999 2.9912 11.7849 2.0443 10.1132C1.6519 9.42049 1.6519 8.57939 2.0443 7.88669C2.9913 6.21499 5.2056 3.5 9 3.5C12.7944 3.5 15.0088 6.21499 15.9557 7.88669Z"></path></g></svg>
</span>
</button>
</div>
</div>
Password with Strength Indicator
A password input that shows real-time password strength feedback.
Password strength
<div class="w-full max-w-sm">
<label for="password2" class="label">Create Password</label>
<div data-controller="password" data-password-strength-value="true" data-password-icon-size-value="size-5">
<div class="relative">
<input type="password"
id="password2"
name="password"
class="form-control !pr-12"
placeholder="Create a strong password..."
data-password-target="input"
data-action="input->password#handleInput">
<button type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors"
data-action="click->password#toggle"
aria-label="Toggle password visibility">
<span data-password-target="toggleIcon">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.75C10.5188 11.75 11.75 10.5188 11.75 9C11.75 7.48122 10.5188 6.25 9 6.25C7.48122 6.25 6.25 7.48122 6.25 9C6.25 10.5188 7.48122 11.75 9 11.75Z"></path> <path d="M15.9557 7.88669C16.3481 8.57939 16.3481 9.42049 15.9557 10.1132C15.0087 11.7849 12.7944 14.4999 9 14.4999C5.2056 14.4999 2.9912 11.7849 2.0443 10.1132C1.6519 9.42049 1.6519 8.57939 2.0443 7.88669C2.9913 6.21499 5.2056 3.5 9 3.5C12.7944 3.5 15.0088 6.21499 15.9557 7.88669Z"></path></g></svg>
</span>
</button>
</div>
<!-- Strength indicator -->
<div class="mt-2">
<div class="flex items-center justify-between mb-1">
<span class="text-xs text-neutral-500 dark:text-neutral-400">Password strength</span>
<span class="text-xs font-medium hidden" data-password-target="strengthText"></span>
</div>
<div class="h-1.5 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
<div class="h-full transition-all duration-300 ease-out rounded-full"
data-password-target="strengthBar"
style="width: 0%"></div>
</div>
</div>
</div>
</div>
Password Confirmation
Password and confirm password fields with real-time matching validation.
<div class="w-full max-w-sm" data-controller="password" data-password-confirm-value="true" data-password-icon-size-value="size-5">
<!-- Password field -->
<div class="mb-4">
<label for="password3" class="label">Password</label>
<div class="relative">
<input type="password"
id="password3"
name="password"
class="form-control !pr-12"
placeholder="Enter your password"
data-password-target="input"
data-action="input->password#handleInput">
<button type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors"
data-action="click->password#toggle"
aria-label="Toggle password visibility">
<span data-password-target="toggleIcon">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.75C10.5188 11.75 11.75 10.5188 11.75 9C11.75 7.48122 10.5188 6.25 9 6.25C7.48122 6.25 6.25 7.48122 6.25 9C6.25 10.5188 7.48122 11.75 9 11.75Z"></path> <path d="M15.9557 7.88669C16.3481 8.57939 16.3481 9.42049 15.9557 10.1132C15.0087 11.7849 12.7944 14.4999 9 14.4999C5.2056 14.4999 2.9912 11.7849 2.0443 10.1132C1.6519 9.42049 1.6519 8.57939 2.0443 7.88669C2.9913 6.21499 5.2056 3.5 9 3.5C12.7944 3.5 15.0088 6.21499 15.9557 7.88669Z"></path></g></svg>
</span>
</button>
</div>
</div>
<!-- Confirm password field -->
<div>
<label for="password3_confirm" class="label">Confirm Password</label>
<div class="relative">
<input type="password"
id="password3_confirm"
name="password_confirmation"
class="form-control !pr-12"
placeholder="Confirm your password"
data-password-target="confirm"
data-action="input->password#checkMatch">
<button type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors"
data-action="click->password#toggleConfirm"
aria-label="Toggle confirm password visibility">
<span data-password-target="confirmToggleIcon">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.75C10.5188 11.75 11.75 10.5188 11.75 9C11.75 7.48122 10.5188 6.25 9 6.25C7.48122 6.25 6.25 7.48122 6.25 9C6.25 10.5188 7.48122 11.75 9 11.75Z"></path> <path d="M15.9557 7.88669C16.3481 8.57939 16.3481 9.42049 15.9557 10.1132C15.0087 11.7849 12.7944 14.4999 9 14.4999C5.2056 14.4999 2.9912 11.7849 2.0443 10.1132C1.6519 9.42049 1.6519 8.57939 2.0443 7.88669C2.9913 6.21499 5.2056 3.5 9 3.5C12.7944 3.5 15.0088 6.21499 15.9557 7.88669Z"></path></g></svg>
</span>
</button>
</div>
<!-- Match indicator -->
<div class="mt-1.5 flex items-center gap-1.5 text-xs hidden" data-password-target="matchIndicator">
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="14" height="14" viewBox="0 0 14 14">
<g fill="currentColor">
<path d="M5.25,11.75h-.002c-.177,0-.344-.081-.454-.219L1.794,7.531c-.202-.252-.161-.62,.092-.821,.253-.202,.621-.161,.821,.092l2.544,3.18L10.411,2.549c.204-.251,.571-.29,.822-.086,.251,.204,.29,.572,.086,.822l-5.611,8.246c-.111,.138-.278,.219-.458,.219Z" fill="currentColor"></path>
</g>
</svg>
<span data-password-target="matchText"></span>
</div>
</div>
</div>
Password with Requirements Checklist
A password input with an interactive requirements checklist.
At least 8 characters
One lowercase letter
One uppercase letter
One number or special character
<div class="w-full max-w-sm" data-controller="password" data-password-requirements-value="true" data-password-icon-size-value="size-5">
<label for="password4" class="label">Create a secure password</label>
<div class="relative">
<input type="password"
id="password4"
name="password"
class="form-control !pr-12"
placeholder="Enter a secure password..."
data-password-target="input"
data-action="input->password#handleInput">
<button type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors"
data-action="click->password#toggle"
aria-label="Toggle password visibility">
<span data-password-target="toggleIcon">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.75C10.5188 11.75 11.75 10.5188 11.75 9C11.75 7.48122 10.5188 6.25 9 6.25C7.48122 6.25 6.25 7.48122 6.25 9C6.25 10.5188 7.48122 11.75 9 11.75Z"></path> <path d="M15.9557 7.88669C16.3481 8.57939 16.3481 9.42049 15.9557 10.1132C15.0087 11.7849 12.7944 14.4999 9 14.4999C5.2056 14.4999 2.9912 11.7849 2.0443 10.1132C1.6519 9.42049 1.6519 8.57939 2.0443 7.88669C2.9913 6.21499 5.2056 3.5 9 3.5C12.7944 3.5 15.0088 6.21499 15.9557 7.88669Z"></path></g></svg>
</span>
</button>
</div>
<!-- Requirements checklist -->
<div class="mt-3 space-y-1.5">
<div class="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 transition-colors" data-password-target="lengthCheck">
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 hidden" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle><polyline points="5.75 9.25 8 11.75 12.25 6.25"></polyline></g></svg>
<span>At least 8 characters</span>
</div>
<div class="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 transition-colors" data-password-target="lowercaseCheck">
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 hidden" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle><polyline points="5.75 9.25 8 11.75 12.25 6.25"></polyline></g></svg>
<span>One lowercase letter</span>
</div>
<div class="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 transition-colors" data-password-target="uppercaseCheck">
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 hidden" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle><polyline points="5.75 9.25 8 11.75 12.25 6.25"></polyline></g></svg>
<span>One uppercase letter</span>
</div>
<div class="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 transition-colors" data-password-target="numberCheck">
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 hidden" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="9" cy="9" r="7.25"></circle><polyline points="5.75 9.25 8 11.75 12.25 6.25"></polyline></g></svg>
<span>One number or special character</span>
</div>
</div>
</div>