loading . . . ✍🏻 Visual Validation Feedback for Form Fields Password requirements, username rules, input format constraints: forms often have multiple validation requirements, but users frequently do not find out whether they are meeting them until they hit submit. The `form-validation-list` web component changes that by providing real-time visual feedback as users type, showing exactly which requirements are met and which are not.
This is a modern replacement for my old jQuery Easy Validation Rules plugin, reimagined as a web component with native form validation integration.
* * *
To get started, associate the component with an `input` element using the `for` attribute and define your validation rules:
<form><labelfor=“username”>Username:</label><inputtype=“text”id=“username”name=“username”required/><form-validation-listfor=“username”><ul><lidata-pattern=“[A-Z]+”>At least one capital letter</li><lidata-pattern=“[a-z]+”>At least one lowercase letter</li><lidata-pattern=“[\d]+”>At least one number</li></ul></form-validation-list><buttontype=“submit”>Submit</button></form>
As users type, each rule is checked against its regular expression pattern. Matched rules get a checkmark (✓), unmatched rules get an X (✗). When all rules match, the field is valid and the form can be submitted. No guessing required.
## # What’s happening under the hood?
The component:
1. Associates with an input via the `for` attribute (just like a `label` element)
2. Finds all elements with `data-pattern` attributes
3. Tests the input value against each pattern
4. Adds `validation-matched` or `validation-unmatched` classes accordingly
5. Shows visual indicators (✓ or ✗)
6. Uses `setCustomValidity()` to integrate with native form validation
7. Prevents form submission until all rules match
The cascade animation, controlled by `each-delay`, creates a pleasant visual effect as rules are checked sequentially. It is a small touch, but a nice one.
## # Whose rules? Your rules.
Define rules using regular expression patterns in the `data-pattern` attribute:
<form-validation-listfor=“password”><ul><!-- Length requirements --><lidata-pattern=“.{8,}”>At least 8 characters</li><lidata-pattern=“.{8,32}”>Between 8 and 32 characters</li><!-- Character type requirements --><lidata-pattern=“[A-Z]+”>At least one uppercase letter</li><lidata-pattern=“[a-z]+”>At least one lowercase letter</li><lidata-pattern=“[\d]+”>At least one number</li><lidata-pattern=“[!@#$%^&_]+ “>At least one special character</li><!-- Format requirements --><lidata-pattern=”.+@.+..+“>Valid email format</li><lidata-pattern=”1+$“>Only letters and numbers</li></ul></form-validation-list>
Each pattern is a standard JavaScript regular expression. The component tests the `input` value against all patterns on every input event (i.e., as users type).
## # Input event too noisy? No worries.
By default, validation runs on the `input` event, but you can change it by setting the `trigger-event` attribute to ”blur”:
<form-validation-listfor=”email“trigger-event=”blur“><ul><lidata-pattern=”.+@.+“>Contains @ symbol</li><lidata-pattern=”.+@.+..+“>Valid email format</li></ul></form-validation-list>
With this atttribute in place, the validation runs only when the field loses focus. This is useful when you don’t want to start validating while users are still in the middle of typing.
## # Wanna adjust the cascade delay? Go for it.
Use the `each-delay` attribute to control the delay between checking each rule. The default speed is 150ms, but you can tune it to any number of milliseconds:
<form-validation-listfor=”password“each-delay=”100“><!-- rules --></form-validation-list>
Set it to “0” to remove the cascade effect entirely and check all rules simultaneously.
## # Need full design control? You got it.
If you want full design control over the component, you can absolutely have it. The whole component operated in light DOM, so your styles will piecrce through. And you can customize `class` names for integration with CSS frameworks using a set of attributes on the `form-validation-list` element. The `field-valid-class` and `field-invalid-class` attributes control the class names applied to the `input` field itself, while the `rule-matched-class` and `rule-unmatched-class` attributes control the `class` names applied to each rule item.
Here’s a complete example:
<style>.is-valid{border-color: green;}.is-invalid{border-color: red;}.rule-pass{color: green;}.rule-fail{color: red;}</style><form-validation-listfor=”username“field-valid-class=”is-valid“field-invalid-class=”is-invalid“rule-matched-class=”rule-pass“rule-unmatched-class=”rule-fail“><ul><lidata-pattern=”.{5,}“>At least 5 characters</li><lidata-pattern=”[!@#]+“>Special char (!@#)</li></ul></form-validation-list>
This approach lets you use `class` names that match your existing CSS architecture, rather than making one small component dictate terms to the rest of your styles.
You can also control the visual indicators using CSS custom properties:
* `–validation-icon-matched` - Content for matched state (default: “✓”)
* `–validation-icon-unmatched` - Content for unmatched state (default: “✗”)
* `–validation-icon-size` - Size of icons (default: 1em)
* `–validation-matched-color` - Color for matched rules (default: green)
* `–validation-unmatched-color` - Color for unmatched rules (default: red)
Here’s an example of that:
form-validation-list{–validation-icon-matched:”✅“;–validation-icon-unmatched:”❌“;–validation-icon-size: 1.2em;–validation-matched-color: #28a745;–validation-unmatched-color: #dc3545;}
## # Bit of a control freak? There’s an API.
If you really want to get into the weeds, you can also listen for validation changes in your JavaScript code:
const validationList = document.querySelector(“form-validation-list”);validationList.addEventListener(“form-validation-list:validated”,(event)=>{const{ isValid, matchedRules, totalRules, field }= event.detail;console.log(</span><span class="token string">Matched </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>matchedRules<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> of </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>totalRules<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> rules</span><span class="token template-punctuation string">);console.log(</span><span class="token string">Field is </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>isValid <span class="token operator">?</span> <span class="token string">"valid"</span> <span class="token operator">:</span> <span class="token string">"invalid"</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">);});
The event fires after validation completes and gives you the current state. Nice and tidy.
You can also manually trigger validation and check the element’s current state at any time:
const validationList = document.querySelector(“form-validation-list”);// Trigger validationconst isValid = validationList.validate();console.log(“Is valid:”, isValid);// Check current stateconsole.log(“Current state:”, validationList.isValid);
## # Global site? _Relaje!_
If you need the component to work in different languages, that’s totally doable. You can customize the validation message for different languages using the `validation-message` attribute. It supports placeholders `{matched}` and `{total}` which are replaced with the current count of matched rules and total rules:
<!-- Spanish --><form-validation-listfor=”contrasena“validation-message=”Por favor, cumple todos los requisitos ({matched} de {total})“><ul><lidata-pattern=”[A-Z]+“>Al menos una letra mayúscula</li><lidata-pattern=”[a-z]+“>Al menos una letra minúscula</li><lidata-pattern=”[\d]+“>Al menos un número</li></ul></form-validation-list><!-- French --><form-validation-listfor=”mot-de-passe“validation-message=”Veuillez satisfaire à toutes les exigences ({matched} sur {total})“><ul><lidata-pattern=”[A-Z]+“>Au moins une lettre majuscule</li><lidata-pattern=”[a-z]+“>Au moins une lettre minuscule</li><lidata-pattern=”[\d]+“>Au moins un chiffre</li></ul></form-validation-list>
## # Is it a progressive enhancement? Heck yeah!
The component uses light DOM, so if JavaScript fails, users still see the validation requirements as a standard list. They can read what is expected even without the visual feedback. Your server-side validation still does the important enforcement work regardless… right? _Right?_
## # Is it screen reader accessible? Yep.
The component is built with accessibility in mind:
* **Proper description support** : The validation list is automatically associated with the `input` via `aria-describedby`, but if the field already has `aria-describedby`, the original value is preserved.
* **Validation changes are announced** : Each rule has `aria-live="polite"`, so when it updates, screen readers announce the change.
If you have suggestions for other ways to improve the accessibility of this component, please open an issue on GitHub.
## # Does it integrate with the browser’s validation engine? Naturally.
The component uses `setCustomValidity()` to participate in native form validation:
* When all rules match, custom validity is cleared
* When rules don’t match, a custom validity message is set
* Form submission is prevented until all rules pass
* Works with `:valid` and `:invalid` CSS pseudo-classes
* Compatible with the Constraint Validation API
const form = document.querySelector(“form”);const field = document.getElementById(“username”);form.addEventListener(“submit”,(e)=>{if(!form.checkValidity()){e.preventDefault();console.log(“Validation failed:”, field.validationMessage);}});
## # Here’s a real-world example
Here’s a complete password validation setup:
<form><labelfor=”password“>Password:</label><inputtype=”password“id=”password“name=”password“required/><form-validation-listfor=”password“><ul><lidata-pattern=”.{8,}“>At least 8 characters</li><lidata-pattern=”[A-Z]+“>At least one uppercase letter</li><lidata-pattern=”[a-z]+“>At least one lowercase letter</li><lidata-pattern=”[\d]+“>At least one number</li><lidata-pattern=”[!@#$%^&_]+”>At least one special character (!@#$%^&*)</li></ul></form-validation-list><buttontype=“submit”>Submit</button></form>
Users see exactly which requirements they have met and which they still need to satisfy. That tends to be a lot kinder than springing the whole list on them after submit.
## # Play with it
Check out the demo with various examples:
## # Grab it
View the project on GitHub.
Install via `npm`:
npminstall @aarongustafson/form-validation-list
Import and use:
import“@aarongustafson/form-validation-list”;
Happy validating!
* * *
#### Footnotes
1. a-zA-Z0-9 ↩︎
https://www.aaron-gustafson.com/notebook/visual-validation-feedback-for-form-fields/