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>

Table of contents

Get notified when new components come out