AdminKitDocs
Forms

React Bootstrap Form Validation

React Bootstrap form validation in AdminKit: the browser's constraint checks with Bootstrap feedback, a was-validated class from state, and server errors.

Validation tells people what is wrong with a form before it is sent, right next to the field. AdminKit for React uses the browser's own checks: the constraints you put on a field, such as required, type="email", minLength and pattern. Bootstrap's validation classes then show the result. There is no validation library and no component: a few lines of state set .was-validated on the form. It works in the free and PRO versions of AdminKit for React, and the PRO demo shows it on the Validation page.

Example

noValidate turns off the browser's own messages. When the form is submitted, checkValidity() checks every field. .was-validated on the form then makes Bootstrap show the :valid and :invalid styles and the feedback texts.

import { type FormEvent, useState } from "react";
import { cx } from "@/lib/cx";

export function SignupForm() {
  const [validated, setValidated] = useState(false);

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setValidated(true);
    if (!event.currentTarget.checkValidity()) {
      return;
    }
    const data = new FormData(event.currentTarget);
    // send data to your API
  };

  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 class names and drops false. The demo's CustomValidation component only calls preventDefault() when the form is invalid. The browser then submits a valid form the normal way and reloads the page. In an app, prevent the default every time and send 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, shown for the matching state
valid-tooltip, invalid-tooltip<div> after the fieldThe message as a tooltip; the parent needs position-relative

Feedback works for .form-control, .form-select and .form-check. In an .input-group, put the feedback inside the group, after the input.

Browser defaults

For a simple form, the browser's built-in messages may be enough. Without noValidate, the browser blocks the submit of an invalid form and shows its own message bubble. This needs no state at all:

<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>

Errors from your API

Some checks only your server can do, such as "this username is taken". Keep the errors it returns in state, and set .is-invalid on the fields:

<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

  • Link each feedback text to its field with aria-describedby, so screen readers read the message with the field.
  • Bootstrap's classes are visual only. Set aria-invalid on fields you mark with .is-invalid, as in the example above.
  • After a failed submit, move the focus to the first invalid field, for example with form.querySelector<HTMLElement>(":invalid")?.focus().
  • Do not rely on color alone: keep a feedback text for every invalid state.

Last updated on

On this page