React does not include form validation. Most teams end up either writing it from scratch or pulling in a large library. But browsers already ship a validation API: the HTML5 Constraint Validation API. It understands required, type="email", pattern, min, max, minLength, maxLength — everything you would otherwise reimplement.
This package pairs React with that API. You write your inputs the same way you always have. Form and InputGroup wrap them, read the browser's validity state, and expose it to your render props so you can show errors, toggle classes, and control submission.
- Works with any input: plain HTML elements or third-party React components
- Validation constraints are standard HTML attributes (
required,pattern,type="email", etc.) — nothing new to learn - Custom validators slot in via the same API (
setCustomValidity) - Custom error messages per input and per constraint type (
translateprop) FormandInputGroupboth expose validity state, error messages, and a ref — directly in the render prop- Optional Redux integration: exposes a reducer so form state can live in the store
Form renders a <form noValidate> wrapper and tracks the overall validity state across all registered input groups.
InputGroup wraps one or more related inputs (for example, three selects for day, month, year). It queries the DOM for inputs by name, reads their ValidityState, and exposes valid, error, errors, and inputGroup in its render prop.
You register inputs with the validate prop. You can pass an array of names (for standard HTML5 validation) or an object of name: validator pairs (when you need custom logic on top).
npm i react-html5-form- Bootstrap 4 example — plain HTML inputs
- Material UI example — third-party input components
- Redux example — form state in Redux store
import { Form } from "react-html5-form";<Function>onSubmit— form submit handler (optional). Receives theForminstance. Can be async.<Function>onMount— called incomponentDidMountwith the component instance (optional)<Function>onUpdate— called when validity state changes (optional). Useful for syncing to Redux.- Any standard
<form>HTML attributes
The render prop receives:
<String>error— error message set withsetError()<Boolean>valid— overall form validity (logical AND of all input groups)<Boolean>pristine— true until the user first interacts with the form<Boolean>submitting— true while theonSubmithandler is running<Boolean>submitted— true after a successful submission<React.Component>form— the component instance (access to the API below)
valid,pristine,submitting, andsubmittedare also set asdata-*attributes on the<form>element, e.g.data-submitting="true".
checkValidityAndUpdateInputGroups()— validate all input groups and update their statecheckValidityAndUpdate()— validate form validity without updating input groupssetError( message, ms? )— set a form-level error message. Passmsto auto-clear after that many milliseconds.submit()— programmatically submit the formscrollIntoViewFirstInvalidInputGroup()— scroll the first invalid group into view (called automatically on submit)getRef()— returns theReffor the<form>DOM node (form.getRef().current)debugInputGroups( index? )— returns debug info for all input groups, or a specific one by index
import React from "react";
import { render } from "react-dom";
import { Form } from "react-html5-form";
const MyForm = () => (
<Form id="myform">
{({ error, valid }) => (
<>
Form content
</>
)}
</Form>
);
render( <MyForm />, document.getElementById( "app" ) );Form renders a <form noValidate> element and passes any extra props straight through — so id, className, action, etc. all work as expected.
async function onSubmit( form ) {
try {
const res = await fetch( "/api/submit" ).then( r => r.json() );
if ( !res.ok ) {
form.setError( res.error );
}
} catch ( e ) {
form.setError( "Server error, please try again" );
}
}
const MyForm = () => (
<Form onSubmit={onSubmit} id="myform">
{({ error, valid, pristine, submitting, form }) => (
<>
{ error && <div className="alert alert-danger">{error}</div> }
Form content
<button disabled={ pristine || submitting } type="submit">Submit</button>
</>
)}
</Form>
);onSubmit is called only when all input groups are valid. While it runs, submitting is true. The submit button is disabled until the user first interacts with the form (pristine) and while the handler is in flight.
InputGroup defines a scope for one or more related inputs. It reads their ValidityState and makes it available in the render prop.
import { Form, InputGroup } from "react-html5-form";<Object|Array>validate— (required) register inputs by name. Accepts an array of name strings or an object ofname: validatorpairs.<Object>translate— (optional) custom messages per input per constraint. Keys areValidityStateproperty names (see table below).<String>tag— (optional) tag for the wrapper element, default"div"<Function>onMount— called incomponentDidMountwith the component instance (optional)<Function>onUpdate— called when validity state changes (optional)- Any standard HTML attributes
<String>error— validation message for the first invalid input<String[]>errors— validation messages for all invalid inputs in the group<Boolean>valid— true when all inputs in the group are valid<Boolean>pristine— true until the user first interacts with the form<React.Component>inputGroup— the component instance (access to the API below)
checkValidityAndUpdate()— validate the group and update its statecheckValidity()— returns boolean; does not update stategetInputByName( name )— get anInputinstance by namegetValidationMessages()— get all validation messages in the groupgetRef()— returns theReffor the wrapper DOM node
<InputGroup
tag="fieldset"
validate={[ "email" ]}
translate={{
email: {
valueMissing: "Email is required",
typeMismatch: "Enter a valid email address"
}
}}>
{({ error, valid }) => (
<div className="form-group">
<label htmlFor="emailInput">Email address</label>
<input
type="email"
required
name="email"
className={`form-control ${ !valid && "is-invalid" }`}
id="emailInput"
placeholder="Enter email" />
{ error && <div className="invalid-feedback">{error}</div> }
</div>
)}
</InputGroup>The input uses standard HTML5 attributes (type="email", required). When the form is submitted with an empty field, the browser sets ValidityState.valueMissing = true and the group receives the translated message in error.
When the built-in HTML5 constraints are not enough, pass a validator function. It receives the Input instance and must return a boolean. Call setCustomValidity to set the error message.
<InputGroup validate={{
"vatId": ( input ) => {
if ( !input.current.value.startsWith( "DE" ) ) {
input.setCustomValidity( "VAT number must start with DE" );
return false;
}
return true;
}
}}>
{({ error, valid }) => (
<div className="form-group">
<label htmlFor="vatIdInput">VAT number (optional)</label>
<input
className={`form-control ${ !valid && "is-invalid" }`}
id="vatIdInput"
name="vatId"
placeholder="DE123456789" />
{ error && <div className="invalid-feedback">{error}</div> }
</div>
)}
</InputGroup>A single InputGroup can wrap several related inputs. All their error messages are collected into the errors array.
const validateSelect = ( input ) => {
if ( input.current.value === "Choose..." ) {
input.setCustomValidity( `Please select a ${input.current.title}` );
return false;
}
return true;
};<InputGroup validate={{ "day": validateSelect, "month": validateSelect }}>
{({ errors, valid }) => (
<div className="form-group">
<div className="form-row">
<div className="form-group col-md-6">
<label htmlFor="selectDay">Day</label>
<select name="day" id="selectDay" title="day" className={`form-control ${ !valid && "is-invalid" }`}>
<option>Choose...</option>
{ [ ...Array( 31 ).keys() ].map( i => <option key={i}>{i + 1}</option> ) }
</select>
</div>
<div className="form-group col-md-6">
<label htmlFor="selectMonth">Month</label>
<select name="month" id="selectMonth" title="month" className={`form-control ${ !valid && "is-invalid" }`}>
<option>Choose...</option>
{ [ "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
].map( ( m, i ) => <option key={i}>{m}</option> ) }
</select>
</div>
</div>
{ errors.map( ( error, i ) => <div key={i} className="alert alert-danger">{error}</div> ) }
</div>
)}
</InputGroup>By default, groups validate on submit. To validate as the user types, call checkValidityAndUpdate from an onInput handler.
const onInput = ( e, inputGroup, form ) => {
inputGroup.checkValidityAndUpdate();
form.checkValidityAndUpdate();
};<InputGroup
validate={[ "firstName" ]}
translate={{
firstName: {
patternMismatch: "Must be 5 to 30 characters"
}
}}>
{({ error, valid, inputGroup }) => (
<div className="form-group">
<label htmlFor="firstNameInput">First name</label>
<input
pattern="^.{5,30}$"
required
className={`form-control ${ !valid && "is-invalid" }`}
id="firstNameInput"
name="firstName"
onInput={( e ) => onInput( e, inputGroup, form )}
placeholder="Enter first name" />
{ error && <div className="invalid-feedback">{error}</div> }
</div>
)}
</InputGroup>All standard HTML5 constraints are supported. Each maps to a ValidityState property name, which is the key to use in the translate prop.
| HTML attribute | ValidityState key | Default message |
|---|---|---|
required |
valueMissing |
"Please fill out this field." |
type (email, url, …) |
typeMismatch |
"Enter a valid {type}." |
pattern |
patternMismatch |
title attribute, or "Value does not match the required format." |
min |
rangeUnderflow |
"Value must be {min} or more." |
max |
rangeOverflow |
"Value must be {max} or less." |
minlength |
tooShort |
"Use at least {minLength} characters." |
maxlength |
tooLong |
"Use {maxLength} characters or fewer." |
step |
stepMismatch |
"Value does not match the required step ({step})." |
| (browser cannot parse the value) | badInput |
"Enter a valid value." |
via setCustomValidity() |
customError |
(message you passed) |
All keys can be overridden per input using the translate prop:
<InputGroup
validate={[ "age" ]}
translate={{
age: {
valueMissing: "Age is required",
rangeUnderflow: "Must be 18 or older",
rangeOverflow: "Must be 120 or younger",
stepMismatch: "Whole numbers only"
}
}}>
{({ error, valid }) => (
<input type="number" name="age" required min="18" max="120" step="1" />
)}
</InputGroup>Each input registered in an InputGroup is wrapped in an Input instance. You get access to it via inputGroup.getInputByName( name ).
current— the underlying HTML elementsetCustomValidity( message )— put the input in an invalid state with a custom message. Pass an empty string to clear.checkValidity()— returns booleangetValidationMessage()— returns the current message, respecting anytranslatemapping
Import the html5form reducer and add it to your store. Then pass formActions and formState props to Form so it keeps the store in sync.
import { createStore, combineReducers } from "redux";
import { html5form } from "react-html5-form";
const store = createStore( combineReducers({ html5form }) );Once connected, all form, input group, and input validity states are available in the Redux tree:
See the full Redux example.


