stimulus-password-strength
Importmap-friendly password strength field for Rails 8+ with Stimulus, zxcvbn, and Tailwind-friendly markup.
Use it either as a drop-in password UX upgrade or as part of a broader signup simplification rollout. The same gem can support a conservative integration that only adds the strength meter and toggle, or a conversion-focused integration that helps remove unnecessary auth friction such as password confirmation, composition rules, or extra signup fields.
Product Goal
Reduce signup abandonment by improving password UX:
- one password field with
show/hide - real-time strength meter
- requirements placed above the input so they stay visible with 1Password/LastPass overlays
- no Node.js required in the host app
- simpler signup flow while keeping security standards and sound UX practices
This gem is intentionally adaptive. Depending on the host app and the user's product goal, it can be used in two valid modes:
- UI integration only: add the password strength meter, requirements, and show/hide toggle while keeping the existing auth policy
- conversion-focused simplification: use the gem rollout to simplify the signup flow, reduce unnecessary password friction, and improve registration completion
What the Gem Includes
- Rails engine:
StimulusPasswordStrength::Engine - Stimulus controller:
password-strength - vendored
zxcvbn.js - helper:
password_strength_field - partial:
_field.html.erb - installer:
rails g stimulus_password_strength:install - default i18n files:
en,pl
Installation
Add the gem to your app:
gem "stimulus-password-strength"Then run:
bundle install
bin/rails generate stimulus_password_strength:installThe installer:
- adds importmap pins to
config/importmap.rb - registers the controller in
app/javascript/controllers/index.js - creates
config/initializers/stimulus_password_strength.rb - creates
app/lib/password_policy.rbas the single source of truth for the rules shown in the UI
Usage
<%= form_with(model: @user) do |form| %>
<%= password_strength_field form, :password,
placeholder: "Minimum 12 characters",
requirements: PasswordPolicy.requirements %>
<% end %>With custom labels:
<%= password_strength_field form, :password,
strength_labels: { weak: "Weak", fair: "Fair", good: "Good", strong: "Strong" },
toggle_labels: { show: "Show", hide: "Hide" } %>Password Policy
The installer creates a sample password_policy.rb.tt file that should become the shared source of truth for:
- backend model validation
- requirements rendered by the gem
Example host app validation:
# app/models/user.rb
validates :password, length: { minimum: PasswordPolicy::MIN_LENGTH }, allow_nil: trueExample view usage:
<%= password_strength_field form, :password,
requirements: PasswordPolicy.requirements %>min_length can use dynamic live copy from PasswordPolicy, for example:
REQUIREMENTS = [
{
rule: :min_length,
value: MIN_LENGTH,
label: "At least #{MIN_LENGTH} characters",
remaining_singular: "Type 1 more character",
remaining_plural: "Type %{count} more characters",
met_label: "#{MIN_LENGTH}+ chars"
}
].freezeThe gem does not try to infer rules from the model and does not add hidden fallbacks for requirements. The host app must pass policy explicitly from PasswordPolicy.
Configuration
config/initializers/stimulus_password_strength.rb:
StimulusPasswordStrength.configure do |config|
config.input_class = "w-full rounded-md border px-3 py-2 pr-16"
config.wrapper_style = "position: relative;"
config.toggle_style = "position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%);"
config.toggle_class = "cursor-pointer text-xs font-medium text-gray-500 hover:text-gray-700"
config.text_style = "width: 5.5rem; text-align: right; white-space: nowrap;"
config.status_row_class = ""
config.requirements_style = "min-height: 1rem;"
config.requirement_pending_style = "color: #6b7280;"
config.requirement_met_style = "color: #047857;"
config.requirement_unmet_style = "color: #b91c1c;"
config.bar_colors = {
weak: "#f87171",
fair: "#fbbf24",
good: "#22c55e",
strong: "#059669"
}
endAdding more languages is standard Rails I18n: add another locale file in config/locales.
Layout Contract
The gem now treats critical layout as component mechanics, not host-app theme:
- the password-field wrapper must stay positioned
- the show/hide toggle must stay inside the input
- the label/requirements row must remain a flex row
- the strength label uses a fixed width by default
If you override these settings, preserve the same mechanics and keep right-side padding on the input (pr-16 or equivalent) so typed text does not collide with the toggle.
Do not put layout or positional utility classes into toggle_class, status_row_class, requirements_class, label_row_class, or bar_track_class. Mechanics already live in the corresponding *_style settings. Keep classes visual only.
Post-Install Checklist
- Signup: weak password -> backend validation still works.
- Signup:
requirementsmatchPasswordPolicyand model validation. - Signup:
Show/Hidetoggle works on mobile and desktop. - Password reset: meter, requirements, and toggle behave the same way as signup.
- Password autofill: the strength meter and requirements refresh correctly.
- JS or
zxcvbnfailure: the form still allows submission. - i18n:
show/hide/weak/fair/good/stronglabels are correct for the current locale.
Migrating from Custom Password UI
If the host app already has its own password strength UI, this is a migration, not a fresh install.
Typical signs:
- a custom Stimulus controller such as
password_field_controller.js - manual strength bar markup in signup or reset views
- a custom
zxcvbnpin inconfig/importmap.rb - tests tied to old DOM details such as
data-testid="password-input"
Recommended migration order:
- Add the gem and run the installer.
- Move the real password rule into
PasswordPolicy. - Replace the custom password markup with
password_strength_fieldin:- signup / registration
- password reset / change password
- Remove the old password-specific Stimulus controller and its registration from
app/javascript/controllers/index.js. - Remove duplicate importmap pins if the app already pinned
zxcvbnor a custom password controller setup. - Update tests to target user-facing behavior, not the old internal DOM structure.
For example, prefer:
fill_in "Password", with: "SecurePass123!"instead of a selector tied to the old implementation:
find("[data-testid='password-input']").fill_in with: "SecurePass123!"After migration, there should be only one password UI implementation in the app: the gem-based one.
Agent Guidance
If you are installing this gem through an AI coding agent, use:
These files contain agent-specific project screening, rollout, and simplification guidance. They tell the agent how to inspect the host app, which questions to ask before changing auth UX, and how to choose between compatibility mode and conversion-focused simplification. Keep that logic there, not in this README.
Development
cd /Users/justi/projects_prod/stimulus-password-strength
bundle install
npm install
bundle exec rake test
npm test