A web component that provides visual validation feedback for form fields using a list of validation rules. As users type, each rule is checked against a regular expression pattern and displays a checkmark (✓) or X (✗) to indicate whether the requirement is met. The component integrates with the browser's built-in form validation and is fully accessible.
setCustomValidity.d.ts definitions so editors and TypeScript builds understand FormValidationListElement and defineFormValidationList.for, trigger-event, input-throttle, each-delay, the icon/state text attributes, and every class-related attribute reflect between properties and attributes, keeping reactive frameworks and declarative templates in sync with DOM state._upgradeProperty helper captures properties that were assigned before the element upgraded, ensuring early property sets (common in SSR or JSX) are not lost.HTMLElementTagNameMap is augmented so document.querySelector('form-validation-list') is strongly typed in TS/JSX projects.Additional examples:
npm install @aarongustafson/form-validation-list
Import the class and define the custom element with your preferred tag name:
import { FormValidationListElement } from '@aarongustafson/form-validation-list';
customElements.define('my-custom-name', FormValidationListElement);
Use the guarded definition helper to register the element when customElements is available:
import '@aarongustafson/form-validation-list/define.js';
If you prefer to control when the element is registered, call the helper directly:
import { defineFormValidationList } from '@aarongustafson/form-validation-list/define.js';
defineFormValidationList();
You can also include the guarded script from HTML:
<script src="./node_modules/@aarongustafson/form-validation-list/define.js" type="module"></script>
<form>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<form-validation-list for="username">
<ul>
<li data-pattern="[A-Z]+">At least one capital letter</li>
<li data-pattern="[a-z]+">At least one lowercase letter</li>
<li data-pattern="[\d]+">At least one number</li>
</ul>
</form-validation-list>
<button type="submit">Submit</button>
</form>
for attribute to the <form-validation-list> element with the ID of the input field you want to validatedata-pattern attribute containing a regular expressionvalidation-matched class and show a checkmark; unmatched rules get validation-unmatched and show an XsetCustomValidity() to participate in the browser's form validationtrigger-event="input" (default)input-throttle)aria-describedby to avoid duplicate announcementsannouncement templatearia-describedby is restored after a brief delay so returning to the field reads the full criteria state againtrigger-event="blur"input-throttle attribute is ignored| Attribute | Type | Default | Description |
|---|---|---|---|
for |
string |
"" |
Required. The ID of the input field to validate |
trigger-event |
string |
"input" |
When to trigger validation: "input" (with throttle) or "blur" (immediate, no announcements while typing) |
input-throttle |
number |
250 |
Delay in milliseconds before running validation for input events. Only used when trigger-event="input". Set to 0 to disable throttling. |
each-delay |
number |
150 |
Delay in milliseconds between each rule being classified (creates cascade effect) |
field-invalid-class |
string |
"validation-invalid" |
Class to apply to the field when invalid |
field-valid-class |
string |
"validation-valid" |
Class to apply to the field when valid |
rule-unmatched-class |
string |
"validation-unmatched" |
Class to apply to unmatched rules |
rule-matched-class |
string |
"validation-matched" |
Class to apply to matched rules |
rule-matched-icon |
string |
"✓" |
Override the matched icon glyph for this instance |
rule-unmatched-icon |
string |
"✗" |
Override the unmatched icon glyph for this instance |
rule-matched-alt |
string |
"Criteria met" |
Localized hidden state text inserted for matched rules once the field has a value |
rule-unmatched-alt |
string |
"Criteria not met" |
Localized hidden state text inserted for unmatched rules once the field has a value |
announcement |
string |
"Criteria met: {matched} of {total}" |
Live region summary while typing. Use {matched} and {total} placeholders. |
validation-message |
string |
"Please match all validation requirements ({matched} of {total})" |
Custom validation message template. Use {matched} and {total} as placeholders for internationalization |
<form-validation-list
for="password"
trigger-event="input"
input-throttle="0"
rule-matched-alt="Requirement met"
rule-unmatched-alt="Requirement not met"
announcement="{matched} of {total} requirements met"
each-delay="100"
field-valid-class="is-valid"
field-invalid-class="is-invalid">
<ul>
<li data-pattern=".{8,}">At least 8 characters</li>
<li data-pattern="[A-Z]+">At least one uppercase letter</li>
<li data-pattern="[\d]+">At least one number</li>
</ul>
</form-validation-list>
The component automatically enhances your markup with additional elements and IDs:
Your HTML:
<form-validation-list for="password">
<ul>
<li data-pattern=".{8,}">At least 8 characters</li>
</ul>
</form-validation-list>
Generated DOM (simplified):
<form-validation-list for="password">
<ul id="form-validation-list-abc123xyz">
<li data-pattern=".{8,}" class="validation-unmatched" aria-atomic="true">
<span class="form-validation-list-rule-icon" aria-hidden="true"></span>
<span class="form-validation-list-rule-state">Criteria not met </span>
At least 8 characters
</li>
</ul>
<span aria-live="polite" aria-atomic="true" class="form-validation-list-live-region"></span>
</form-validation-list>
What gets added:
<ul> or <ol> element and linked to the input via aria-describedby<span class="form-validation-list-rule-icon"> is prepended to each rule showing ✓ or ✗aria-hidden="true" prevents screen readers from announcing the iconrule-matched-icon and rule-unmatched-icon attributes or the --rule-matched-icon and --rule-unmatched-icon CSS properties<span class="form-validation-list-rule-state"> is inserted after the icon with visually hidden state textclip: rect(0 0 0 0) and position: absoluterule-matched-alt and rule-unmatched-alt attributes).has-value class on the component)<span class="form-validation-list-live-region"> with aria-live="polite" is added as a sibling to your listtrigger-event="input"Rules are defined using the data-pattern attribute on any element inside the <form-validation-list>. The value should be a valid regular expression pattern.
<form-validation-list for="password">
<ul>
<!-- Length requirements -->
<li data-pattern=".{8,}">At least 8 characters</li>
<li data-pattern=".{8,32}">Between 8 and 32 characters</li>
<!-- Character type requirements -->
<li data-pattern="[A-Z]+">At least one uppercase letter</li>
<li data-pattern="[a-z]+">At least one lowercase letter</li>
<li data-pattern="[\d]+">At least one number</li>
<li data-pattern="[!@#$%^&*]+">At least one special character</li>
<!-- Format requirements -->
<li data-pattern=".+@.+\..+">Valid email format</li>
<li data-pattern="^[a-zA-Z0-9]+$">Only letters and numbers</li>
</ul>
</form-validation-list>
The component fires a custom event when validation completes:
| Event | Description | Detail |
|---|---|---|
form-validation-list:validated |
Fired after validation completes | { isValid, matchedRules, totalRules, field } |
const validationList = document.querySelector('form-validation-list');
validationList.addEventListener('form-validation-list:validated', (event) => {
const { isValid, matchedRules, totalRules, field } = event.detail;
console.log(`Matched ${matchedRules} of ${totalRules} rules`);
console.log(`Field is ${isValid ? 'valid' : 'invalid'}`);
});
validate()Manually trigger validation and return the current validation state.
const validationList = document.querySelector('form-validation-list');
const isValid = validationList.validate();
console.log('Is valid:', isValid);
isValid (getter)Returns the current validation state as a boolean.
const validationList = document.querySelector('form-validation-list');
console.log('Current state:', validationList.isValid);
| Property | Default | Description |
|---|---|---|
--rule-matched-icon |
"✓" |
Content for the matched state icon. Legacy alias: --validation-icon-matched |
--rule-unmatched-icon |
"✗" |
Content for the unmatched state icon. Legacy alias: --validation-icon-unmatched |
--rule-icon-size |
1em |
Size of the validation icons. Legacy alias: --validation-icon-size |
--rule-matched-color |
green |
Color for matched rules. Legacy alias: --validation-matched-color |
--rule-unmatched-color |
red |
Color for unmatched rules. Legacy alias: --validation-unmatched-color |
form-validation-list {
--rule-matched-icon: "✅";
--rule-unmatched-icon: "❌";
--rule-icon-size: 1.2em;
--rule-matched-color: #28a745;
--rule-unmatched-color: #dc3545;
}
form-validation-list ul {
list-style: none;
padding-left: 0;
}
form-validation-list li {
padding: 0.5rem 0;
transition: all 0.3s ease;
}
The component exposes separate templates for browser validation messages, live typing announcements, and per-rule hidden state text:
validation-message supports {matched} and {total} placeholders for native form validation.announcement supports {matched} and {total} placeholders for the live region summary while typing.rule-matched-alt and rule-unmatched-alt provide localized rule state text when the field is revisited.<!-- Spanish -->
<form-validation-list
for="contrasena"
announcement="{matched} de {total} criterios cumplidos"
rule-matched-alt="Criterio cumplido"
rule-unmatched-alt="Criterio pendiente"
validation-message="Por favor, cumple todos los requisitos ({matched} de {total})">
<ul>
<li data-pattern="[A-Z]+">Al menos una letra mayúscula</li>
<li data-pattern="[a-z]+">Al menos una letra minúscula</li>
<li data-pattern="[\d]+">Al menos un número</li>
</ul>
</form-validation-list>
<!-- French -->
<form-validation-list
for="mot-de-passe"
announcement="{matched} critères satisfaits sur {total}"
rule-matched-alt="Critère satisfait"
rule-unmatched-alt="Critère non satisfait"
validation-message="Veuillez satisfaire à toutes les exigences ({matched} sur {total})">
<ul>
<li data-pattern="[A-Z]+">Au moins une lettre majuscule</li>
<li data-pattern="[a-z]+">Au moins une lettre minuscule</li>
<li data-pattern="[\d]+">Au moins un chiffre</li>
</ul>
</form-validation-list>
The message template uses {matched} and {total} as placeholders that will be replaced with the current count of matched rules and total rules.
The component is built with accessibility in mind:
ul/li markuptrigger-event="input", keeping screen reader chatter minimalaria-describedby. With trigger-event="input", the list is temporarily suspended while typing to avoid duplicate announcements, then restored on blur. With trigger-event="blur", the list remains visible at all times.aria-describedby attribute, the component preserves existing values and appends its own IDtrigger-event="input" (recommended for complex validation)When a user focuses on the input field, screen readers announce the field label followed by the validation requirements. While the user types:
announcement template (e.g., "Criteria met: 2 of 3")On blur (when leaving the field):
aria-describedby is restored after a brief delaytrigger-event="blur" (for simple or expensive validation)aria-describedby at all timesThe component participates in the browser's native form validation using the setCustomValidity() API:
setCustomValidity('')):valid and :invalid CSS pseudo-classesconst form = document.querySelector('form');
const field = document.getElementById('username');
form.addEventListener('submit', (e) => {
if (!form.checkValidity()) {
e.preventDefault();
console.log('Validation failed:', field.validationMessage);
}
});
This component uses modern web standards:
For older browsers, you may need polyfills.
If you're migrating from the jQuery version:
| jQuery Version | Web Component Version |
|---|---|
data-validation-rules="id" |
for="id" |
data-validation-rules-rule="pattern" |
data-pattern="pattern" |
trigger_event: "keyup" |
trigger-event="input" |
each_delay: 150 |
each-delay="150" |
field_invalid_class: "class" |
field-invalid-class="class" |
field_valid_class: "class" |
field-valid-class="class" |
rule_unmatched_class: "class" |
rule-unmatched-class="class" |
rule_matched_class: "class" |
rule-matched-class="class" |
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Lint code
npm run lint
# Format code
npm run format
# View demo
open demo/index.html
MIT © Aaron Gustafson