npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@georapbox/validated-form

v2.0.0

Published

A Web Component that wraps native HTML form validation and surfaces the browser's validation messages as accessible inline errors.

Readme

npm version npm license

<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 using explicit, declarative markup.

API documentationDemo

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
  • Uses the authored aria-errormessage relationship and supplements it with aria-describedby for broader announcement support

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-form

Importing 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 control that should display an inline error needs an associated error element

Each control for which you want to display an inline validation message must use aria-errormessage to reference an error element by its id. The component writes the validation message into the referenced element and manages its visibility by toggling the hidden attribute.

<input type="email" required  aria-errormessage="email-error">

<div id="email-error" hidden></div>

The referenced error element's id must be unique within the document. The error element can be placed wherever it best fits the field's layout, provided that it is inside <validated-form>.

A name attribute is not required by the component for validation or error-message association. However, controls generally still need a name if their value should be included when the form is submitted.

If the required association is missing or invalid, the component fails gracefully: the control is still validated and receives aria-invalid, but no inline error is displayed.

3. Radio groups should share one error element

Radio buttons with the same name attribute represent a single native radio group. Each radio in the group should reference the same error element using aria-errormessage.

The shared name is still required for the browser to treat the radio buttons as one group.

Place the error element after the last radio button for consistent layout.

<label>
  <input
    type="radio"
    name="gender"
    value="m"
    required
    aria-errormessage="gender-error"
  >
  Male
</label>

<label>
  <input
    type="radio"
    name="gender"
    value="f"
    aria-errormessage="gender-error"
  >
  Female
</label>

<div id="gender-error" hidden></div>

When the group is invalid, the radios share the same validation message. On form submission, focus moves to the first invalid radio unless no-focus is enabled.

Notes

  • The error element can be any HTML element, such as div, span, or p.
  • aria-describedby can be used for persistent hints or supporting text. When an error element is resolved, the component preserves any existing aria-describedby references and appends the error element's ID.
  • The error element remains referenced by aria-describedby when the control becomes valid. In that state, the error element is empty and hidden.
  • When an error element has neither role nor aria-live, the component adds aria-live="polite". Existing values are preserved.
  • The component adds or removes aria-invalid based on the control's validation state.
  • The component toggles hidden on the referenced error element.
  • Controls do not need a name for validation, but they normally need one for form submission.
  • Radio buttons still need a shared name to form a native radio group.
  • If JavaScript is unavailable, native browser validation still works.
  • The component does not apply any styles, so error elements can be styled 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:

  1. Detects which validation rule failed (via the Constraint Validation API)
  2. Looks for a matching custom message attribute
  3. Falls back to the browser's localized validationMessage if 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. With report="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>
    <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."
      aria-errormessage="email-error"
    >
    <div id="email-error" hidden></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."
      aria-errormessage="password-error"
    >
    <div id="password-error" hidden></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 | Controls whether focus moves to the first invalid control when validation fails. When false (default), the first invalid control receives focus. When true, validation errors are displayed without moving focus. | | report | ✓ | 'all' \| 'first' | - | 'all' | Controls which validation messages are displayed. Use 'all' to display messages for every invalid control, or 'first' to display only the message for the first invalid control. Once validation has started, messages continue to update for the field currently being edited. |

Methods

| Name | Type | Description | Arguments | | ---- | ---- | ----------- | --------- | | define | Static | Registers the custom element with the browser's CustomElementRegistry unless it has already been defined. | tagName='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 | Checks whether all validatable controls satisfy the browser's native validation rules without displaying validation messages. | - |

[!NOTE] Instance methods are only available after the component has been defined. To ensure the component is defined, you can use the whenDefined() method of the CustomElementRegistry interface, for example: 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. Preferably, 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 install

Build for development

Watch for changes and start a development server by running the following command.

npm start

Linting

Lint the code by running the following command.

npm run lint

Testing

Run the tests by running any of the following commands.

npm test
npm run test:watch # watch mode

Build for production

Create a production build by running the following command.

npm run build

License

The MIT License (MIT)