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>

Table of contents

Get notified when new components come out