# 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](/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.

```html
<form class="row g-3 needs-validation" id="validationExample" novalidate>
  <div class="col-md-6">
    <label for="validationFirstName" class="form-label">First name</label>
    <input type="text" class="form-control" id="validationFirstName" value="Mark" required>
    <div class="valid-feedback">Looks good!</div>
  </div>
  <div class="col-md-6">
    <label for="validationCity" class="form-label">City</label>
    <input type="text" class="form-control" id="validationCity" required>
    <div class="invalid-feedback">Please provide a valid city.</div>
  </div>
  <div class="col-12">
    <div class="form-check">
      <input class="form-check-input" type="checkbox" value="" id="validationTerms" required>
      <label class="form-check-label" for="validationTerms">Agree to terms and conditions</label>
      <div class="invalid-feedback">You must agree before submitting.</div>
    </div>
  </div>
  <div class="col-12">
    <button class="btn btn-primary" type="submit">Submit form</button>
  </div>
</form>

<script>
const form = document.querySelector("#validationExample");
form.addEventListener("submit", (event) => {
  event.preventDefault();
  form.classList.add("was-validated");
});
</script>
```
```tsx code
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

| 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, shown for the matching state |
| `valid-tooltip`, `invalid-tooltip` | `<div>` after the field | The 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:

```html
<form class="row g-3">
  <div class="col-md-8">
    <label for="defaultEmail" class="form-label">Email</label>
    <input type="email" class="form-control" id="defaultEmail" required>
  </div>
  <div class="col-12">
    <button class="btn btn-primary" type="submit">Submit form</button>
  </div>
</form>
```
```tsx code
<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:

```html
<div>
  <label for="serverUsername" class="form-label">Username</label>
  <input type="text" class="form-control is-invalid" id="serverUsername" value="admin" aria-describedby="serverUsernameFeedback">
  <div id="serverUsernameFeedback" class="invalid-feedback">This username is taken.</div>
</div>
```
```tsx code
<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.

## Related

- [Next.js Bootstrap form validation](/nextjs/docs/forms/validation/): the same validation in AdminKit for Next.js
- [Bootstrap 5 validation](/bootstrap/docs/forms/validation/): validation in the HTML version
- [Form controls](/react/docs/forms/form-control/): inputs, textareas and reading a form
- [Validation demo](https://react-demo.adminkit.io/forms-validation)

---

Canonical HTML page: https://adminkit.io/react/docs/forms/validation/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
