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

```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 title="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

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

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

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

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

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

## Related

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

---

Canonical HTML page: https://adminkit.io/nextjs/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
