Form fields
Text fields, selects and text areas with Bootstrap 5's names: a label floating in the field or above it, a hint, an error said in words, an icon inside the field.
Examples
form-floating keeps the label inside the field and lifts it when the field has a value or the focus. The label follows the control, and the control needs a placeholder - it is how CSS knows the field is empty.
<div class="form-floating">
<input class="form-control" id="form-floating-name" value="North Bay: marine park by speedboat" placeholder="A name people search for">
<label for="form-floating-name">Name</label>
</div>
<div class="form-floating">
<input class="form-control" id="form-floating-mail" type="email" placeholder="name@example.com">
<label for="form-floating-mail">E-mail</label>
</div>
<div class="form-floating">
<select class="form-select" id="form-floating-section"><option>Marine</option><option>Land</option><option>Evening</option></select>
<label for="form-floating-section">Section</label>
</div>
<div class="form-floating">
<textarea class="form-control" id="form-floating-note" placeholder="A note for the guides">Pick up at the pier at 8:30.</textarea>
<label for="form-floating-note">Note</label>
</div>A label above, a hint, an error
form-field stacks a field's parts: form-label, the control, form-text for a hint and invalid-feedback for an error. The control points at both with aria-describedby; an error sets aria-invalid="true", and the browser's own :user-invalid draws the same after the person has edited the field.
<div class="form-field">
<label class="form-label" for="form-stacked-slug">Address</label>
<input class="form-control font-monospace" id="form-stacked-slug" value="north-bay" aria-describedby="form-stacked-slug-hint">
<div class="form-text" id="form-stacked-slug-hint">Full address /excursions/north-bay/. The old one answers with a 301 redirect.</div>
</div>
<div class="form-field">
<label class="form-label" for="form-stacked-price">Adult price, THB</label>
<input class="form-control tabular-nums" id="form-stacked-price" type="number" value="2500" min="0" step="100" aria-invalid="true" aria-describedby="form-stacked-price-error">
<div class="invalid-feedback" id="form-stacked-price-error"><i class="bi bi-exclamation-triangle-fill" aria-hidden="true"></i>Lower than the child price</div>
</div>A longer hint
A details with form-more folds what a short hint cannot say. Its chevron turns as it opens.
More
<div class="form-field">
<label class="form-label" for="form-more-keep">Keep the log for, days</label>
<input class="form-control tabular-nums" id="form-more-keep" type="number" value="90" min="1" aria-describedby="form-more-hint">
<div class="form-text" id="form-more-hint">Older records are removed at night.</div>
<details class="form-more form-text"><summary><i class="bi bi-chevron-right" aria-hidden="true"></i>More</summary><div class="mt-4">The count starts from the day a record is written. A record a person marked to keep stays, whatever this number says, until the mark is taken off.</div></details>
</div>An icon in the field
form-adorn puts an icon inside a field or a select, at its start; the field keeps its own name. With form-floating on the same element the label starts after the icon, inside the one border - the way a sign-in form draws its fields.
<div class="form-adorn">
<i class="bi bi-search" aria-hidden="true"></i>
<input class="form-control" type="search" placeholder="Name, slug or code" aria-label="Search excursions">
</div>
<div class="form-floating form-adorn">
<i class="bi bi-envelope" aria-hidden="true"></i>
<input class="form-control" id="form-adorn-mail" type="email" placeholder="name@example.com" autocomplete="email">
<label for="form-adorn-mail">E-mail</label>
</div>
<div class="form-floating form-adorn">
<i class="bi bi-globe2" aria-hidden="true"></i>
<select class="form-select" id="form-adorn-country" autocomplete="country-name"><option>Northland</option><option>Southland</option><option>Eastland</option></select>
<label for="form-adorn-country">Country</label>
</div>
<div class="input-group">
<div class="form-floating form-adorn">
<i class="bi bi-key" aria-hidden="true"></i>
<input class="form-control" id="form-adorn-password" type="password" placeholder="Password" autocomplete="current-password">
<label for="form-adorn-password">Password</label>
</div>
<button class="btn btn-icon" type="button" commandfor="form-adorn-password" command="--reveal" aria-pressed="false" aria-label="Show the password"><i class="bi bi-eye" aria-hidden="true"></i></button>
</div>Sizes
form-control-sm and form-select-sm are 28 px high, the default 32 px, -lg 40 px - the same heights as the buttons beside them.
<input class="form-control form-control-sm" placeholder="Small" aria-label="Small field">
<input class="form-control" placeholder="Default" aria-label="Default field">
<input class="form-control form-control-lg" placeholder="Large" aria-label="Large field">
<select class="form-select form-select-sm" aria-label="Small select"><option>Small select</option></select>A one-time code
form-control-otp spaces the digits of a code from an app or a message in one field. One field takes a paste and the code the system offers above the keyboard - a box per digit takes neither - and the form goes by its button, never by the last digit.
<div class="form-field">
<label class="form-label" for="form-otp-code">6-digit code from your app</label>
<input class="form-control form-control-otp" id="form-otp-code" type="text" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9 \-]*" spellcheck="false" enterkeyhint="done" aria-describedby="form-otp-code-hint">
<div class="form-text" id="form-otp-code-hint">Spaces and dashes are fine.</div>
</div>A new password
A button with command="--reveal" for a password field shows its text and hides it again - aria-pressed says which - and the field hides it on its form's submit and when the page comes back from the history. Each element with data-strength naming the field shows its level as it is typed: shorter than its minlength, long enough, then longer or of three kinds of characters, then both - a meter at once, and the words, the page's texts strength.short, .fair, .good and .strong, once the typing pauses, said politely by a live region of the field's own - VoiceOver says nothing of a password field's own description changing, so the words' element needs no aria-live. The level is a hint: the server keeps its own rules.
<div class="form-field">
<label class="form-label" for="form-password-new">New password</label>
<div class="input-group">
<input class="form-control" id="form-password-new" type="password" autocomplete="new-password" minlength="12" spellcheck="false" aria-describedby="form-password-hint form-password-level">
<button class="btn btn-icon" type="button" commandfor="form-password-new" command="--reveal" aria-pressed="false" aria-label="Show the password"><i class="bi bi-eye" aria-hidden="true"></i></button>
</div>
<meter class="meter" min="0" max="4" low="2" high="3" optimum="4" value="0" data-strength="form-password-new" aria-label="Password strength"></meter>
<div class="form-text" id="form-password-level" data-strength="form-password-new"></div>
<div class="form-text" id="form-password-hint">At least 12 characters. A phrase of a few words is easy to remember.</div>
</div>A range
form-range on an <input type="range"> is the browser's own slider in the theme's accent: it keeps its keys (the arrows, Home, End, Page Up and Page Down) and its look in forced colors. The label says what it sets; when the number matters, write it beside the slider.
<div class="form-field">
<label class="form-label" for="form-range-quality">Photo quality</label>
<input class="form-range" id="form-range-quality" type="range" min="40" max="100" step="5" value="80">
</div>A button beside a floating field
btn-float makes a button as tall as a floating field, for the one action beside it; with btn-icon it is square.
<div class="hstack gap-8">
<div class="form-floating flex-fill">
<input class="form-control" id="form-float-button-code" placeholder="Voucher code">
<label for="form-float-button-code">Voucher code</label>
</div>
<button class="btn btn-secondary btn-float" type="button">Apply</button>
</div>A text that grows
data-grow on a textarea starts it at its rows - rows="1" is one line - and grows it with its text, without the five lines a text area takes otherwise. Where the browser has field-sizing: content - Chrome 123, Firefox 152, Safari 26.2 - CSS does it; below, the library sizes it from its text as it is typed, reset and loaded. A max-h-* caps it, and past the cap it scrolls.
<div class="form-field">
<label class="form-label" for="form-grow-text">Translation</label>
<textarea class="form-control max-h-160" id="form-grow-text" rows="1" data-grow>Speedboat to the marine park</textarea>
</div>A count of characters
form-counter with data-counter naming a field shows how much is typed against its maxlength, or against the counter's data-counter-max, which lets the text go over - a title the search results cut. From nine tenths it turns to the warning tone, past the most to danger (data-state near and over), and once the typing pauses near or over, the count is said politely in the page's text counter. Joined to the field by aria-describedby, the count is read with it.
<div class="form-field">
<label class="form-label" for="form-counter-title">Page title</label>
<input class="form-control" id="form-counter-title" value="North Bay marine park by speedboat" aria-describedby="form-counter-title-count">
<div class="form-counter" id="form-counter-title-count" data-counter="form-counter-title" data-counter-max="60"></div>
</div>Number steps
Buttons with command="--step-up" or "--step-down" and aria-controls naming a number field step it by its step within its min and max, and fire input and change as typing does; at an end a button is aria-disabled. The arrow keys in the field step it already: the buttons are for a pointer.
<div class="form-field">
<label class="form-label" for="form-step-guests">Guests</label>
<div class="input-group max-w-160">
<button class="btn btn-icon" type="button" command="--step-down" aria-controls="form-step-guests" aria-label="One guest fewer"><i class="bi bi-dash-lg" aria-hidden="true"></i></button>
<input class="form-control tabular-nums text-center" id="form-step-guests" type="number" min="1" max="12" value="2">
<button class="btn btn-icon" type="button" command="--step-up" aria-controls="form-step-guests" aria-label="One guest more"><i class="bi bi-plus-lg" aria-hidden="true"></i></button>
</div>
</div>A colour
form-control-color is the browser's own colour picker in a field's box. A text field with data-color-for naming the picker is its hex twin: each follows the other, and a twin that holds no colour - #rgb or #rrggbb, the # optional - is aria-invalid. A stored colour is drawn by data-color, which the library sets as --redz-color through the CSSOM, so a page under a strict CSP writes no style: on a dot, an icon-tile - its icon black or white by the colour's lightness -, a choice-media, and the edges edge-top-color and edge-start-color.
<div class="form-field">
<label class="form-label" for="form-color-hex">Label colour</label>
<div class="input-group max-w-240">
<input class="form-control form-control-color" id="form-color-pick" type="color" value="#2a9d8f" aria-label="Pick the label colour">
<input class="form-control font-monospace" id="form-color-hex" data-color-for="form-color-pick" value="#2a9d8f" spellcheck="false" autocomplete="off">
</div>
</div>
<div class="hstack gap-12">
<span class="dot" data-color="#2a9d8f"></span>
<span class="icon-tile" data-color="#e9c46a"><i class="bi bi-tag" aria-hidden="true"></i></span>
<span class="icon-tile" data-color="#264653"><i class="bi bi-tag" aria-hidden="true"></i></span>
<div class="card edge-start-color" data-color="#e76f51"><div class="card-body">Overdue</div></div>
</div>Sent on a change
data-submit="change" on a form or on one of its controls sends the form when such a control changes - a filter's select, a sort, a switch - by requestSubmit, so the browser's checks and the page's submit listeners run. A text field changes when it is left, not at every key; a combobox's list when a value is chosen.
<form class="hstack flex-wrap gap-12" action="#form-submit" data-submit="change" data-docs-submit>
<select class="form-select max-w-240" name="sort" aria-label="Sort by"><option value="new">Newest first</option><option value="price">Lowest price first</option></select>
<label class="form-check"><input class="check" type="checkbox" name="open"> Open now</label>
<output class="text-muted" data-docs-sent></output>
</form>Shown by another field
data-show-when on a region names a field of its form, and data-show-in the values that show it, a space between them; without it, any value does. A checkbox or a radio counts its value when checked. A hidden field beside a checkbox of the same name is what the form sends while it is off: data-show-in can name its value, and a region without data-show-in does not count it. While the region is hidden its fields are disabled, so the form sends none of them; a field the page disabled itself stays disabled. A region inside a hidden region counts as hidden, whatever its own field holds.
<form class="vstack gap-12">
<div class="form-floating max-w-320">
<select class="form-select" id="form-show-pay" name="pay"><option value="cash">Cash on the day</option><option value="card">Card</option><option value="invoice">Invoice</option></select>
<label for="form-show-pay">Payment</label>
</div>
<div class="form-floating max-w-320" id="form-show-tax-region" data-show-when="pay" data-show-in="invoice">
<input class="form-control" id="form-show-tax" name="tax_id" placeholder="Tax ID">
<label for="form-show-tax">Tax ID</label>
</div>
<div class="vstack gap-4">
<input type="hidden" name="remind" value="0">
<label class="form-check"><input class="switch" type="checkbox" role="switch" name="remind" value="1"> Remind the guest</label>
<div class="form-text" id="form-show-on" data-show-when="remind">A message goes out the evening before.</div>
<div class="form-text" id="form-show-off" data-show-when="remind" data-show-in="0">The guest is told nothing.</div>
</div>
</form>States
| class | rest | :focus | aria-invalid | readonly | disabled |
|---|---|---|---|---|---|
| form-control | |||||
| form-select | - | ||||
| form-floating |
Keyboard and accessibility
- Every control has a visible label: a
<label for>, oraria-labelonly where the context says it all - a search field with its icon. - A placeholder is never the label: it goes away as the person types. In
form-floatingthe placeholder is only the empty marker. - An error is said in words in
invalid-feedback, joined to the control byaria-describedby; a color alone never tells it. - The borders of fields and the focus ring are at least 3:1 against the page in both themes; the text 4.5:1.
form-selectpaints its own chevron from the theme, mirrored right to left; a multiple select or one withsizedrops it.- A count of characters is said only near or over its most, once the typing pauses; at every key it would drown the typing.
- Number step buttons are named by
aria-labeland keep their place in the Tab order; from the keyboard the field's own arrow keys step it. - A region shown by another field is
hiddenwhile it does not apply, so a screen reader skips it and the form sends nothing of it.
Tokens
From css/components/form.css, which declares no custom property of its own.
It reads the theme's --redz-space-4, --redz-text, --redz-fs-12, --redz-text-muted, --redz-link, --redz-motion, --redz-ease, --redz-danger, --redz-control-height, --redz-space-12, --redz-border-strong, --redz-radius-8, --redz-bg, --redz-focus, --redz-space-8, --redz-grow-rows, --redz-form-select-chevron, --redz-warning, --redz-radius-6, --redz-bg-page, --redz-primary-subtle, --redz-control-height-sm, --redz-control-height-lg, --redz-fs-16, --redz-control-height-float, --redz-fs-14, --redz-primary.
From Bootstrap 5.3
| Bootstrap 5.3 | REDZ.UI | Change | Note |
|---|---|---|---|
col-form-label | form-label | review | a form-label in a form-field |
form-control-plaintext | - | review | readonly on a form-control, or plain text |
has-validation | - | remove | fields have no success state |
invalid-tooltip | invalid-feedback | review | the error under the field |
is-invalid | - | state | aria-invalid="true" on the control |
is-valid | - | remove | fields have no success state |
valid-feedback | - | remove | fields have no success state |
valid-tooltip | - | remove | fields have no success state |
was-validated | - | state | the browser's :user-invalid shows after the person edits |