React Form Validation: HTML5, Bootstrap, React Hook Form, Native, and APIs
Compare native HTML5, Bootstrap, React Hook Form and API validation in React, with runnable code, accessibility guidance and server error patterns.
Short answer
React form validation has three separate jobs: the browser checks basic constraints, your UI manages and presents feedback, and your server or API makes the final decision. Use semantic HTML and the Constraint Validation API for baseline rules, Bootstrap or React Bootstrap for presentation, React Hook Form when you need managed state and reusable rules, and server validation for every value you accept. These layers can be combined.
Native HTML validation is the smallest starting point. Bootstrap 5.0 validation adds classes and feedback markup. React Hook Form manages registration, errors and schema resolvers. None of them makes client input trustworthy: a caller can alter the page or send an HTTP request directly, so validate again on the server (MDN, OWASP).
Choose an approach
| Approach | Best fit | Control and complexity | Authoritative? |
|---|---|---|---|
| Native HTML5 | Required fields, email, length, ranges and patterns | Lowest complexity; browser controls default timing and UI | No |
| Constraint Validation API | Cross-field or domain checks | Use checkValidity(), reportValidity() and setCustomValidity() |
No |
| Bootstrap / React Bootstrap | Consistent Bootstrap feedback | CSS and component state; you choose when to mark validated | No |
| React Hook Form | Many fields, reusable rules, touched state or schemas | Library setup with centralized registration and errors | No |
| API/server | Permissions, uniqueness, business and security rules | Runs on every request; returns structured errors | Yes |
A practical default is native constraints plus server validation. Add Bootstrap for a Bootstrap design system, and React Hook Form when form state or schema integration becomes difficult to maintain manually.
Native HTML5 validation in React
React renders normal HTML controls, so semantic types and attributes work as they do in plain HTML.
import { useState } from 'react';
export default function SignupForm() {
const [sent, setSent] = useState(false);
function onSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
if (!form.checkValidity()) { form.reportValidity(); return; }
const values = Object.fromEntries(new FormData(form));
console.log(values); // send to your API
setSent(true);
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" minLength={12} required />
<label htmlFor="age">Age</label>
<input id="age" name="age" type="number" min="13" max="120" required />
<button type="submit">Create account</button>
{sent && <p role="status">Submitted</p>}
</form>
);
}
required, type, minLength, maxLength, min, max, step and pattern cover common constraints. inputMode suggests a mobile keyboard but is not validation. Browser messages and timing differ between engines, and browser-default feedback cannot be styled consistently.
Custom rules with the Constraint Validation API
function validatePasswords(form) {
const password = form.elements.password;
const confirmation = form.elements.confirmation;
confirmation.setCustomValidity(
password.value === confirmation.value ? '' : 'Passwords must match.'
);
}
function onSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
validatePasswords(form);
if (!form.reportValidity()) return;
// POST FormData here
}
Use checkValidity() for a boolean without UI and reportValidity() to show a message. Clear custom errors with an empty string whenever values become valid. For asynchronous checks such as username availability, keep a pending state and enforce the rule on the server.
Bootstrap and React Bootstrap feedback
Bootstrap 5.0 scopes its :valid and :invalid styles under .was-validated, so empty required controls do not look invalid on first paint. Its custom-feedback example adds novalidate to suppress browser popups while leaving validation APIs available.
import { useState } from 'react';
export default function BootstrapForm() {
const [validated, setValidated] = useState(false);
function submit(event) {
event.preventDefault();
const form = event.currentTarget;
setValidated(true);
if (!form.checkValidity()) return;
// send FormData(form)
}
return (
<form className={`needs-validation ${validated ? 'was-validated' : ''}`} noValidate onSubmit={submit}>
<label className="form-label" htmlFor="b-email">Email</label>
<input className="form-control" id="b-email" name="email" type="email" required aria-describedby="email-error" />
<div id="email-error" className="invalid-feedback">Enter a valid email address.</div>
<button className="btn btn-primary" type="submit">Save</button>
</form>
);
}
For API responses, add is-invalid to the control and connect the message with aria-describedby. React Bootstrap exposes the same general state through its validated and noValidate props; these are component APIs, not replacements for browser constraints.
Bootstrap 5.0 documents this version-specific caveat: “We are aware that currently the client-side custom validation styles and tooltips are not accessible, since they are not exposed to assistive technologies.” Prefer native browser messages or server-rendered, programmatically associated feedback when this matters.
React Hook Form
React Hook Form helps when registration, touched and dirty state, repeated rules or schema validation would otherwise spread through many components. Its official examples show required, pattern and custom validate rules; the project supports resolvers including Yup, Zod, AJV and Superstruct.
import { useForm } from 'react-hook-form';
export default function ProfileForm() {
const { register, handleSubmit, formState: { errors, isSubmitting }, setError } = useForm();
async function save(values) {
const response = await fetch('/api/profile', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(values)
});
if (!response.ok) {
const data = await response.json();
for (const [field, message] of Object.entries(data.fieldErrors || {}))
setError(field, { type: 'server', message });
}
}
return (
<form onSubmit={handleSubmit(save)} noValidate>
<label htmlFor="username">Username</label>
<input id="username" {...register('username', {
required: 'Username is required',
pattern: { value: /^[a-z0-9_]{3,20}$/, message: 'Use 3–20 lowercase letters, numbers or underscores.' }
})} aria-invalid={errors.username ? 'true' : 'false'} aria-describedby="username-error" />
<p id="username-error" role="alert">{errors.username?.message}</p>
<button disabled={isSubmitting}>Save</button>
</form>
);
}
Choose timing with mode, such as onBlur or onChange. Keep expensive asynchronous checks out of every keystroke. A resolver centralizes a schema, but equivalent rules must still run on the server.
API validation and error mapping
Return a stable shape that supports field and form messages:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"message": "Please correct the highlighted fields.",
"fieldErrors": {
"email": "That address is already registered.",
"startDate": "Start date must be before end date."
}
}
Validate types, lengths, normalization, authorization, uniqueness and business rules on the server. Treat unknown fields deliberately. Avoid leaking sensitive details and include a request ID for logs. TanStack Form documents a similar pattern with form-level and field-level server errors; it is an example, not a requirement.
Combining the layers
- Write semantic controls and native constraints.
- Choose when feedback appears: submit, blur or change.
- Use Bootstrap classes or React Hook Form state to render one clear message per field.
- Submit a normalized payload over HTTPS.
- Run authoritative API validation and map its response back to fields.
- Focus the first invalid control and announce form-level failures with a live region.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Required fields look red immediately | Bootstrap selectors are active before submission | Apply was-validated only after submit. |
| Browser popup and Bootstrap message both appear | Native UI was not suppressed | Use noValidate/novalidate for custom feedback. |
checkValidity() unexpectedly fails |
A hidden, disabled or pattern-constrained control remains in the form | Inspect validity, names and values. |
| Custom error never clears | setCustomValidity('') is not called |
Recompute it on every relevant input/change. |
| Server error is not announced | Message is not associated with the input | Use aria-describedby and an appropriate live region. |
| Client says valid but API rejects | Business rule exists only on the server | Display the structured 422 response and add only a convenience hint client-side. |
| Async checks race | An older response overwrites a newer value | Abort stale requests or compare the checked value before applying results. |
Performance, reliability and cost
Native checks add negligible code. Libraries add JavaScript and state work, so avoid expensive validation on every keypress. Debounce or cancel remote checks, and never make submit depend solely on a client availability request. Server validation is the reliability boundary; handle timeouts, retries and duplicate submissions with idempotency keys where needed. Log validation failures without storing secrets.
Or skip the browser setup
If you need screenshots of validated states for documentation, regression fixtures or review, ScreenshotNeo captures the rendered page through one request. Cookie banners, newsletter popups and chat widgets are removed before capture; bot checks, blank pages and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. See the ScreenshotNeo API docs.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Create a free ScreenshotNeo account with 1,000 screenshots per month and no card.
FAQ
Does React replace HTML validation?
No. React renders constraint-capable controls and provides submission APIs; choose the validation layers you need.
Should I use React Hook Form and Bootstrap together?
Yes, when React Hook Form manages state and Bootstrap presents feedback. Keep one source of truth for each rule.
Can a pattern protect an API?
No. Patterns improve feedback; the API must validate independently.
When should validation run?
Submit suits whole-form checks, blur suits field feedback, and change suits short cheap rules. Remote checks should be debounced or deliberate.


