The FormValidityObserver
is an extension of the FormObserver
that automatically validates your fields and displays accessible error messages for those fields as users interact with your forms. Additionally, it exposes methods that can be used to handle manual field/form validation and manual error display/removal.
Example
<textarea name="external-textbox" form="my-form" maxlength="150" required aria-describedby="textarea-error"></textarea>
<div id="textarea-error"></div>
<form id="my-form">
<input name="textbox" type="text" minlength="10" pattern="\d+" required aria-describedby="textbox-error" />
<div id="textbox-error"></div>
<input name="checkbox" type="checkbox" required aria-describedby="checkbox-error" />
<div id="checkbox-error"></div>
<fieldset role="radiogroup" aria-describedby="radios-error">
<input name="flavor" type="radio" value="vanilla" required />
<input name="flavor" type="radio" value="strawberry" />
<input name="flavor" type="radio" value="chocolate" />
</fieldset>
<div id="radios-error"></div>
<select name="settings" required aria-describedby="combobox-error">
<option value="">Select an Option</option>
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<div id="combobox-error"></div>
<button type="submit">Submit</button>
</form>
import { FormValidityObserver } from "@form-observer/core";
// or import FormValidityObserver from "@form-observer/core/FormValidityObserver";
// Automatically validate fields that a user leaves.
// When a field that a user has left is invalid, an accessible error message will be displayed for that field.
const observer = new FormValidityObserver("focusout");
const form = document.getElementById("my-form");
observer.observe(form);
// Prevent the browser from creating error bubbles `onsubmit` (optional)
form.setAttribute("novalidate", "");
form.addEventListener("submit", handleSubmit);
function handleSubmit(event) {
event.preventDefault();
const success = observer.validateFields({ focus: true });
if (success) {
// Submit data to server
}
}
As a child of the FormObserver
, the FormValidityObserver
inherits the same benefits as its parent class. Besides its great performance, there are 2 benefits of the FormValidityObserver
that we want to call attention to in particular:
Like the Testing Library Family, the FormValidityObserver
gives you a simple-yet-powerful API that works with pure JS and with all JS frameworks out of the box. Tired of having to learn a new form validation library every time you try (or abandon) another JS framework? We've got you covered. And for those who are interested, we also provide (optional) convenience wrappers for several popular frameworks. (Don't worry, you'll still be working with the exact same API.)
The FormValidityObserver
embraces Svelte's philosophy of enhancing the features that browsers provide natively (instead of replacing them or introducing unnecessary complexity). Consequently, you won't have to reach for our API unless you want to. When you do, the code you write will be minimal, and it will feel very similar to what you'd write if you were using the browser's form validation functions.
Want to reuse the browser's native error messages and make them accessible? Simply add the correct HTML validation attributes to your field, add an error element to your markup, and the FormValidityObserver
will take care of the rest. (This is what our earlier code example did.) No extra JS is required; there's no need to complicate your code with framework-specific components or functions.
As expected for any form validation library, we also support the following features for both accessible error messages and the browser's native error bubbles:
- Synchronous and asynchronous custom validation.
- Custom error messages.
- Automatic handling of fields that are dynamically added to (or removed from) the DOM. (Say goodbye to complex calls to
register
andunregister
.) - Progressive Enhancement: Because the
FormValidityObserver
enhances browser functionality, your form validation will fallback to the browser's native behavior when JS is unavailable. - And much more...
The FormValidityObserver()
constructor creates a new observer and configures it with the options
that you pass in. Because the FormValidityObserver
only focuses on one task, it has a simple constructor with no overloads.
type: EventType | null
-
A string representing the type of event that should cause a form's field to be validated. As with the
FormObserver
, the string can be a commonly recognized event type or your own custom event type. But in the case of theFormValidityObserver
, only one event type may be specified.If you only want to validate fields manually, you can specify
null
instead of an event type. This can be useful, for instance, if you only want to validate your fieldsonsubmit
. (You would still need to callFormValidityObserver.validateFields()
manually in yoursubmit
handler in that scenario.) options
(Optional)-
The options used to configure the
FormValidityObserver
. The following properties can be provided:useEventCapturing: boolean
-
Indicates that the observer's event listener should be called during the event capturing phase instead of the event bubbling phase. Defaults to
false
. See DOM Event Flow for more details on event phases. scroller: (fieldOrRadiogroup: ValidatableField) => void
-
The function used to scroll a field (or radiogroup) that has failed validation into view. Defaults to a function that calls
scrollIntoView()
on the field (or radiogroup) that failed validation. revalidateOn: EventType
-
The type of event that will cause a form field to be revalidated. (Revalidation for a form field is enabled after it is validated at least once -- whether manually or automatically.)
This can be helpful, for example, if you want to validate your fields
oninput
, but only after the user has visited them. In that case, you could writenew FormValidityObserver("focusout", { revalidateOn: "input" })
. Similarly, you might only want to validate your fieldsoninput
after your form has been submitted. In that case, you could writenew FormValidityObserver(null, { revalidateOn: "input" })
. renderer: (errorContainer: HTMLElement, errorMessage: M | null) => void
-
The function used to render error messages (typically to the DOM) when a validation constraint's
render
option istrue
or whenFormValidityObserver.setFieldError()
is called with therender=true
option. (See theValidationErrors
type for more details about validation constraints.) When a field becomes valid (or whenFormValidityObserver.clearFieldError()
is called), this function will be called withnull
. Note that this function will only be called if the field has an accessible error container.The Message Type,
M
, is determined from your function definition. The type can be anything (e.g., astring
, anobject
, aReactElement
, or anything else).The
renderer
defaults to a function that accepts error messages of typestring
and renders them to the DOM as raw HTML. renderByDefault: R extends boolean
-
Determines the default value for every validation constraint's
render
option. (Also sets the default value forFormValidityObserver.setFieldError
'srender
option.)Note: When
renderByDefault
istrue
, therenderer
function must account for error messages of typestring
. (The defaultrenderer
function already accounts for this.) So, for example, if you wanted yourrenderer
function to supportReactElement
s, and therenderByDefault
option wastrue
, then yourrenderer
's Message Type,M
, would need to beReactElement | string
. defaultErrors: ValidationErrors<M, E, R>
-
Configures the default error messages to display for the validation constraints. (See the
configure
method for more details about error message configuration, and refer to theValidationErrors
type for more details about validation constraints.)Note: The
defaultErrors.validate
option will provide a default custom validation function for all fields in your form. This is primarily useful if you have a reusable validation function that you want to apply to all of your form's fields (for example, if you are using Zod). See Getting the Most out of thedefaultErrors
Option for examples on how to use this option effectively.
Example
// Use default `scroller` and `renderer`
const observerWithDefaults = new FormValidityObserver("input");
// Use custom `scroller` and `renderer`
const observer = new FormValidityObserver("focusout", {
// Scroll field into view WITH its label (if possible)
scroller(fieldOrRadiogroup) {
if ("labels" in fieldOrRadiogroup) {
const [label] = fieldOrRadiogroup.labels as NodeListOf<HTMLLabelElement>;
return label.scrollIntoView({ behavior: "smooth" });
}
fieldOrRadiogroup.scrollIntoView({ behavior: "smooth" });
},
// Error messages will be rendered to the DOM as raw DOM Nodes
renderer(errorContainer: HTMLElement, errorMessage: HTMLElement | null) {
if (errorMessage === null) return errorContainer.replaceChildren();
errorContainer.replaceChildren(errorMessage);
},
});
Instructs the observer to validate any fields (belonging to the provided form) that a user interacts with, and registers the observer's validation methods with the provided form. Automatic field validation will only occur when a field belonging to the form emits an event matching the type
that was specified during the observer's construction. Unlike the FormObserver
and the FormStorageObserver
, the FormValidityObserver
may only observe 1 form at a time.
Note that the name
attribute is what the observer uses to identify fields during manual form validation and error handling. Therefore, a valid name
is required for all validated fields. If a field does not have a name
, then it will not participate in form validation. Since the web specification does not allow nameless fields to participate in form submission, this is likely a requirement that your application already satisfies.
If the provided form element was not being watched before observe()
was called, the method will run any necessary setup logic and return true
. Otherwise, the method does nothing and returns false
.
Example
const observer = new FormValidityObserver("input");
const form = document.getElementById("my-form");
observer.observe(form); // Returns `true`, sets up manual validation/error-handling methods
observer.observe(form); // Returns `false`, does nothing
form.elements[0].dispatchEvent(new InputEvent("input", { bubbles: true })); // Field gets validated
Instructs the observer to stop watching a form for user interactions. The form's fields will no longer be validated when a user interacts with them, and the observer's manual validation methods will be disabled.
If the provided form element was being watched before unobserve()
was called, the method will run any necessary teardown logic and return true
. Otherwise, the method does nothing and returns false
.
Example
const observer = new FormValidityObserver("change");
const form = document.getElementById("my-form");
observer.unobserve(form); // Returns `false`, does nothing
observer.observe(form);
form.elements[0].dispatchEvent(new Event("change", { bubbles: true })); // Field gets validated
observer.unobserve(form); // Returns `true`, disables manual validation/error-handling methods
form.elements[1].dispatchEvent(new Event("change", { bubbles: true })); // Does nothing, the form is no longer being observed
Behaves the same way as unobserve
, except that 1) You do not need to provide the currently-observed form
as an argument, and 2) the method does not return a value.
Example
const observer = new FormValidityObserver("focusout");
const form = document.getElementById("my-form");
observer.observe(form);
observer.disconnect(); // `unobserve`s the currently-watched form
observer.unobserve(form); // Returns `false` because the form was already `unobserve`d
form1.elements[0].dispatchEvent(new FocusEvent("focusout", { bubbles: true })); // Does nothing
Method: FormValidityObserver.configure<E>(name: string, errorMessages:
ValidationErrors<M, E, R>
): void
Configures the error messages that will be displayed for a form field's validation constraints. If an error message is not configured for a validation constraint and there is no corresponding default configuration, then the field's validationMessage
will be used instead. For native form fields, the browser automatically supplies a default validationMessage
depending on the broken constraint.
Note: If the field is only using the configured
defaultErrors
and/or the browser's default error messages, it does not need to beconfigure
d.
The Field Element Type, E
, represents the form field being configured. This type is inferred from the errorMessages
configuration and defaults to a general ValidatableField
.
name
- The
name
of the form field. errorMessages
-
A
key
-value
pair of validation constraints (key) and their corresponding error messages (value).
Example
<form>
<label for="credit-card">Credit Card</label>
<input id="credit-card" name="credit-card" pattern="\d{16}" required aria-describedby="credit-card-error" />
<div id="credit-card-error" role="alert"></div>
<!-- Other Form Fields -->
</form>
const observer = new FormValidityObserver("focusout");
const form = document.querySelector("form");
observer.observe(form);
// `configure` a field
observer.configure("credit-card", { pattern: "Card number must be 16 digits" });
const creditCardField = document.querySelector("[name='credit-card']");
// Browser's native error message for `required` fields will be ACCESSIBLY displayed.
creditCardField.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
// Our custom error message for `pattern` will be ACCESSIBLY displayed,
// _not_ the browser's native error message for the `pattern` attribute.
creditCardField.value = "abcd";
creditCardField.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
Method: FormValidityObserver.validateFields(options?: ValidateFieldsOptions): boolean | Promise<boolean>
Validates all of the observed form's fields, returning true
if all of the validated fields pass validation and false
otherwise. The boolean
that validateFields()
returns will be wrapped in a Promise
if any of the validated fields use an asynchronous function for the validate
constraint. This promise will resolve
after all asynchronous validation functions have settled
.
validateFields()
accepts a single argument: an optional options
object. The object supports the following properties:
focus
Indicates that the first field in the DOM that fails validation should be focused and scrolled into view. Defaults to false
.
When the focus
option is false
, you can consider validateFields()
to be an enhanced version of form.checkValidity()
. When the focus
option is true
, you can consider validateFields()
to be an enhanced version of form.reportValidity()
.
enableRevalidation
Enables revalidation for all of the form's fields. Defaults to true
. (This option is only relevant if a value was provided for the observer's revalidateOn
option.)
Note that the enableRevalidation
option can prevent field revalidation from being turned on, but it cannot be used to turn off revalidation.
Method: FormValidityObserver.validateField(name: string, options?: ValidateFieldOptions): boolean | Promise<boolean>
Validates the form field with the specified name
, returning true
if the field passes validation and false
otherwise. The boolean
that validateField()
returns will be wrapped in a Promise
if the field's validate
constraint runs asynchronously. This promise will resolve
after the asynchronous validation function resolves
. Unlike the validateFields()
method, this promise will also reject
if the asynchronous validation function rejects
.
Note: Per the HTML spec, any field whose
willValidate
property isfalse
will automatically pass validation.
name
- The name of the form field being validated
options
(Optional)-
An object used to configure the
validateField()
method. The following properties are supported:focus
-
Indicates that the field should be focused and scrolled into view if it fails validation. Defaults to
false
.When the
focus
option isfalse
, you can considervalidateField()
to be an enhanced version offield.checkValidity()
. When thefocus
option istrue
, you can considervalidateField()
to be an enhanced version offield.reportValidity()
. enableRevalidation
-
Enables revalidation for the validated field. Defaults to
true
. (This option is only relevant if a value was provided for the observer'srevalidateOn
option.)Note that the
enableRevalidation
option can prevent field revalidation from being turned on, but it cannot be used to turn off revalidation.
Method: FormValidityObserver.setFieldError<E>(name: string, message:
ErrorMessage<string, E>
|
ErrorMessage<M, E>
, render?: boolean): void
Marks the form field having the specified name
as invalid (via the [aria-invalid="true"]
attribute) and applies the provided error message
to it. Typically, you shouldn't need to call this method manually; but in rare situations it might be helpful.
The Field Element Type, E
, represents the invalid form field. This type is inferred from the error message
if it is a function. Otherwise, E
defaults to a general ValidatableField
.
name
- The name of the invalid form field
message
-
The error message to apply to the invalid form field. If the field has an accessible error container, then the field's error message will be displayed there. Otherwise, the error will only be displayed when the browser displays its native error bubbles.
render
(Optional)-
Indicates that the field's error message should be rendered using the observer's
renderer
function. Defaults to the value of the observer'srenderByDefault
configuration option.When the
render
argument isfalse
, then the error message must be of typestring
. Whenrender
istrue
, then the error message must be of typeM
, whereM
is determined from the observer'srenderer
function.
Example
// By default, the `renderer` renders strings as raw HTML, and the `renderByDefault` option is `false`
const observer = new FormValidityObserver("change");
const form = document.getElementById("my-form");
observer.observe(form);
// Regular `string` Error Messages
observer.setFieldError("combobox", "There was a problem with this field...");
// Special `rendered` Error Messages
const htmlErrorString = `<ul><li>Field is missing a cool word.</li><li>Also we just don't like the value.</li></ul>`;
observer.setFieldError("textbox", htmlErrorString, true);
Marks the form field with the specified name
as valid (via the [aria-invalid="false"]
attribute) and clears its error message.
Typically, you shouldn't need to call this method manually; but in rare situations it might be helpful. For example, if you manually exclude a field from constraint validation by marking it as disabled
with JavaScript, then you can use this method to delete the obsolete error message.
name
- The
name
of the form field whose error should be cleared.
All frontend tools for forms require you to adhere to certain guidelines in order for the tool to function correctly with your application. Our tool is no different. But instead of introducing you to several tool-specific props, components, or functions to accomplish this goal, we rely on what HTML provides out of the box wherever possible. We do this for two reasons:
- If you're writing accessible, progressively enhanced forms, then you'll already be following the guidelines that we require without any additional effort.
- This approach results in developers writing less code.
The idea here is to make form validation as quick and easy as possible for those who are already following good web standards, and to encourage good web standards for those who aren't yet leaning into all of the power and accessibility features of the modern web. Here are our 3 unique requirements:
1) Form fields that participate in validation must have a name
attribute.
Justification
If your forms are progressively enhanced, you will already be satisfying this requirement. Leveraging the name
attribute enables users who lack access to JavaScript to use your forms. Moreover, the name
attribute enables many well-known form-related tools to identify fields without causing friction for developers. Given these realities, this restriction seems reasonable to us.
2) Only valid form controls may participate in form field validation.
Justification
Again, if your forms are progressively enhanced, you will already be satisfying this requirement. Using valid form controls is required to enable users who lack access to JavaScript to use your forms. It also enables form validation libraries like this one to leverage the ValidityState
interface for form validation, which is great for simplicity and performance.
If you're new to progressive enhancement, then don't worry. It's fairly easy to update your code to satisfy this requirement -- whether it's written with pure JS or with the help of a JS framework.
(Note: For complex form controls, you can create a Web Component that acts like a form control. However, Web Components are not accessible to users who don't have JavaScript; so it is still recommended to have a fallback that functions with just HTML -- though it is not required.)
3) A radio button group will only be validated if it is inside a fieldset
element with the radiogroup
role.
Justification
If your forms provide accessible radio button groups to your users, you will likely already be satisfying this requirement. (At most, you will only need to add role="radiogroup"
to a few fieldset
s.) We believe this requirement improves accessibility for end users by distinguishing radiogroup
s from general group
s. It also provides a clear way for the FormValidityObserver
to identify radio button groups without sacrificing the developer experience. (If you want deeper insight into why we made this decision, see Why Are Radio Buttons Only Validated When They're inside a fieldset
with Role radiogroup
?.)
If you're familiar with the aria-errormessage
attribute, then you'll know that it is technically "better" than the aria-describedby
attribute when it comes to conveying error messages for invalid form fields. Although it is technically superior, the aria-errormessage
attribute is also far less supported by assistive technologies (as of 2024-04-13). Because the aria-describedby
attribute is accepted by the WAI as a valid means to convey error messages for fields, and because the attribute is more widely supported by assistive technologies, the FormValidityObserver
uses this attribute for conveying error messages instead.
In the future, when aria-errormessage
has better support, the FormValidityObserver
will be updated to support it. Until then, the attribute will not be supported.
- Read our guides to find out how you can get the most out of the
FormValidityObserver
. - Using this tool in a JS framework? Check out the integration guides for ideas on how to reduce code duplication in your project.
- If you're particularly curious, you can also visit the development notes to learn how and why certain design decisions were made.
- Play with our live examples on
StackBlitz
: