# Next.js Bootstrap Form Controls

Next.js Bootstrap form controls: inputs and textareas as Bootstrap markup in server components, with state and submits handled in client components.

Most forms are built from text inputs and textareas. In AdminKit for Next.js you write them as Bootstrap's markup in JSX, with `className` and `htmlFor`, not as components. Bootstrap's Sass styles them in the free and PRO versions of [AdminKit for Next.js](/nextjs/), and every class of the [Bootstrap form docs](https://getbootstrap.com/docs/5.3/forms/form-control/) works.

In the App Router, pages are server components: they render on the server and have no state. Plain form markup is fine there, so you don't need `"use client"` for it. A form that holds state or handles events, such as `onChange` and `onSubmit`, needs the browser. Put it in a client component, a file that starts with `"use client"`.

AdminKit PRO adds three field components: the [select](/nextjs/docs/forms/select/), the [input mask](/nextjs/docs/forms/input-mask/) and the [date picker](/nextjs/docs/forms/date-picker/).

## Example

```html
<div class="mb-3">
  <label for="formControlEmail" class="form-label">Email address</label>
  <input type="email" class="form-control" id="formControlEmail" placeholder="name@example.com">
</div>
<div class="mb-3">
  <label for="formControlMessage" class="form-label">Message</label>
  <textarea class="form-control" id="formControlMessage" rows="3"></textarea>
</div>
```
```tsx code
<div className="mb-3">
  <label htmlFor="formControlEmail" className="form-label">Email address</label>
  <input type="email" className="form-control" id="formControlEmail" placeholder="name@example.com" />
</div>
<div className="mb-3">
  <label htmlFor="formControlMessage" className="form-label">Message</label>
  <textarea className="form-control" id="formControlMessage" rows={3} />
</div>
```

## Classes

| Class | Element | Description |
| --- | --- | --- |
| `form-label` | `<label>` | The label above a field |
| `form-control` | `<input>`, `<textarea>` | The text field |
| `form-control-lg`, `form-control-sm` | `.form-control` | Larger or smaller field |
| `form-control-plaintext` | `<input readOnly>` | A read-only value without the field border |
| `form-control-color` | `<input type="color">` | The color picker |
| `form-text` | `<div>` | Help text below a field |
| `form-select` | `<select>` | The native select, see [Select](/nextjs/docs/forms/select/) |

## Submitting a form

AdminKit for Next.js builds to static files (`output: "export"`). No Next.js server runs while people use the site, so Server Actions and `<form action={serverAction}>` are not available. Handle the submit in a client component instead, and send the data to your API with `fetch`:

```tsx title="src/components/contact-form.tsx"
"use client";

import type { FormEvent } from "react";

export function ContactForm() {
  const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    await fetch("https://api.example.com/contact", {
      method: "POST",
      body: new FormData(event.currentTarget),
    });
  };

  return (
    <form onSubmit={onSubmit}>
      <div className="mb-3">
        <label htmlFor="contact-email" className="form-label">Email address</label>
        <input type="email" className="form-control" id="contact-email" name="email" required />
      </div>
      <div className="mb-3">
        <label htmlFor="contact-message" className="form-label">Message</label>
        <textarea className="form-control" id="contact-message" name="message" rows={3} />
      </div>
      <button type="submit" className="btn btn-primary">Send</button>
    </form>
  );
}
```

The page stays a server component and renders the form. `pageMetadata()` reads the title and description of `/contact` from `src/data/pages.json`, so add the route there:

```tsx title="src/app/(dashboard)/contact/page.tsx"
import { ContactForm } from "@/components/contact-form";
import { pageMetadata } from "@/lib/site";

export const metadata = pageMetadata("/contact");

export default function ContactPage() {
  return (
    <>
      <h1 className="h3 mb-3">Contact</h1>
      <ContactForm />
    </>
  );
}
```

## Controlled and uncontrolled inputs

The fields above are uncontrolled. They keep their own value, `defaultValue` sets the starting one, and the submit handler reads them with `FormData`. When the page has to react to each keystroke, use a controlled field. It takes `value` from state and updates that state in `onChange`:

```tsx title="src/components/bio-field.tsx"
"use client";

import { useState } from "react";

export function BioField() {
  const [bio, setBio] = useState("");

  return (
    <div className="mb-3">
      <label htmlFor="bio" className="form-label">Bio</label>
      <textarea
        className="form-control"
        id="bio"
        rows={3}
        maxLength={160}
        value={bio}
        onChange={(event) => setBio(event.target.value)}
        aria-describedby="bio-count"
      />
      <div className="form-text" id="bio-count">{160 - bio.length} characters left</div>
    </div>
  );
}
```

Next.js builds the HTML of the page ahead of time, and the browser then takes over that HTML. This step is called hydration, and the browser's first render has to match the HTML. So start the state with a fixed value, not one that only the browser knows, such as `localStorage`. A `value` without `onChange` makes the field read-only, and React warns about it.

## Sizes

For a field that has to line up with a large button or fit a compact filter bar, add `form-control-lg` or `form-control-sm`.

```html
<input class="form-control form-control-lg mb-3" type="text" placeholder="Large input" aria-label="Large input">
<input class="form-control mb-3" type="text" placeholder="Medium input" aria-label="Medium input">
<input class="form-control form-control-sm" type="text" placeholder="Small input" aria-label="Small input">
```
```tsx code
<input className="form-control form-control-lg mb-3" type="text" placeholder="Large input" aria-label="Large input" />
<input className="form-control mb-3" type="text" placeholder="Medium input" aria-label="Medium input" />
<input className="form-control form-control-sm" type="text" placeholder="Small input" aria-label="Small input" />
```

## Disabled and read only

Sometimes a form shows a value that people may not change. A `disabled` field is grayed out and left out of the form data. A `readOnly` field cannot be edited, but it is still submitted.

```html
<input class="form-control mb-3" type="text" placeholder="Disabled input" aria-label="Disabled input" disabled>
<input class="form-control" type="text" placeholder="Readonly input" aria-label="Readonly input" readonly>
```
```tsx code
<input className="form-control mb-3" type="text" placeholder="Disabled input" aria-label="Disabled input" disabled />
<input className="form-control" type="text" placeholder="Readonly input" aria-label="Readonly input" readOnly />
```

## Accessibility

- Label every field: a `<label>` whose `htmlFor` matches the field's `id`, or `aria-label` without a visible label. A placeholder does not replace a label.
- Link help text with `aria-describedby`, so screen readers read it with the field.
- Set the `type` (`email`, `tel`, `url`, `number`) and `autoComplete`: mobile keyboards and password managers use them.
- A disabled field is skipped by the keyboard. Use `readOnly` when the value must stay reachable.

## Related

- [React Bootstrap form controls](/react/docs/forms/form-control/): the same form controls in AdminKit for React (Vite)
- [Bootstrap 5 form controls](/bootstrap/docs/forms/form-control/): the form controls of the HTML version
- [Form layout](/nextjs/docs/forms/layout/): grids, horizontal forms and input groups
- [Validation](/nextjs/docs/forms/validation/): feedback for invalid fields
- [Basic Inputs demo](https://nextjs-demo.adminkit.io/forms-basic-inputs)

---

Canonical HTML page: https://adminkit.io/nextjs/docs/forms/form-control/

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
