Password Rails Components
Password input components with various features for your Ruby on Rails application.
Installation
1. Stimulus Controller Setup
Add the following controller to your project:
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/6 font-medium text-neutral-700 dark:text-neutral-100;
}
.form-input[disabled] {
@apply cursor-not-allowed bg-neutral-200;
}
/* Custom search input clear button styling */
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
height: 16px;
width: 16px;
cursor: pointer;
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='%236b7280'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
background-size: 12px 12px;
background-repeat: no-repeat;
background-position: center;
}
input[type="search"]::-webkit-search-cancel-button:hover {
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='%23374151'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
.dark input[type="search"]::-webkit-search-cancel-button {
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='%23d1d5db'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
.dark input[type="search"]::-webkit-search-cancel-button:hover {
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='%23f3f4f6'%3E%3Cpath d='m2.25,10.5c-.192,0-.384-.073-.53-.22-.293-.293-.293-.768,0-1.061L9.22,1.72c.293-.293.768-.293,1.061,0s.293.768,0,1.061l-7.5,7.5c-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3Cpath d='m9.75,10.5c-.192,0-.384-.073-.53-.22L1.72,2.78c-.293-.293-.293-.768,0-1.061s.768-.293,1.061,0l7.5,7.5c.293.293.293.768,0,1.061-.146.146-.338.22-.53.22Z' stroke-width='0'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
/* 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/6 text-neutral-900 shadow-xs 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/6 text-neutral-900 shadow-xs 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/6 text-neutral-900 shadow-xs 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-700 focus:outline-2 focus:outline-offset-2 focus:outline-neutral-600 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:outline-neutral-200 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-400 bg-neutral-500 dark:border-white/20 dark:bg-neutral-700;
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-gray-200 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 bg-gray-200 dark:border-neutral-600 dark:bg-neutral-600;
}
/* Anchor indicator for shift-click range selection */
[type="checkbox"].checkbox-anchor {
outline: 2px dashed currentColor;
outline-offset: 2px;
@apply outline-neutral-600 dark:outline-neutral-200;
}
[type="radio"] {
@apply size-4 cursor-pointer appearance-none rounded-full border border-neutral-300 bg-white checked:border-neutral-700 checked:bg-neutral-700 focus:outline-2 focus:outline-offset-2 focus:outline-neutral-600 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:outline-neutral-200 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>
Password with Strength + Requirements
A password input that combines a strength indicator with a live requirements checklist.
Password strength
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-strength-value="true" data-password-requirements-value="true" data-password-icon-size-value="size-5">
<label for="password5" class="label">Create a secure password</label>
<div class="relative">
<input type="password"
id="password5"
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>
<!-- 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>
<!-- 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>