AdminKitDocs
Forms

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.

src/components/signup-form.tsx
"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

ClassElementDescription
was-validated<form>Shows the :valid and :invalid styles of every field in it
is-valid, is-invalidfieldMarks one field, without .was-validated
valid-feedback, invalid-feedback<div> after the fieldThe message for the matching state
valid-tooltip, invalid-tooltip<div> after the fieldThe 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-describedby at the feedback text, so screen readers read the message with the field.
  • Bootstrap's classes only change the look. Add aria-invalid where 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.

Last updated on

On this page