Custom Form Controls
Browsers render checkboxes, radios, selects, and file inputs with their own inconsistent native
styling that's hard to theme with CSS alone. Bootstrap 4 solves this with a set of
custom form control classes that visually hide the native control and rebuild its
appearance from a sibling <label>, while keeping the real input underneath for
accessibility and form submission. We'll use them to style the difficulty select and a public/private
toggle on TrailMap's "Submit a Trail" form.
.custom-select→.form-select.custom-control+.custom-checkbox→ simply.form-check.custom-control+.custom-radio→ simply.form-check(withtype="radio").custom-switch→.form-switch.custom-file→ dropped; a plain.form-controlnow styles<input type="file">
.form-check needs far
fewer wrapper elements than Bootstrap 4's .custom-control. Every example below is the
Bootstrap 4 structure; don't copy this markup into a Bootstrap 5 project.Custom Checkboxes: .custom-control / .custom-checkbox
Every custom checkbox needs this exact nested structure: a wrapper with .custom-control .custom-checkbox, an <input> with .custom-control-input, and a <label> with .custom-control-label whose for attribute matches the input's id. The label's ::before/::after pseudo-elements draw the visible box and checkmark.
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="agreeTerms">
<label class="custom-control-label" for="agreeTerms">I agree to the terms & conditions</label>
</div>
Custom Radios: .custom-control / .custom-radio
Same nested structure as checkboxes, swapping .custom-checkbox for .custom-radio and type="radio" on the input. Radios sharing a group need the same name attribute.
<div class="custom-control custom-radio">
<input type="radio" name="sortOrder" class="custom-control-input" id="sortDistance">
<label class="custom-control-label" for="sortDistance">Sort by distance</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" name="sortOrder" class="custom-control-input" id="sortRating">
<label class="custom-control-label" for="sortRating">Sort by rating</label>
</div>
Custom Select: .custom-select
.custom-select goes directly on the <select> element itself — no wrapper or extra label markup needed. It replaces the browser's native dropdown arrow with a consistent custom-drawn one.
<select class="custom-select" id="trailDifficulty">
<option value="">Choose difficulty...</option>
<option value="easy">Easy</option>
<option value="moderate">Moderate</option>
<option value="hard">Hard</option>
</select>
Custom Switch: .custom-switch
.custom-switch is a small addition on top of the checkbox structure — add it alongside .custom-control instead of .custom-checkbox, and the label renders as a sliding toggle switch rather than a square box.
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="trailPublic" checked>
<label class="custom-control-label" for="trailPublic">Make this trail public</label>
</div>
Custom File Input: .custom-file
.custom-file wraps a hidden native <input type="file"> (with .custom-file-input) plus a .custom-file-label that displays either placeholder text or the chosen filename via a small script listening for the change event.
<div class="custom-file">
<input type="file" class="custom-file-input" id="trailPhoto" accept="image/*">
<label class="custom-file-label" for="trailPhoto">Choose a photo...</label>
</div>
Worked Example: TrailMap "Submit a Trail" Form
Here's the custom-styled portion of TrailMap's trail submission form — a .custom-select for difficulty, a .custom-switch for visibility, a .custom-file for the trail photo, and a .custom-checkbox for accepting the terms.
<!-- TrailMap: Submit a Trail — custom controls -->
<div class="form-group">
<label for="trailDifficulty">Difficulty</label>
<select class="custom-select" id="trailDifficulty" name="difficulty">
<option value="easy">Easy</option>
<option value="moderate">Moderate</option>
<option value="hard">Hard</option>
</select>
</div>
<div class="custom-file mb-3">
<input type="file" class="custom-file-input" id="trailPhoto" name="photo" accept="image/*">
<label class="custom-file-label" for="trailPhoto">Choose a trail photo...</label>
</div>
<div class="custom-control custom-switch mb-3">
<input type="checkbox" class="custom-control-input" id="trailPublic" name="is_public" checked>
<label class="custom-control-label" for="trailPublic">Make this trail public</label>
</div>
<div class="custom-control custom-checkbox mb-3">
<input type="checkbox" class="custom-control-input" id="agreeTerms" name="agree_terms" required>
<label class="custom-control-label" for="agreeTerms">I agree to TrailMap's submission terms</label>
</div>
<button type="submit" class="btn btn-primary">Submit Trail</button>