<validated-form>

A Web Component that brings automatic browser form validation to your markup, displaying accessible error messages with zero configuration or frameworks.

Source code & documentation

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.

Fields marked with * are required.

Requires a valid email format.

Minimum 8 characters including letter, number, and symbol.

Enter an even number between 2 and 10.

Select a date and time between and .

Choose one value.

Select at least one value.

Single choice group (radio buttons)

Exactly one option must be selected.

Checkbox group

Option A is required to proceed

Accepted formats: PDF, JPG, PNG.

Not required, but must be between 20 and 300 characters if filled.

Password field with custom validation messages for required and too-short errors.

This field is disabled. Although it is required, it will not trigger validation errors since it's not interactive.

This field is readonly. Although it is required, it will not trigger validation errors since it's not editable.

Options

Use these controls to change how the <validated-form> demo behaves.

Focus behavior
Error reporting
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?.();
});

Form submitted successfully!

The form has passed validation and was submitted without errors.

License

Licensed under the The MIT License (MIT)