Skip to content

Text Field component

FormFieldSet is one component that dispatches to 12 different field implementations based on its type prop — text, number, email, password, select, radio, checkbox, switch, date picker, phone, file, and more — sharing one validation system and one Form context, so every field type is styled, labeled, and validated consistently without hand-building 12 separate wrappers.

Forms31 propsAccessibility notes

Playground

Change a prop on the right and the preview updates. Switch to Code to copy what you see.

Recipes

Copy-paste patterns for the ways Text Field actually gets used — each one a real, self-contained example.

Basic text field

A labelled input with helper text. Wrap fields in a Form so their values are collected and validated together.

<Form onSubmit={(data) => console.log(data)}>
  <FormFieldSet
    type="text"
    name="fullName"
    label="Full name"
    placeholder="Ada Lovelace"
    description="As it appears on your ID."
  />
</Form>

Leading & trailing icons

startIcon and endIcon sit inside the control — a search glyph, a status tick — without disturbing the input's own padding.

<FormFieldSet
  type="search"
  name="q"
  label="Search"
  placeholder="Search customers…"
  startIcon={<SearchIcon />}
/>

Prefix & suffix affixes

prefix and suffix render attached segments — a currency symbol, a unit, a fixed URL base — that are visually part of the field but never part of its value.

<FormFieldSet
  type="number"
  name="price"
  label="Price"
  prefix="$"
  suffix="USD"
  placeholder="0.00"
/>
<FormFieldSet
  type="text"
  name="site"
  label="Website"
  prefix="https://"
/>

Declarative validation

validation.rules covers the common cases — required, email, length, pattern — and Form shows the error per-field once it's touched.

<Form onSubmit={(data) => console.log(data)}>
  <FormFieldSet
    type="email"
    name="email"
    label="Work email"
    validation={{ rules: { required: true, email: true } }}
  />
  <Button type="submit" color="primary">Continue</Button>
</Form>

Floating label

labelPlacement="floating" starts the label inside the control and animates it up on focus or value — a tighter footprint that still keeps a real, associated label.

<FormFieldSet
  type="text"
  name="company"
  label="Company"
  labelPlacement="floating"
/>

Visual variants

Four surfaces for different densities — bordered by default, soft and filled for grouped forms, underlined for a minimal look.

<FormFieldSet type="text" name="a" label="Bordered" variant="bordered" />
<FormFieldSet type="text" name="b" label="Soft" variant="soft" />
<FormFieldSet type="text" name="c" label="Filled" variant="filled" />
<FormFieldSet type="text" name="d" label="Underlined" variant="underlined" />

One field, twelve types

type picks the field implementation; every other prop after that is shared (label, description, size, variant, color, labelPlacement) plus whichever props are specific to that type (options for select/radio/checkbox, length for otp, accept for file). FormFieldSet must live inside a Form for validation/submission to work — it registers itself with the nearest Form context on mount.

<Form onSubmit={(data) => console.log(data)}>
  <FormFieldSet type="text" name="name" label="Name" />
  <FormFieldSet type="email" name="email" label="Email" validation={{ rules: { required: true, email: true } }} />
  <FormFieldSet
    type="select"
    name="role"
    label="Role"
    options={[
      { label: "Admin", value: "admin" },
      { label: "Editor", value: "editor" },
      { label: "Viewer", value: "viewer" },
    ]}
  />
  <FormFieldSet type="switch" name="notifications" label="Email notifications" />
  <Button type="submit">Save</Button>
</Form>

Validation

validation.rules covers the common cases declaratively — required, email, minLength/maxLength, min/max, pattern, and a custom function rule for anything bespoke. Errors are computed by Form and shown per-field automatically once a field is touched (or immediately, depending on Form's showErrorsOn setting).

<FormFieldSet
  type="password"
  name="password"
  label="Password"
  validation={{
    rules: {
      required: true,
      minLength: 8,
      custom: (value) => /[A-Z]/.test(String(value)) || "Needs at least one uppercase letter",
    },
  }}
/>

Controlled and uncontrolled

Every field type works both ways: pass value + onChange for a fully controlled field, or defaultValue and let Form track the value internally through its own formData state — the same convention across all 12 types, so switching a field between controlled and uncontrolled doesn't change how you read its value elsewhere (Form's formData always reflects the current value either way).

Label placement

labelPlacement controls where the label sits relative to the control — top (the default), left, right, or floating (the label starts inside the control and animates up once it has a value or focus). All 12 field types support the full set.

<FormFieldSet type="text" name="company" label="Company" labelPlacement="floating" />
<FormFieldSet type="text" name="role" label="Role" labelPlacement="left" />

Repeating fields with LoopFields

For a variable-length group of fields (a list of email addresses, multiple phone numbers), LoopFields handles the add/remove/reindex bookkeeping — each item gets a stable identity across removals so validation state and values stay attached to the right item, not just the right position.

<LoopFields name="emails" minItems={1}>
  {({ index, name }) => (
    <FormFieldSet type="email" name={`${name}`} label={`Email ${index + 1}`} />
  )}
</LoopFields>

Props

The full public API for Text Field. Every prop is stable and intentionally typed.

TextField — Content

type

"text" | "number" | "url" | "search" | "color" | "email"

default: "text"

Native input type -- all six render through TextField.

placeholder

string

default: —

Placeholder text.

startIcon

ReactNode

default: —

Leading icon.

endIcon

ReactNode

default: —

Trailing icon.

prefix

ReactNode

default: —

Leading affix (text or custom JSX).

suffix

ReactNode

default: —

Trailing affix (text or custom JSX).

label

ReactNode

default: —

Field label.

description

ReactNode

default: —

Helper text below the control.

error

ReactNode | boolean

default: —

Error message, or true for an invalid state with no message.

TextField — Events

onChange

(value: string) => void

default: —

Called with the input's resolved string value, not a raw ChangeEvent.

TextField — Appearance

size

"xs" | "xs-sm" | "sm" | "md" | "lg" | "xl"

default: "md"

Control size.

variant

"bordered" | "soft" | "filled" | "underlined"

default: "bordered"

Visual variant. "filled" (text types) is borderless with a neutral fill at rest, border + ring on focus; other field types fall back to "bordered". "default"/"underline" are legacy aliases.

color

"default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger" | string

default: "default"

Semantic tone.

colorDepth

number

default: 500

Shade depth for color.

radius

"none" | "xs" | "xs-sm" | "sm" | "md" | "lg" | "full"

default: "md"

Corner radius. `rounded` is a legacy alias.

TextField — Layout & positioning

labelPlacement

"top" | "left" | "right" | "floating"

default: "top"

Label position relative to the control.

colSpan

number

default: —

Sets `grid-column: span N` on the field wrapper — a real home for a layout hint in a spread config object (instead of leaking to the <input>).

TextField — Styling

wrapperClassName

string

default: —

Class on the field wrapper (the element that also carries colSpan), separate from `className` on the control.

className

string

default: —

Class applied to the root element.

style

CSSProperties

default: —

Inline styles for the root element.

TextField — Behavior & states

disabled

boolean

default: false

Disables the control.

required

boolean

default: false

Marks the field required.

Form — Content

children

ReactNode

default: —

FormFieldSet fields and other form content.

Form — Behavior & states

validationMode

"blur" | "change"

default: "blur"

When fields are (re-)validated.

showErrorsOn

"blur" | "change"

default: "blur"

When validation errors start showing.

validateOnMount

boolean

default: false

Runs validation immediately on mount.

initialValues

FormData

default: {}

Initial values for uncontrolled fields.

disableAutofill

boolean

default: true

Adds autocomplete/autofill-defeating attributes to the form.

Form — Events

onSubmit

(formData: FormData) => void | Promise<void>

default: —

Called with collected field values on submit (required).

onError

(errors: FormErrors) => void

default: —

Called when submit is blocked by validation errors.

Form — Styling

className

string

default: ""

Class applied to the form element.

Accessibility

  • Every field type renders a real, correctly-associated <label> (or aria-label for labelPlacement="floating" edge cases) tied to its control via htmlFor/id, generated automatically when you don't pass an explicit id.
  • Validation errors render with role="alert" and are linked to their field via aria-describedby, so a screen reader announces the error in context with the field it belongs to.
  • labelPlacement="floating" keeps a real, associated label at all times — it's a visual animation, not a placeholder-only pattern that would leave the field unlabeled once the placeholder text disappears.

Best practices

  • Group related fields under one Form even when you only care about validating a subset at a time (as SteppedForm does internally) — Form's full-schema validation on submit is what catches anything missed by earlier per-step or per-field checks.
  • Prefer declarative validation.rules over a custom function whenever the built-in rules cover the case — they're what Form's error-timing logic (showErrorsOn, validateOnMount) is built around, and stay consistent across every field type.
  • Use LoopFields instead of manually managing an array of FormFieldSets in your own state — the reindexing-on-removal behavior is easy to get subtly wrong by hand (stale keys, misattributed errors after a middle item is removed).
  • Match labelPlacement to information density: "top" for typical forms, "left"/"right" for compact settings-style rows, "floating" for a tighter visual footprint where a persistent top label would waste vertical space.