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
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
.checkbox-input