RZREDZ.UI

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.

Full address /excursions/north-bay/. The old one answers with a 301 redirect.
Lower than the child price
<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.

Older records are removed at night.
More
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 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.

Spaces and dashes are fine.
<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.

At least 12 characters. A phrase of a few words is easy to remember.
<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.

Overdue
<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.

A message goes out the evening before.
The guest is told nothing.
<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

classrest:focusaria-invalidreadonlydisabled
form-control
form-select -
form-floating

Keyboard and accessibility

  • Every control has a visible label: a <label for>, or aria-label only 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-floating the placeholder is only the empty marker.
  • An error is said in words in invalid-feedback, joined to the control by aria-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-select paints its own chevron from the theme, mirrored right to left; a multiple select or one with size drops 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-label and 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 hidden while 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.3REDZ.UIChangeNote
col-form-labelform-labelreviewa form-label in a form-field
form-control-plaintext-reviewreadonly on a form-control, or plain text
has-validation-removefields have no success state
invalid-tooltipinvalid-feedbackreviewthe error under the field
is-invalid-statearia-invalid="true" on the control
is-valid-removefields have no success state
valid-feedback-removefields have no success state
valid-tooltip-removefields have no success state
was-validated-statethe browser's :user-invalid shows after the person edits