Form Inputs

← Back to components

Overview

Every field the kit ships: text fields and their states, dropdowns, the search group, the copy group, radio buttons and checkboxes. They share one field style — .form-input — and the composed variants change only what their job requires, so a field is recognisably the same control wherever it appears.

Two of the demos need JS: initLabelStateSync() mirrors a control's disabled and invalid state onto a label in a different parent, and initCopyButton() drives the copy group. The rest is CSS alone.

Forms

Account Settings

your display name across MarketData

Please enter a valid email address.

contact support to regenerate

empty + disabled — placeholder is suppressed so the field reads as empty rather than filled

assigned at signup, included in form submissions

contact support to change billing region

the same .form-input as every field above

Multi-line fields

There is no .form-textarea. A <textarea> takes .form-input and the element it lands on decides the rest, so a multi-line field cannot drift away from the single-line ones beside it. Height is rows, the native lever.

Three things change by element. It resizes vertically — not the browser default both, because dragging horizontally breaks the form's column; override with resize-none. It drops the read-only not-allowed cursor, which is wrong over text a reader is still scrolling and selecting, while keeping the read-only fill. And it drops Chrome's autofill repaint, which a textarea never triggers.

resizes vertically

resize-none opts out

greyed, but readable

placeholder hidden, as on any field

Fields with a leading icon

.form-input-group plus a .form-icon-* class, both on the wrapper. There is no icon markup: the glyph is a mask-image on the wrapper's own ::before, so there is no <svg> to paste and no aria-hidden to remember.

The field's leading padding is not the consumer's decision either. The icon class sets --form-input-ps and .form-input reads it, so the room for the glyph arrives as an inherited value rather than as a ps-9 competing with p-2.5 at the same specificity.

A bare .form-input-group with no icon class is just a relative wrapper — the pseudo-element paints no mask and, being absolutely positioned, takes no space, so the field keeps its normal padding. The group works on a <select> and a <textarea> too; on a textarea the glyph sits against the first line.

For a glyph the kit does not ship, set the pair inline on the wrapper: style="--form-icon: url(…); --form-input-ps: calc(var(--spacing) * 9)". They are one decision — the glyph without the padding lands on top of the text, and the padding without the glyph leaves a gap.

Cross-container label state sync

When a <label for="X"> sits in a different parent from #X (e.g. a two-column form grid where labels and inputs live in separate cells), CSS alone can't gray the label when the input is disabled or color it red when the input is aria-invalid="true" — there's no selector that compares a label's for against another element's id. Importing @marketdataapp/ui/label-state-sync and calling initLabelStateSync() reflects each control's state onto its label via disabled and error attributes, which the CSS picks up. Re-syncs automatically as inputs flip or DOM mutates.

The checkbox rows use .form-checkbox-label — a companion-label utility for checkboxes/radios whose input lives in a sibling parent. Unlike .form-label it drops the mb-2 and font-medium stacked-field affordances, and bakes in its own &[disabled] rule so consumer overrides at higher selector specificity can't strip the disabled styling.

data-state-for override for broken for markup

When a <label for="X"> points at an id that doesn't exist — or points at a wrapper around the real controls — add data-state-for="<id> [<id> …]" to the label. The override replaces for as the source for state mirroring (the native for keeps its click-to-focus role even if the id is wrong). Multiple ids combine with ANY semantics: if any listed target is disabled / invalid / focused, the label gets the matching attribute. Common case: amember's swap-input pattern where a <select> and fallback <input> share a single label and toggle visibility with display: none.

Form Classes

.form-container .form-heading .form-label .form-checkbox-label .form-input .form-input-disabled .form-input-error .form-dropdown-input .form-helper-text .form-helper-text-error label[disabled] label[error] data-state-for @marketdataapp/ui/label-state-sync

Search Input

A text field with a submit button joined to its trailing edge, the two reading as one control. The field composes .form-input, so a search field and a plain field cannot drift apart — it changes only the edge it shares with the button, its boundary contrast, and the decorations Chromium paints inside a type="search" field. Put .search-input-group on the <form>, wrap it in <search role="search">, and give the field an .sr-only label.

Icon and label

.search-input-button-icon. The glyph is a mask-image on ::before, the way .copy-icon carries its clipboard — the SVG lives in CSS, so there is none to paste and it follows currentColor on its own.

Icon only

.search-input-button-compact. Same markup, same label — the class collapses the label's glyphs and tightens the side padding. The text stays in the DOM, so it is still the button's accessible name and there is no aria-label to remember. Nothing here is marked aria-hidden either: a pseudo-element is already decorative to assistive technology, which an inline <svg> is not.

On a raised panel

Where the group actually lives: a breadcrumb bar, which is a panel rather than the page. The field's surface colours mirror Flowbite's own search input — bg-neutral-secondary-medium and border-default-medium, both theme-aware on their own.

Switch to dark mode here and one consequence is visible: that fill is gray-800, which is card-surface's own colour, so on a panel the field has no edge. On the page it separates from the ground and reads as intended.

The boundary measures 1.18:1 in light and 1.42:1 in dark against its own fill, under the 3:1 WCAG 1.4.11 asks of a UI component's boundary. That is settled rather than outstanding: the kit follows Flowbite's design system, including here, and issue #45 is closed as won't-fix. A version clearing 3:1 shipped first and was rejected on appearance — it read as a bright rim beside Flowbite's.

.search-input-group .search-input .search-input-button .search-input-button-icon .search-input-button-compact

Copy Input

Readonly input with an overlaid copy-to-clipboard button. Click the button to copy the input's value; success state resets after 2 seconds. Multiple instances on a page operate independently. Pass data-copy-value on the button to copy a value other than the visible text.

Icon-only variant

Tighter footprint — just the icon, with a tooltip on hover. Pair .copy-input-compact with .copy-input-icon-button.

initCopyButton() .copy-input-group .copy-input .copy-input-compact .copy-input-button .copy-input-action .copy-input-icon-button .copy-input-tooltip

Radio Group

Standalone

Radio group on the page background. Uses default contrast tiers.

Select a data plan:

Inside a Form

When nested inside .form-container, backgrounds automatically shift one tier deeper for proper contrast.

Create Account

Select a data plan:

Simple Radio List

Standalone radio buttons without a container, for inline use.

Notification preference:

Inline Radio Buttons

Horizontal radio buttons for compact yes/no or short-option questions.

Enable notifications?
Preferred contact method:

Disabled

Add the disabled attribute — the border lightens, the label dims, and cursor-not-allowed is applied automatically.

.radio-group-container .radio-group-item .radio-button-input .radio-button-helper

Checkboxes

Default

The standard Flowbite checkbox using brand-soft focus ring with full dark-mode support.

Disabled

Add the disabled attribute — the border lightens, the sibling label dims, and cursor-not-allowed is applied automatically.

Inside a Form

Checkboxes work the same inside a .form-container.

Notification Settings

Send me email about:
.checkbox-input