@georapbox/validated-form
v1.0.0
Published
A Web Component that wraps native HTML form validation and surfaces the browser's validation messages as accessible inline errors.
Maintainers
Readme
<validated-form>
A Web Component that wraps native HTML form validation and surfaces the browser's validation messages as accessible inline errors. It does not implement validation rules or schemas — it reads the validation state through the Constraint Validation API and displays the control's validationMessage, optionally allowing per-field message overrides via data-msg-* attributes.
The component follows a progressive-enhancement approach: the browser remains responsible for validation, while JavaScript improves how errors are presented and announced. If JavaScript fails to load, the form still works using native browser validation UI.
<validated-form> is a good fit when you want to keep native browser validation and add accessible inline error presentation with minimal configuration.
Why?
Modern browsers already provide native form validation using attributes like required, pattern, minlength, maxlength, type="email", and others.
In many cases, that is enough.
However, native validation UI has practical limitations:
- Error messages appear in browser popups that cannot be styled
- Messages are not reliably associated with fields for assistive technologies
- Error bubbles may disappear before users can read them
- Accessibility behavior differs across browsers
This component builds on native validation rather than replacing it.
Using the Constraint Validation API, it:
- Reads the browser's validation state
- Reuses localized validation messages
- Renders persistent inline errors
- Associates errors with fields using proper ARIA attributes
This progressive-enhancement approach was informed by writing from Adrian Roselli and HTMHell on the accessibility limitations of default browser validation UI and the benefits of layering accessible feedback on top of native validation.
Usage
Installation
npm install --save @georapbox/validated-formImporting the component
By default, the package exports the element class without registering it. This lets the application decide when the custom element is defined.
Manual definition
import { ValidatedForm } from '@georapbox/validated-form';
// Define using the default tag name
ValidatedForm.define();Auto-defined (convenience)
If you don't need control over registration, you can import the pre-defined build which immediately registers <validated-form>.
import '@georapbox/validated-form/define';Requirements
For the component to function correctly, the markup must follow a few conventions.
1. The element must wrap a <form>
<validated-form> enhances an existing form — it does not create one.
<validated-form>
<form>
...
</form>
</validated-form>2. Each validated control must have a name attribute
The component identifies fields using their name attribute (the same identifier used during form submission).
<input type="email" name="email" required>Controls without a name still participate in validation, but they cannot be associated with an error element, so no inline error message will be shown for them.
3. Each control needs an associated error element
Every field you want to validate must have an element with a data-error-for attribute whose value matches the control's name attribute. If an error element is not provided, the component will create one automatically after the control that triggered validation. For certain controls, such as checkboxes or radio groups, this may place the message between options, so providing the error element in the markup is recommended.
The component manages the visibility of the error element by toggling the hidden attribute based on the validation state.
<input type="email" name="email" required>
<div data-error-for="email" hidden></div>4. Radio groups should share one error element
Radio buttons with the same name represent a single logical field and must share one error container. Provide a single element with data-error-for="<name>" that matches the group's name attribute.
The component treats the group as a single logical field and uses one shared error container. If the group is invalid, the error message is associated with the radio that triggered validation. On form submission, focus moves to the first invalid radio in the group.
By default (report="all"), the error element is linked to all radios in the group. When using report="first", only the first invalid radio is linked and focused.
For consistent layout and semantics, place the error element after the last radio button in the group.
<label><input type="radio" name="gender" value="m" required> Male</label>
<label><input type="radio" name="gender" value="f"> Female</label>
<div data-error-for="gender" hidden></div>If an error element is not provided, the component will create one automatically after the radio that triggered validation. For grouped controls this may place the message between options, so providing the element in the markup is recommended.
Notes
- The error element can be any element (
div,span,p, etc.) - The component will automatically set the necessary ARIA attributes
- If JavaScript is unavailable, native browser validation still works
- The component does not apply any styles — you can style the error elements as needed
Custom Validation Messages
The component supports custom validation messages per input without using setCustomValidity() internally.
Custom messages affect only what <validated-form> displays. They do not change the browser's validation rules, the control's validity state, or native validation UI.
You can provide custom messages using data-* attributes on individual form controls. For each invalid control, the component:
- Detects which validation rule failed (via the Constraint Validation API)
- Looks for a matching custom message attribute
- Falls back to the browser's localized
validationMessageif no custom message is provided
This preserves native validation behavior while allowing message customization.
Supported Attributes
The following attributes can be added to form controls:
| Attribute | ValidityState flag | Description |
| --------- | --------------- | ----------- |
| data-msg-required | valueMissing | The control is required, but no value has been provided. |
| data-msg-type | typeMismatch | The value does not match the expected input type, such as email or url. |
| data-msg-pattern | patternMismatch | The value does not match the pattern defined by the pattern attribute. |
| data-msg-too-short | tooShort | The value is shorter than the length required by the minlength attribute. |
| data-msg-too-long | tooLong | The value is longer than the length allowed by the maxlength attribute. |
| data-msg-min | rangeUnderflow | The value is less than the minimum allowed by the min attribute. |
| data-msg-max | rangeOverflow | The value is greater than the maximum allowed by the max attribute. |
| data-msg-step | stepMismatch | The value does not conform to the interval defined by the step attribute. |
| data-msg-bad-input | badInput | The browser could not convert the entered value into a valid value for that control type. |
When a control is invalid, the component checks for a matching data-msg-* attribute and uses its value as the error message.
If no matching attribute is present, the browser's default localized message is used.
[!NOTE] For radio groups with a shared error element, apply
data-msg-*attributes consistently across the group. Withreport="all", the message is resolved per radio and the last processed radio determines the final text in the shared error container.
Example markup with custom messages:
<validated-form>
<form>
<div>
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
required
data-msg-required="Email is required."
data-msg-type="Please enter a valid email address."
>
<div data-error-for="email" hidden></div>
</div>
<div>
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
required
minlength="8"
data-msg-required="Password is required."
data-msg-too-short="Password must be at least 8 characters."
>
<div data-error-for="password" hidden></div>
</div>
<button type="submit">Submit</button>
</form>
</validated-form>How Message Resolution Works
When a control is invalid:
- The component checks validation flags in priority order.
- If a matching data-msg-* attribute exists, that message is displayed.
- Otherwise, it falls back to control.validationMessage.
This ensures:
- Native validation semantics remain intact.
- Browser localization is preserved by default.
- You can override only the rules you care about.
Interaction with setCustomValidity()
Although the component does not use setCustomValidity() internally, consumers can still use it.
If a consumer sets a custom validity message:
input.setCustomValidity('This value is not allowed.');That message becomes the control's validationMessage and will be displayed when no data-msg-* override applies.
Per-rule data-msg-* attributes still take precedence for the specific rule they match.
API
Properties
| Name | Reflects | Type | Required | Default | Description |
| ---- | -------- | ---- | -------- | ------- | ----------- |
| noFocusno-focus | ✓ | boolean | - | false | Determines whether the component focuses the first invalid control when validation fails. When false (default), focus moves to the first invalid control. When true, errors are shown without changing focus. |
| report | ✓ | 'all' \| 'first' | - | 'all' | Determines how validation messages are reported when the form is validated. Use 'all' to show messages for all invalid controls, or 'first' to show only the first invalid control's message. After validation has started, live updates still reflect the field being edited. |
Methods
| Name | Type | Description | Arguments |
| ---- | ---- | ----------- | --------- |
| define | Static | Defines the custom element using the provided name. If no name is given, the default tag name is used. If the element is already registered, the method does nothing. | elementName='validated-form' |
| validate | Instance | Validates the form, updates the displayed validation feedback, and returns whether the form is valid. | - |
| resetValidation | Instance | Resets the component's validation UI by clearing displayed error messages and validation feedback. It does not reset form field values or change the browser's underlying validity state. | - |
| isValid | Instance | Returns whether the form is currently valid according to the browser's native validation rules, without showing validation messages. | - |
1 Instance methods are only available after the component has been defined. To ensure the component is defined, you can use whenDefined method of the CustomElementRegistry interface, eg customElements.whenDefined('validated-form').then(() => { /* call methods here */ });
Changelog
For API updates and breaking changes, check the CHANGELOG.
Development setup
Prerequisites
The project requires Node.js and npm to be installed on your environment. Preferrably, use nvm Node Version Manager and use the version of Node.js specified in the .nvmrc file by running nvm use.
Install dependencies
Install the project dependencies by running the following command.
npm installBuild for development
Watch for changes and start a development server by running the following command.
npm startLinting
Lint the code by running the following command.
npm run lintTesting
Run the tests by running any of the following commands.
npm test
npm run test:watch # watch modeBuild for production
Create a production build by running the following command.
npm run build