Web Components for Password Input Enhancements

Posted on September 20, 2024
Takes about 8 minutes to read

So there I was, experimenting with HTML password inputs and Web Components. I'm not sure why the idea even came up but it quickly snowballed into a curious expedition. The result from the journey was a set of custom elements that provide extra functionality and information about the text being typed into a password input field. I shared my CodePen demo in a Mastodon post and soon after decided to push these scripts up to a GitHub repo.

Open CodePen demo

Get started

The repo includes two Web Component scripts. They operate independent of one another. I recommend reading through the repo documentation but here's a rundown of what's included.

To get started, add the scripts to a project and include them on the page.

<script type="module" src="path/to/password-rules.js"></script>
<script type="module" src="path/to/password-toggle.js"></script>

Below is an example of using both custom elements with a password input.

<label for="new-password">Password</label>
<input type="password" id="new-password" />
<div id="status" aria-live="polite"></div>

<password-toggle data-input-id="new-password" data-status-id="status">
  <button type="button">Toggle password visibility</button>
</password-toggle>

<password-rules data-input-id="new-password" data-rules=".{9}, [A-Z], .*\d">
  <ul>
    <li data-rule-index="0">Longer than 8 characters</li>
    <li data-rule-index="1">Includes an uppercase letter</li>
    <li data-rule-index="2">Includes a number</li>
  </ul>
</password-rules>

Password toggle

password-toggle expects a button element to be inside it. This button will be augmented with the ability to toggle the visibility of the input field's value.

When the toggle button is clicked, the "status" element containing the aria-live attribute will send a notification to screen readers that the password value is currently visible or hidden. For instance, when clicking for the first time, the string "Password is visible" is inserted into the container and announced by a screen reader.

We can also style the toggle button when it enters its pressed or "visible password" state. In the CodePen demo, this is how the eye icon (aye aye!) is being swapped.

button svg:last-of-type {
  display: none;
}

button[aria-pressed="true"] {
  svg:first-of-type {
    display: none;
  }
  svg:last-of-type {
    display: block;
  }
}

Targeting the [aria-pressed] attribute selector ensures that our styles stay in sync with their accessibility counterpart. It also means that we don't need to manage a semantic attribute value as well as some generic class selector like "is-active". Ben Myers shares great knowledge on this subject in Style with stateful, semantic selectors. A must-have in the bookmarks 🏆

Password rules

The password-rules element is passed a comma-separated list of regular expression strings, each related to a specific rule. We also have the option to connect any child element to a rule by passing the index of that string to a data-rule-index attribute. The placement or type of element doesn't matter as long as it's contained within the password-rules.

Here's an alternate version to drive that point home:

<password-rules data-input-id="new-password" data-rules=".{8}, [A-Z], .*\d">
  <div class="one-column" data-rule-index="0">
    Longer than 8 characters
  </div>
  <div class="two-columns">
    <span data-rule-index="2">Includes a number</span>
    <span data-rule-index="1">Includes an uppercase letter</span>
  </div>
</password-rules>

Check it off

When a rule is met that matches the data-rule-index value on an element, an is-match class gets added to the element. The demo styles use this selector to add a checkmark emoji when present.

.password-rules__checklist .is-match::before {
  content: "âś…";
}

score/total

The current password "score" and rules "total" are passed to the custom element as data attributes and CSS variables. The score value updates as rules are met. This allows us to do some fancy things like change the colors in a score meter and present the current tally. All of it done with CSS.

/** Incrementally adjust background colors */
password-rules[data-score="1"] .password-rules__meter :first-child,
password-rules[data-score="2"] .password-rules__meter :nth-child(-n + 2),
password-rules[data-score="3"] .password-rules__meter :nth-child(-n + 3),
password-rules[data-score="4"] .password-rules__meter :nth-child(-n + 4) {
  background-color: dodgerblue;
}

/** When all rules are met, swap to a new color for each meter element */
password-rules[data-score="5"] .password-rules__meter :nth-child(-n + 5) {
  background-color: mediumseagreen;
}

CSS variables are passed into a CSS counter() to render the current score and total.

.password-rules__score::before {
  counter-reset: score var(--score, 0) total var(--total, 5);
  content: counter(score) "/" counter(total);
}

I added fallback values to the CSS variables when I realized that the --total value, specifically, renders as 0 on page load and doesn't update until we begin typing in the input field. I did discover that we could skip the fallback by registering the custom property. This ensures the total is correctly reflected when the component initializes. But, to be honest, this feels unnecessary when the fallback here will suffice.

@property --total {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

If this @property stuff is unfamiliar, Stephanie Eckles has got you covered in Providing Type Definitions for CSS with @property. Another one to bookmark! I've also recently spent time with this newly supported at-rule in CSS @property and the New Style.

Progressively enhanced for the win

I believe this tells a fairly nice progressive enhancement story. Without JavaScript, the password input still works as expected. But when these scripts run, users get additional feedback and interactivity. Developers get access to extra selectors that can be useful for styling state changes. And listen, I get it–there are better ways to handle client-side form validation, but this was a fun exploration nonetheless.

Back to all blog posts