<validated-form>
A Web Component that brings automatic browser form validation to your markup, displaying accessible error messages with zero configuration or frameworks.
Demo
This demo showcases the capabilities of the <validated-form> component, demonstrating various form fields and validation scenarios. Try submitting the form with different inputs to see how it handles validation and error messages.
Options
Use these controls to change how the <validated-form> demo behaves.
Code sample
HTML
The HTML markup for the demo form includes a variety of form controls, each with different validation requirements.
The form is wrapped in the <validated-form> component, which handles the validation logic and
error message display. Each form control has an associated error container defined by the data-error-for
attribute, where validation messages will be displayed when errors occur. Additionally, descriptive text is provided
for each field to guide users on the expected input and validation rules.
<validated-form>
<form id="demo-form" autocomplete="off">
<p>Fields marked with * are required.</p>
<!-- Text / Email -->
<div>
<label for="text-input">Text input (email type) <span aria-hidden="true">*</span></label>
<p id="text-input-hint">Requires a valid email format.</p>
<input
type="email"
id="text-input"
name="text-input"
required
aria-describedby="text-input-hint"
>
<div data-error-for="text-input" hidden></div>
</div>
<!-- Pattern -->
<div>
<label for="pattern-input">Pattern-validated input <span aria-hidden="true">*</span></label>
<p id="pattern-input-hint">Minimum 8 characters including letter, number, and symbol.</p>
<input
type="text"
id="pattern-input"
name="pattern-input"
pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z\d]).{8,}$"
required
aria-describedby="pattern-input-hint"
>
<div data-error-for="pattern-input" hidden></div>
</div>
<!-- Number -->
<div>
<label for="number-input">Number input <span aria-hidden="true">*</span></label>
<p id="number-input-hint">Enter an even number between 2 and 10.</p>
<input
type="number"
id="number-input"
name="number-input"
required
min="2"
max="10"
step="2"
aria-describedby="number-input-hint"
>
<div data-error-for="number-input" hidden></div>
</div>
<!-- Datetime-local -->
<div class="form-group">
<label for="datetime-input">Datetime input <span class="required-hint" aria-hidden="true">*</span></label>
<p class="help-text" id="datetime-input-hint">
Select a date and time between
<time datetime="2026-03-28T09:00">28 March 2026, 09:00</time>
and
<time datetime="2026-03-28T18:00">28 March 2026, 18:00</time>.
</p>
<input
class="form-control"
type="datetime-local"
id="datetime-input"
name="datetime-input"
required
min="2026-03-28T09:00"
max="2026-03-28T18:00"
aria-describedby="datetime-input-hint"
>
<div data-error-for="datetime-input" hidden></div>
</div>
<!-- Select -->
<div>
<label for="select-single">Single select <span aria-hidden="true">*</span></label>
<p id="select-single-hint">Choose one value.</p>
<select
id="select-single"
name="select-single"
required
aria-describedby="select-single-hint"
>
<option value="">Select an option</option>
<option value="option-a">Option A</option>
<option value="option-b">Option B</option>
<option value="option-c">Option C</option>
</select>
<div data-error-for="select-single" hidden></div>
</div>
<!-- Multi Select -->
<div>
<label for="select-multiple">Multiple select <span aria-hidden="true">*</span></label>
<p id="select-multiple-hint">Select at least one value.</p>
<select
id="select-multiple"
name="select-multiple"
multiple
required
aria-describedby="select-multiple-hint"
>
<option value="option-a">Option A</option>
<option value="option-b">Option B</option>
<option value="option-c">Option C</option>
<option value="option-d">Option D</option>
</select>
<div data-error-for="select-multiple" hidden></div>
</div>
<!-- Radio -->
<fieldset aria-describedby="radio-hint">
<legend>Single choice group (radio buttons) <span aria-hidden="true">*</span></legend>
<p id="radio-hint">Exactly one option must be selected.</p>
<label class="inline-option">
<input
type="radio"
name="radio-group"
value="option-a"
required
>
Option A
</label>
<label class="inline-option">
<input
type="radio"
name="radio-group"
value="option-b"
>
Option B
</label>
<label class="inline-option">
<input
type="radio"
name="radio-group"
value="option-c"
>
Option C
</label>
<div data-error-for="radio-group" hidden></div>
</fieldset>
<!-- Checkbox -->
<fieldset>
<legend>Checkbox group</legend>
<p id="checkbox-hint">Option A is required to proceed</p>
<label>
<input
type="checkbox"
name="checkbox-input-required"
value="option-a"
required
aria-describedby="checkbox-hint"
>
Option A
<span aria-hidden="true">*</span>
</label>
<div data-error-for="checkbox-input-required" hidden></div>
<label>
<input
type="checkbox"
name="checkbox-input-optional"
value="option-b"
>
Option B
</label>
</fieldset>
<!-- File -->
<div>
<label for="file-input">File upload <span aria-hidden="true">*</span></label>
<p id="file-input-hint">Accepted formats: PDF, JPG, PNG.</p>
<input
type="file"
id="file-input"
name="file-input"
required
accept=".pdf,.jpg,.jpeg,.png"
aria-describedby="file-input-hint"
>
<div data-error-for="file-input" hidden></div>
</div>
<!-- Textarea -->
<div>
<label for="textarea-input">Textarea</label>
<p id="textarea-hint">Not required, but must be between 20 and 300 characters if filled.</p>
<textarea
id="textarea-input"
name="textarea-input"
rows="4"
minlength="20"
maxlength="300"
aria-describedby="textarea-hint"
></textarea>
<div data-error-for="textarea-input" hidden></div>
</div>
<!-- Custom validation errors -->
<div>
<label for="custom-errors-input">Input with custom error messages <span aria-hidden="true">*</span></label>
<p id="custom-errors-input-hint">Password field with custom validation messages for required and too-short errors.</p>
<input
type="password"
id="custom-errors-input"
name="custom-errors-input"
required
minlength="8"
autocomplete="off"
aria-describedby="custom-errors-input-hint"
data-msg-required="Password is required."
data-msg-too-short="Password must be at least 8 characters."
>
<div data-error-for="custom-errors-input" hidden></div>
</div>
<!-- Disabled -->
<div>
<label for="disabled-input">Disabled control</label>
<p id="disabled-input-hint">This field is disabled. Although it is required, it will not trigger validation errors since it's not interactive.</p>
<input
type="text"
id="disabled-input"
name="disabled-input"
disabled
required
aria-describedby="disabled-input-hint"
>
<div data-error-for="disabled-input" hidden></div>
</div>
<!-- Readonly -->
<div>
<label for="readonly-input">Readonly control</label>
<p id="readonly-input-hint">This field is readonly. Although it is required, it will not trigger validation errors since it's not editable.</p>
<input
type="text"
id="readonly-input"
name="readonly-input"
readonly
required
aria-describedby="readonly-input-hint"
>
<div data-error-for="readonly-input" hidden></div>
</div>
<!-- Hidden -->
<input type="hidden" name="hidden-input" value="hidden-value">
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</form>
</validated-form>
CSS
The component does not include any default styles for error messages, allowing you to style them as needed.
The following CSS snippet provides a basic example of how to style the error messages that are displayed in
the elements with the data-error-for attribute. The styling of the form is omitted for brevity,
but you can apply your own styles to the form controls and layout as desired.
[data-error-for] {
color: light-dark(#ae1a28, #ff7d89);
font-size: 0.875rem;
word-wrap: break-word;
}
JavaScript
The JavaScript code listens for the form's submit event, prevents the default submission behavior,
and checks if the form is valid using the isValid() method provided by the <validated-form> component.
If the form is valid, it collects the form data into a FormData object and logs it to the console.
You can replace the console log with any custom submission logic, such as sending the data to a server or displaying a success message.
const validatedForm = document.querySelector('validated-form');
const form = document.querySelector('form');
form.addEventListener('submit', evt => {
evt.preventDefault();
if (!validatedForm.isValid?.()) {
return;
}
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
console.log('Form data:', data);
});
form.addEventListener('reset', evt => {
validatedForm.resetValidation?.();
});
License
Licensed under the The MIT License (MIT)