Forms

Bootstrap · Lesson 15 of 28 6 min read Updated

Forms are where users talk to your website: sign-up, login, admission enquiry, checkout. Browsers draw form fields differently on every device, and plain fields look dated. Bootstrap gives every field the same clean look, a clear focus ring and consistent sizes, while keeping the real HTML form elements underneath.

In this lesson you will style every common form control and arrange them into grid, horizontal and inline layouts. The next two lessons add input groups, floating labels and validation.

Text fields, labels and help text

Each field gets three things: a <label> with form-label, the field with form-control, and optional help text with form-text. Wrap each group in a div with a bottom margin such as mb-3.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Admission enquiry</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container py-3" style="max-width: 560px">
    <h1 class="h4 mb-3">Admission enquiry</h1>
    <form>
      <div class="mb-3">
        <label for="name" class="form-label">Student name</label>
        <input type="text" class="form-control" id="name" autocomplete="name">
      </div>
      <div class="mb-3">
        <label for="email" class="form-label">Parent's email</label>
        <input type="email" class="form-control" id="email" aria-describedby="emailHelp" autocomplete="email">
        <div id="emailHelp" class="form-text">We send the fee structure to this address.</div>
      </div>
      <div class="mb-3">
        <label for="course" class="form-label">Class</label>
        <select class="form-select" id="course">
          <option value="">Choose...</option>
          <option>Class 9</option>
          <option>Class 10</option>
          <option>Class 11 (Science)</option>
          <option>Class 11 (Commerce)</option>
        </select>
      </div>
      <div class="mb-3">
        <label for="message" class="form-label">Questions</label>
        <textarea class="form-control" id="message" rows="3"></textarea>
      </div>
      <fieldset class="mb-3">
        <legend class="col-form-label pt-0">Preferred batch</legend>
        <div class="form-check form-check-inline">
          <input class="form-check-input" type="radio" name="batch" id="morning" value="morning" checked>
          <label class="form-check-label" for="morning">Morning</label>
        </div>
        <div class="form-check form-check-inline">
          <input class="form-check-input" type="radio" name="batch" id="evening" value="evening">
          <label class="form-check-label" for="evening">Evening</label>
        </div>
      </fieldset>
      <div class="form-check mb-3">
        <input class="form-check-input" type="checkbox" id="hostel">
        <label class="form-check-label" for="hostel">Hostel needed</label>
      </div>
      <div class="form-check form-switch mb-3">
        <input class="form-check-input" type="checkbox" role="switch" id="updates" checked>
        <label class="form-check-label" for="updates">Send WhatsApp updates</label>
      </div>
      <button type="submit" class="btn btn-primary">Send enquiry</button>
    </form>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Important details:

  • Every <label> has a for that matches the field's id. Clicking the label focuses the field, and screen readers read the label when the field is focused. A placeholder is not a replacement for a label: it disappears as soon as the user types.
  • aria-describedby="emailHelp" links the help text to the field, so screen readers read it too.
  • <select> uses form-select, not form-control. It gets a custom arrow that matches the design.
  • Checkboxes and radios use a wrapper form-check, the input form-check-input, and the label form-check-label. Add form-check-inline to put them side by side.
  • A related group of radios belongs in a <fieldset> with a <legend>, so the question ("Preferred batch") is read with each option.
  • form-switch turns a checkbox into an on/off switch. Add role="switch" so screen readers announce it as a switch.

Sizes, states and other controls

HTML
<!-- Sizes -->
<input class="form-control form-control-lg" type="text" placeholder="Large">
<input class="form-control form-control-sm" type="text" placeholder="Small">
<select class="form-select form-select-sm">...</select>

<!-- States -->
<input class="form-control" type="text" value="KV Rohtak" readonly>
<input class="form-control" type="text" value="Locked" disabled>
<input class="form-control-plaintext" type="text" value="roll-no-1024" readonly>

<!-- Other controls -->
<input class="form-control" type="file" id="photo" accept="image/*">
<input class="form-range" type="range" min="0" max="10" step="1" id="rating">
<input class="form-control form-control-color" type="color" value="#7952b3">
  • readonly shows a value that cannot be changed but is still submitted. disabled greys the field out, and its value is not submitted.
  • form-control-plaintext displays a read-only value as plain text, without the box.
  • File inputs use form-control too, and form-range styles a slider.
  • To disable a whole section at once, put the fields in <fieldset disabled>.

Form layouts with the grid

Forms use the same grid as everything else. Put row g-3 on the form (or a div) and size each field with column classes. This checkout form has two fields per row on tablets and one per row on phones:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Delivery address</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container py-3">
    <h1 class="h5 mb-3">Delivery address</h1>
    <form class="row g-3">
      <div class="col-md-6">
        <label for="first" class="form-label">First name</label>
        <input type="text" class="form-control" id="first" autocomplete="given-name">
      </div>
      <div class="col-md-6">
        <label for="last" class="form-label">Last name</label>
        <input type="text" class="form-control" id="last" autocomplete="family-name">
      </div>
      <div class="col-12">
        <label for="address" class="form-label">House and street</label>
        <input type="text" class="form-control" id="address" autocomplete="street-address">
      </div>
      <div class="col-md-5">
        <label for="city" class="form-label">City</label>
        <input type="text" class="form-control" id="city" value="Pune">
      </div>
      <div class="col-md-4">
        <label for="state" class="form-label">State</label>
        <select id="state" class="form-select">
          <option>Maharashtra</option>
          <option>Kerala</option>
          <option>Rajasthan</option>
        </select>
      </div>
      <div class="col-md-3">
        <label for="pin" class="form-label">PIN code</label>
        <input type="text" class="form-control" id="pin" inputmode="numeric" maxlength="6">
      </div>
      <div class="col-12">
        <button type="submit" class="btn btn-success">Deliver here</button>
      </div>
    </form>

    <h2 class="h6 mt-5">Horizontal layout</h2>
    <form>
      <div class="row mb-3">
        <label for="phone" class="col-sm-3 col-form-label">Mobile</label>
        <div class="col-sm-9"><input type="tel" class="form-control" id="phone" autocomplete="tel"></div>
      </div>
    </form>

    <h2 class="h6 mt-4">Inline layout</h2>
    <form class="row row-cols-md-auto g-2 align-items-center">
      <div class="col-12">
        <label class="visually-hidden" for="track">Order number</label>
        <input type="text" class="form-control" id="track" placeholder="Order number">
      </div>
      <div class="col-12"><button type="submit" class="btn btn-outline-primary">Track</button></div>
    </form>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Three layouts in one example:

  1. Grid form: row g-3 with col-md-* columns. The PIN field uses inputmode="numeric" so phones show a number keypad.
  2. Horizontal form: each field is its own row, the label is a column with col-form-label (which lines the text up with the input vertically), and the input sits in the remaining columns.
  3. Inline form: row-cols-md-auto makes each column only as wide as its content from md upwards, so the field and button sit on one line. The label is kept for screen readers with visually-hidden.

Common mistakes

  • Placeholder instead of a label. The hint vanishes when typing and many screen readers ignore it. Always add a <label>, hidden with visually-hidden if the design has no room.
  • form-control on a <select>. The arrow looks wrong. Use form-select.
  • Duplicate id values. If two fields share an id, the second label points at the first field.
  • Disabled fields you expect to submit. Disabled values are never sent; use readonly for values that should be sent but not edited.

Key points

  • Use form-label on labels, form-control on inputs and textareas, form-select on selects, and form-text for help text.
  • Checkboxes, radios and switches use form-check, form-check-input and form-check-label; add form-switch and role="switch" for switches.
  • Link every label with for and id, link help text with aria-describedby, and group radios in a <fieldset> with a <legend>.
  • Lay out forms with the grid (row g-3 and col-*), horizontal rows with col-form-label, or inline with row-cols-*-auto.
  • Bootstrap 4's form-group, form-row, custom-select and custom-control no longer exist in Bootstrap 5.

Exercise

Build a registration form for a cricket tournament in Kochi. Include: team name, captain's name and phone number side by side from md upwards, a select for the age group (Under 14, Under 17, Open), a radio group for match timing (Morning or Evening) inside a fieldset, a switch for "We need a practice pitch", a range slider for number of players (11 to 15), and a submit button.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tournament registration</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container py-3">
    <form class="row g-3">
      <div class="col-12">
        <label for="team" class="form-label">Team name</label>
        <input type="text" class="form-control" id="team">
      </div>
      <div class="col-md-6">
        <label for="captain" class="form-label">Captain's name</label>
        <input type="text" class="form-control" id="captain">
      </div>
      <div class="col-md-6">
        <label for="phone" class="form-label">Captain's phone</label>
        <input type="tel" class="form-control" id="phone" inputmode="numeric">
      </div>
      <div class="col-md-6">
        <label for="age" class="form-label">Age group</label>
        <select class="form-select" id="age">
          <option>Under 14</option>
          <option>Under 17</option>
          <option>Open</option>
        </select>
      </div>
      <fieldset class="col-md-6">
        <legend class="col-form-label pt-0">Match timing</legend>
        <div class="form-check form-check-inline">
          <input class="form-check-input" type="radio" name="timing" id="am" value="morning" checked>
          <label class="form-check-label" for="am">Morning</label>
        </div>
        <div class="form-check form-check-inline">
          <input class="form-check-input" type="radio" name="timing" id="pm" value="evening">
          <label class="form-check-label" for="pm">Evening</label>
        </div>
      </fieldset>
      <div class="col-12">
        <div class="form-check form-switch">
          <input class="form-check-input" type="checkbox" role="switch" id="pitch">
          <label class="form-check-label" for="pitch">We need a practice pitch</label>
        </div>
      </div>
      <div class="col-12">
        <label for="players" class="form-label">Number of players</label>
        <input type="range" class="form-range" min="11" max="15" step="1" id="players">
      </div>
      <div class="col-12">
        <button type="submit" class="btn btn-primary">Register team</button>
      </div>
    </form>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

The form itself is a row g-3, so each field is a grid column. The radios share name="timing" inside a fieldset, and the switch is a checkbox with form-switch and role="switch".

Quick Quiz

1. Which class should a <select> element use in Bootstrap 5?

2. What replaces Bootstrap 4's form-group wrapper?

3. A field must show a value and send it with the form, but the user must not change it. Which attribute?

4. Why is a placeholder not a replacement for a label?

Learn Bootstrap with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call