Next.js Bootstrap Form Validation
Next.js Bootstrap form validation: the browser's own checks with Bootstrap feedback in a client component, and errors from your API shown with is-invalid.
Good validation catches a missing or wrong value before the form is sent, and says what to fix. AdminKit for Next.js validates forms in the browser. The fields carry HTML constraints, such as required, type="email", minLength and pattern, and Bootstrap's validation classes show the result. Neither the free nor the PRO version uses a validation library.
The site builds to static files, so no Next.js server runs to validate with Server Actions. Validate in a client component instead, a file that starts with "use client". Then send the data to your API with fetch, and show the errors it returns. The PRO demo's Validation page renders its CustomValidation client component from a server component page.
Example
The form has noValidate, so the browser does not show its own messages. When the form is submitted, checkValidity() checks the fields. The validated state then adds .was-validated, which shows Bootstrap's :valid and :invalid styles.
"use client";
import { type FormEvent, useState } from "react";
import { cx } from "@/lib/cx";
export function SignupForm() {
const [validated, setValidated] = useState(false);
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setValidated(true);
if (!event.currentTarget.checkValidity()) {
return;
}
await fetch("https://api.example.com/signup", {
method: "POST",
body: new FormData(event.currentTarget),
});
};
return (
<form className={cx("row g-3 needs-validation", validated && "was-validated")} noValidate onSubmit={onSubmit}>
<div className="col-md-6">
<label htmlFor="validationFirstName" className="form-label">First name</label>
<input type="text" className="form-control" id="validationFirstName" name="firstName" defaultValue="Mark" required />
<div className="valid-feedback">Looks good!</div>
</div>
<div className="col-md-6">
<label htmlFor="validationCity" className="form-label">City</label>
<input type="text" className="form-control" id="validationCity" name="city" required />
<div className="invalid-feedback">Please provide a valid city.</div>
</div>
<div className="col-12">
<div className="form-check">
<input className="form-check-input" type="checkbox" value="" id="validationTerms" required />
<label className="form-check-label" htmlFor="validationTerms">Agree to terms and conditions</label>
<div className="invalid-feedback">You must agree before submitting.</div>
</div>
</div>
<div className="col-12">
<button className="btn btn-primary" type="submit">Submit form</button>
</div>
</form>
);
}cx, from src/lib/cx.ts, joins the truthy class names. The demo's CustomValidation prevents the submit only when the form is invalid, so a valid demo form submits the normal browser way. In your app, always call preventDefault() and post the data yourself, as above.
Classes
| Class | Element | Description |
|---|---|---|
was-validated | <form> | Shows the :valid and :invalid styles of every field in it |
is-valid, is-invalid | field | Marks one field, without .was-validated |
valid-feedback, invalid-feedback | <div> after the field | The message for the matching state |
valid-tooltip, invalid-tooltip | <div> after the field | The message as a tooltip; the parent needs position-relative |
.form-control, .form-select and .form-check have validation styles. In an .input-group, the feedback goes inside the group, after the input.
Browser defaults
If the browser's own messages are good enough, leave out noValidate and the handlers. Such a form has no state, so it can stay in the server component page, as the demo's "Browser validation" card does:
<form className="row g-3">
<div className="col-md-8">
<label htmlFor="defaultEmail" className="form-label">Email</label>
<input type="email" className="form-control" id="defaultEmail" required />
</div>
<div className="col-12">
<button className="btn btn-primary" type="submit">Submit form</button>
</div>
</form>Without an onSubmit handler, a valid form does a regular browser submit to its action. A static site cannot answer that request. Use this kind of form when it posts to an external URL, or add a handler that runs in the browser.
Errors from your API
Checks such as "this username is taken" happen on your API. Store the errors from its response in state and mark the fields with .is-invalid:
<div>
<label htmlFor="serverUsername" className="form-label">Username</label>
<input
type="text"
className={cx("form-control", errors.username && "is-invalid")}
id="serverUsername"
name="username"
defaultValue="admin"
aria-invalid={Boolean(errors.username)}
aria-describedby="serverUsernameFeedback"
/>
<div id="serverUsernameFeedback" className="invalid-feedback">{errors.username}</div>
</div>Accessibility
- Point
aria-describedbyat the feedback text, so screen readers read the message with the field. - Bootstrap's classes only change the look. Add
aria-invalidwhere you set.is-invalid. - After a failed submit, focus the first invalid field, such as
form.querySelector<HTMLElement>(":invalid")?.focus(). - Keep a text message for every invalid state. Color alone is not enough.
Related
- React Bootstrap form validation: the same validation in AdminKit for React (Vite)
- Bootstrap 5 validation: validation in the HTML version
- Form controls: inputs and submitting a form
- Validation demo
Last updated on
Date picker
A Next.js date picker for the App Router with Vanilla Calendar Pro: a client component for an inline calendar, and in AdminKit PRO a date and time input.
Layout
Next.js Bootstrap form layout for the App Router: grid, horizontal and inline forms and input groups as server-rendered markup, plus a client Dropdown.