Forms
Form field stack: .form-group wraps one field (label + control + help/error text) and provides vertical rhythm. Controls are subcomponents with their own roots: .form-control (inputs/textareas), .form-select, .form-check (checkbox/radio rows), and .toggle switches.
Root: .form-group on <div>, <fieldset>
Example
<div class="form-group"><label class="form-label" for="summary">Summary <span class="required">*</span></label><input class="form-control" id="summary" type="text" required><span class="form-text">Keep it short.</span></div>
Contract
Structure
| Selector | Required | Description |
|---|---|---|
.form-label | optional | A <label class="form-label" for="..."> above the control; append <span class="required">*</span> for mandatory fields. |
.form-text | optional | Help text below the control. |
.invalid-feedback | optional | Error text; pair with .is-invalid on the control. |
Accessibility
| On | Attribute | Level | When |
|---|---|---|---|
.form-label | for | recommended | Always when the label is not wrapping its control — every control needs a programmatically associated label. |
Associate every control with a label via for/id (the demo does this throughout). Mark invalid controls with .is-invalid AND aria-invalid="true", and link the .invalid-feedback text with aria-describedby. The .required asterisk is visual; also set the required attribute on the control.
Nesting
Allowed in:
<form>.modal-body.card-bodysettings panels
Never inside (or containing):
.form-group
Subcomponents
Form-control
Text-style input skin for <input> and <textarea>. Textareas grow (min-height 80px, resize vertical). .form-control-subtle is invisible until hovered/focused (inline edit); .is-invalid shows the danger border.
Root: .form-control on <input>, <textarea>
Examples
<input class="form-control" id="summary" type="text" placeholder="What needs doing?">
<textarea class="form-control" id="desc"></textarea>
Variants & modifiers
| Class | Axis | Rules |
|---|---|---|
form-control-compact | modifier | optional |
form-control-subtle | modifier | optional |
is-invalid | modifier | optional |
Accessibility
| On | Attribute | Level | When |
|---|---|---|---|
.form-control.is-invalid | aria-invalid | recommended | Recommended whenever .is-invalid is applied, together with aria-describedby pointing at the .invalid-feedback element. |
Must have an associated <label> (for/id), or aria-label when a visible label is genuinely impossible. Placeholder text is not a label.
Nesting
Allowed in:
.form-group
Form-select
Native <select> skinned with a chevron. Same sizing/interaction states as .form-control.
Root: .form-select on <select>
Example
<select class="form-select" id="type"><option>Story</option><option>Bug</option></select>
Accessibility
Must have an associated <label>. Prefer native select over custom dropdowns for form input — it is free keyboard/AT support.
Nesting
Allowed in:
.form-group
Form-check
One checkbox or radio row: an .form-check-input followed by its text <label>. Stack rows inside a .form-group; radios in one group share a name attribute.
Root: .form-check on <div>
Example
<div class="form-check"><input class="form-check-input" type="checkbox" id="watch" checked><label for="watch">Notify watchers</label></div>
Structure
| Selector | Required | Description |
|---|---|---|
.form-check-input | required | An <input type="checkbox"> or <input type="radio"> with class form-check-input (custom-drawn box/dot; 2px top margin aligns it with the label's first line). |
label | optional | Text label with for pointing at the input's id. Not a CSS dependency, but a control without one has no accessible name. |
Accessibility
Always pair the input with a label via for/id so the text is clickable and the control is named. The custom appearance keeps the native input element, so keyboard and AT behavior are native.
Nesting
Allowed in:
.form-group
Never inside (or containing):
.form-check
Toggle
iOS-style switch: a <label class="toggle"> wrapping a visually-hidden checkbox and a .toggle-slider span. The wrapping label makes the whole switch clickable; .toggle-lg is the larger size.
Root: .toggle on <label>
Example
<label class="toggle"><input type="checkbox" aria-label="Send me updates" checked><span class="toggle-slider"></span></label>
Variants & modifiers
| Class | Axis | Rules |
|---|---|---|
toggle-lg | modifier | optional |
Structure
| Selector | Required | Description |
|---|---|---|
input | required | A visually-hidden <input type="checkbox"> that must come IMMEDIATELY BEFORE .toggle-slider (the CSS drives the slider via input:checked + .toggle-slider). |
.toggle-slider | required | The drawn track/knob span, immediate next sibling of the input. |
Accessibility
The wrapping <label> gives click-anywhere behavior, but the checkbox still needs a text name: put visible text inside the label, or aria-label on the input (the demo places adjacent text outside the label, which does NOT name the control).
Nesting
Allowed in:
.form-groupsettings rows
Never inside (or containing):
.toggle