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, and every class of the Bootstrap form docs 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, the input mask and the date picker.
Example
<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 |
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:
"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:
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:
"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.
<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.
<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>whosehtmlFormatches the field'sid, oraria-labelwithout 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) andautoComplete: mobile keyboards and password managers use them. - A disabled field is skipped by the keyboard. Use
readOnlywhen the value must stay reachable.
Related
- React Bootstrap form controls: the same form controls in AdminKit for React (Vite)
- Bootstrap 5 form controls: the form controls of the HTML version
- Form layout: grids, horizontal forms and input groups
- Validation: feedback for invalid fields
- Basic Inputs demo
Last updated on
PaginationPro
Next.js Bootstrap pagination for the App Router: a pager in a client component that pages a table, with rows passed in from a server component page.
Select
Next.js React Select in AdminKit PRO: a searchable, Bootstrap-styled select with groups and multiple values that renders cleanly on the server.