§9 · Declarative HTML Components Level 2 · proposed direction

Validation

A declared type is a constraint. HTML Forms extends constraint validation to any element with a value; this chapter defines how a component's prop types and a <data> source's schema produce validity in that model, so schema validation stops being a library and becomes native.

Proposed direction · Level 2

At a glance

The same validation that already works on a form <input> works on any typed value, a component prop or a data response, with no forms library:

<!-- a form control validates against its constraints, exactly as today -->
<input bind:value="email" type="email" required>

<!-- a component definition declares the bounds on its own prop -->
<template component="x-age-field">
  <defs><prop name="age" type="integer" min="0" max="120">Age.</prop></defs>
  <div from:data-age="age"></div>
</template>
<x-age-field from:age="draft.age"></x-age-field>

<!-- structured data validates against a schema; failures carry a path -->
<data name="profile" src="/api/me" schema="/schemas/profile.json">

Built on HTML Forms

This chapter does not define a validity model of its own. HTML Forms Level 1 defines one for any element with a value:

  • el.validity with named flags such as valueMissing, rangeOverflow, and tooShort, plus a list of errors with messages and optional paths;
  • el.validate() and el.setValidity(errors);
  • the invalid event, validationMessage, and :valid, :invalid, and :user-invalid.

That model leaves open where an element's constraints come from. A native <input> takes them from its attributes. A component takes them from its declared types, which is what this chapter defines.

Included in the reference implementation

The html-next implementation repository includes a pure validate(value, constraint) helper and a generalized validity surface. Authored prop constraints use the same named failures as HTML's ValidityState.2 Authors use the proposed surface; the implementation handles browser compatibility.

The type is the constraint

A prop's declared type and its constraint attributes (see Types) compile to validity. Each kind of failure produces one of HTML Forms' reasons:

DeclaredA value fails whenReason
requiredit is emptyvalueMissing
the type (number, color, etc.)it is the wrong kind of valuetypeMismatch
valuesit is outside the permitted settypeMismatch
min / maxit is below or above the boundrangeUnderflow / rangeOverflow
minlength / maxlengthit is too short or too longtooShort / tooLong
patternit does not matchpatternMismatch
the type's parserit cannot be parsed as the typebadInput or typeMismatch
a JSON Schema rulea rule with no reason above failsthe schema keyword, with the failing value's path

Invalid declarations and supplied values have different effects:

  • Definition: A default must satisfy its type and constraints when the definition is compiled. Build tools reject a malformed constraint; the live parser warns and ignores that constraint.
  • Value outside a constraint: A supplied number above max is still a number. It becomes the prop's current value and sets rangeOverflow. Ordinary constraint failures do not produce console warnings, whether the value is literal or comes from a reactive binding.
  • Value that cannot be parsed: If a number prop is supplied as amount="oops", that string remains its inputValue and reports badInput or typeMismatch. It does not become the prop's accepted value. The accepted value stays at the last successfully parsed value, or the declared default, or null if neither exists. Template expressions and host.props.amount.value read that accepted value. A user's edit in a native control remains in that control's editing surface, where its validity reports the failure.

The controller can inspect both sides through the prop handle: host.props.amount.inputValue reads the latest direct input, host.props.amount.value reads the accepted value, and host.props.amount.validity describes that input. Declared props are absent from host.state. The component mounts even when the initial input cannot be parsed. Rendering follows the accepted value; the invalid input is never implicitly substituted into a template. A well-typed value outside min, max, values, or another value constraint is accepted and makes the prop invalid.

Direct supply to a number prop with default="5"inputValuevalue read by templatesvalidity
Initial amount="oops""oops"5badInput
Supply 222valid
Supply "oops""oops"2badInput
Supply 777valid

A binding that evaluates to the wrong type does not supply the destination prop at all: its inputValue, accepted value, and validity remain unchanged. See Invalid reactive results.

Validity for a directly supplied prop describes its current input, even when conversion leaves the accepted value unchanged. Validity at a binding destination does not describe an attempt that the binding skipped. For example, if a number prop holds 2 and a binding next evaluates to the string "oops", the destination stays at 2 and remains valid; a directly supplied "oops" would keep 2 but report badInput. A statically provable mismatch in an authored expression is a build error; a live implementation may warn about a malformed authored expression, but ordinary invalid data must not generate repeated console warnings.

On a component with a non-native root, el.validity exposes the corresponding ValidityState flags and el.validity.errors gives each failure's prop path. checkValidity() reports whether the current values pass. When a component renders a native form control, the control retains its native ValidityState; the compatibility layer combines additional component failures with native validity. The Types chapter defines which constraints apply to each type.

This is the schema, made native

This is the mechanism behind “the contract is the schema” (see Types). A typed prop, a bind: input, or a <data> value that fails its declared type produces a native validity error, one that form controls, components, and data sources all share.

Where the validity lives

  • A typed prop gives validity to the component's root element. When that root is a native control, such as a component that lowers to <input>, the prop's failures join the control's own, and the control still takes part in its form natively.
  • A <data> source with a schema has validity for its current value. Each failure carries the path of the part that failed, so a form can point each error at its field.
  • The pure helper validate(value, type | schema) validates raw data attached to no element and returns the same result.
el.validity            // { valid: false, rangeOverflow: true, errors: [{ reason: "rangeOverflow", path: "age", message: "…" }] }

// a failure the type cannot know about, such as a server's answer
el.setValidity([{ reason: "taken", message: "That email is in use." }])

// a pure helper: validate any value against a type or schema, no element involved
validate(value, type | schema)   // → { valid, errors }

When validation runs

A component's values are already reactive dependencies, so validity recomputes whenever a value changes; el.validity is always current, and nothing needs to trigger it. el.validate() is for an explicit check, such as at submit time. Showing an error follows HTML Forms: it waits for interaction, through :user-invalid.

For an asynchronous rule, such as whether a name is taken, a <data> lookup keeps the answer current and setValidity() applies it (see Reactivity).

How it runs today

Authors write the platform syntax

Authors set validity through setValidity() and style :valid, :invalid, and :user-invalid. They do not target polyfill attributes. Until browsers implement HTML Forms' validity for every element, the reference implementation provides it:

  • native form controls use their real setCustomValidity(), so the real :invalid and form submission still work;
  • form-associated custom elements use the real ElementInternals.setValidity();
  • every other element gets the same validity object and event, and the matching ARIA state.

When browsers expose validity on every element, the implementation steps aside without any change to component source or application CSS.

References

  1. HTML Forms Level 1, constraint validation on any element (the validity model this chapter builds on).
  2. WHATWG HTML, the Constraint Validation API and ValidityState.
  3. CSS Selectors Level 4, validity pseudo-classes (:valid, :invalid, :user-invalid).
  4. IETF JSON Schema; Zod / Valibot (the issue-list model this mirrors).

Declarative HTML Components Level 1: Validation. Unofficial Editor's Draft · Stage 0. Published in the HTML Next collection by the Next Web Working Group; not a W3C or WHATWG deliverable. Version history.