AdminKitDocs
Forms

Next.js Bootstrap Form Layout

Next.js Bootstrap form layout for the App Router: grid, horizontal and inline forms and input groups as server-rendered markup, plus a client Dropdown.

The layout decides how a form reads: fields stacked, side by side, or with the labels to the left. In AdminKit for Next.js, you build it with Bootstrap's grid and spacing classes in JSX. This works the same in the free and PRO versions.

In the App Router, pages are server components: they render on the server and have no state. Layout markup has no state either, so it renders in a server component page with no "use client" needed. The PRO demo's Form Layouts and Input Groups pages are both server components.

Only the parts with behavior are client components, files that start with "use client". One is the Dropdown of an input group (below). The other is the submit handler of a form. You write it in a client component because a static export has no Server Actions (see Form controls).

Example

Stack each label and field in a div with mb-3:

<form>
  <div className="mb-3">
    <label className="form-label" htmlFor="layoutEmail">Email address</label>
    <input type="email" className="form-control" id="layoutEmail" placeholder="Email" />
  </div>
  <div className="mb-3">
    <label className="form-label" htmlFor="layoutPassword">Password</label>
    <input type="password" className="form-control" id="layoutPassword" placeholder="Password" />
  </div>
  <div className="mb-3">
    <label className="form-check m-0">
      <input type="checkbox" className="form-check-input" />
      <span className="form-check-label">Check me out</span>
    </label>
  </div>
  <button type="submit" className="btn btn-primary">Submit</button>
</form>

Form grid

Fields that belong together can share a row. A row with col-md-* columns puts them next to each other from md up, and g-3 adds the gutters.

<form className="row g-3">
  <div className="col-md-6">
    <label className="form-label" htmlFor="gridCity">City</label>
    <input type="text" className="form-control" id="gridCity" />
  </div>
  <div className="col-md-4">
    <label className="form-label" htmlFor="gridState">State</label>
    <select className="form-select" id="gridState" defaultValue="Choose...">
      <option>Choose...</option>
      <option>California</option>
    </select>
  </div>
  <div className="col-md-2">
    <label className="form-label" htmlFor="gridZip">Zip</label>
    <input type="text" className="form-control" id="gridZip" />
  </div>
</form>

Horizontal form

For labels to the left of their fields, make every field a row. The label gets col-form-label col-sm-2 text-sm-end, and col-sm-10 goes around the field.

<form>
  <div className="mb-3 row">
    <label className="col-form-label col-sm-2 text-sm-end" htmlFor="horizontalEmail">Email</label>
    <div className="col-sm-10">
      <input type="email" className="form-control" id="horizontalEmail" placeholder="Email" />
    </div>
  </div>
  <div className="mb-3 row">
    <div className="col-sm-10 ms-sm-auto">
      <button type="submit" className="btn btn-primary">Submit</button>
    </div>
  </div>
</form>

Inline form

To fit a few fields on a single line, use row-cols-md-auto. Each column is then as wide as its content, so the fields share a line from md up. The labels stay in the markup with visually-hidden.

<form className="row row-cols-md-auto align-items-center">
  <div className="col-12">
    <label className="visually-hidden" htmlFor="inlineName">Name</label>
    <input type="text" className="form-control mb-2 me-sm-2" id="inlineName" placeholder="Jane Doe" />
  </div>
  <div className="col-12">
    <label className="visually-hidden" htmlFor="inlineUsername">Username</label>
    <div className="input-group mb-2 me-sm-2">
      <div className="input-group-text">@</div>
      <input type="text" className="form-control" id="inlineUsername" placeholder="Username" />
    </div>
  </div>
  <div className="col-12">
    <button type="submit" className="btn btn-primary mb-2">Submit</button>
  </div>
</form>

Input groups

When a field needs a unit, a prefix or a button at its side, use an input group. Put the field in an .input-group, next to .input-group-text or buttons. input-group-lg and input-group-sm resize it. For a dropdown button in the group, use the Dropdown client component with className="input-group". The server component page can render it directly, because its children are plain markup:

src/app/(dashboard)/orders/page.tsx
import { Dropdown, DropdownMenu, DropdownToggle } from "@/components/bootstrap/dropdown";
import { pageMetadata } from "@/lib/site";

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

export default function OrdersPage() {
  return (
    <>
      <h1 className="h3 mb-3">Orders</h1>
      <div className="input-group mb-3">
        <span className="input-group-text">$</span>
        <input type="text" className="form-control" aria-label="Amount (to the nearest dollar)" />
        <span className="input-group-text">.00</span>
      </div>
      <Dropdown className="input-group">
        <DropdownToggle as="button" className="btn btn-secondary dropdown-toggle">
          Action
        </DropdownToggle>
        <DropdownMenu>
          <a className="dropdown-item" href="#">Edit</a>
          <a className="dropdown-item" href="#">Archive</a>
        </DropdownMenu>
        <input type="text" className="form-control" aria-label="Text input with dropdown button" />
      </Dropdown>
    </>
  );
}

List /orders in src/data/pages.json, where pageMetadata() reads the page's title and description. DropdownToggle with as="button" renders type="button", so it never submits the form. Dropdowns lists its props.

Accessibility

  • Give every field a label connected with htmlFor and id, in horizontal forms too.
  • Hide a label with visually-hidden instead of removing it, as the inline form does.
  • Wrap a group of radios or checkboxes in a <fieldset> with a <legend>. In a horizontal form, the legend takes col-form-label.
  • .input-group-text does not label the field. Add a label or aria-label.

Last updated on

On this page