Skip to content

Text fields

components specs/components/text-fields.kmd

Text input control — 2 variants (filled, outlined), single-line and multi-line, with label, helper text, error state, leading/trailing icons, character counter. Material parity (`/components/text-fields`).

When this spec applies

Primary triggers

All triggers

Specification body

Spec — Text fields

Facet Visual do Koder Design. Material parity: https://m3.material.io/components/text-fields.

2 variants

VariantVisualUse
FilledSurface-variant bg + bottom border (accent on focus)Default — denser, more visual weight
OutlinedBorder on all sides + transparent bgForms with grouped fields; cleaner on light bg

Pick one per surface — don't mix in the same form.

Anatomy

[leading icon?]  Label (floating)         [trailing icon?]
                                          [counter?]
┌──────────────────────────────────────────────────────┐
│  [icon?]  Input value here                  [icon?]  │
└──────────────────────────────────────────────────────┘
  Helper text or error message      [chars/max]
  • Container: 56 px height (default), 48 px (compact)
  • Label: body-large floating (above when focused/filled)
  • Input text: body-large
  • Helper / error: body-small below
  • Counter: body-small right-aligned below (when maxLength)
  • Icons: 24 px, leading + trailing optional

R1 — Label behavior

Two label modes:

  1. Floating (default) — label sits inside container at rest; floats up when focused or has value
  2. Always-above — label permanently above the container

Floating is Material standard; always-above is gnome/carbon_ibm preset convention.

R2 — States

Per interaction/states.kmd:

StateFilledOutlined
Empty (rest)Label inside containerLabel inside container
FocusedLabel floats up + bottom border accent (2 px)Label floats up + border accent (2 px)
Filled (has value)Label stays floatedLabel stays floated
HoveredBottom border darkerBorder text-muted darker
ErrorBorder error + helper text becomes error textSame
DisabledOpacity 0.38; no interactionSame

R3 — Single-line vs multi-line

  • Single-line: 56 px height default, scrolls horizontally
  • Multi-line: starts at 56 px, grows to a max (configurable; default max 5 lines), then scrolls vertically inside
  • <input type="text"> vs <textarea> semantics in HTML

R4 — Specialized input types

TypeModifier
Emailinputmode="email", autocomplete=email, validation regex
Numberinputmode="numeric", no spinners (CSS hide), pattern validation
Phoneinputmode="tel", mask per locale
Passwordtype="password" + show/hide eye icon (trailing)
Searchtype="search", ⌘K shortcut hint, clear button (trailing X)
DatePair with date picker (per future date-pickers.kmd)

R5 — Helper text + error

Below the input container, single line:

Filled state:    "Used for sign-in and password reset"   (text-muted)
Error state:     "Email must include @"                  (error color)
Filled + counter:"123/200"                               (text-muted, right)

When error: helper text replaced by error message. Both styled with body-small role. Error must be associated via aria-describedby linking input → error element.

Error format per errors/user-facing-messages.kmd: humanized, specific, no "Invalid" / "Wrong".

R6 — Validation

Validation typeWhen
requiredOn submit, not while typing
Format regexOn blur (after the user finishes typing)
Server-sideOn submit; debounced check on blur for sensitive fields (email exists)
Real-time (length, char restrictions)While typing — give immediate feedback

NEVER validate on first keystroke for format-based rules (annoying; "a" → "Invalid email" before user can type more).

R7 — Leading + trailing icons

  • Leading icon: signals input type (mail icon for email, search icon for search) — decorative, no interaction
  • Trailing icon: actionable (clear X, show/hide password, voice input mic, calendar picker open) — <button> element with aria-label

Tap target on trailing icon: 24 px visible, 40 px hit zone.

R8 — Accessibility

  • <label for="id"> connection MANDATORY (or aria-label/aria-labelledby)
  • Helper text linked via aria-describedby
  • Error linked via aria-describedby + aria-invalid="true" on the input
  • Required fields: aria-required="true" (don't rely on * alone)
  • Autocomplete attribute set for personal data (email, name, address)
  • Screen readers announce label → value → state on focus

R9 — Forbidden patterns

  • ❌ Input without visible label (placeholder is NOT a label)
  • ❌ Placeholder containing format example ("user@example.com") shown only until user types — use helper text instead (or both)
  • ❌ Custom validation messages that contradict spec (errors/user-facing-messages.kmd)
  • ❌ Inputs that look like text but aren't focusable (use a real <button> if action)
  • ❌ Disabling submit until valid (use clear error reveal on submit instead)

R10 — Per-preset variation

PresetVariant default
material3Filled
gnomeOutlined
windows_11Filled
windows_953D bevel border (Outlined visually)
ios_cupertinoOutlined
carbon_ibmUnderline only (lighter than Filled bottom-border)
shadcnOutlined
brutalistOutlined + 3 px thick border
  • interaction/states.kmd — hover/focus/error visuals
  • themes/color-roles.kmd — accent / error / text-muted bindings
  • errors/user-facing-messages.kmd — error message format
  • foundations/ux-writing.kmd — label + helper text style
  • foundations/elements.kmd — Control family

Requirements (testable)

Requirement: Floating label floats on focus or value {#req-text-fields-label-float}

A text field using the floating label mode SHALL keep the label inside the container at rest and SHALL float the label above the container whenever the field is focused or holds a non-empty value, so the label remains visible while the user types.

Scenario: Label floats when the empty field receives focus

  • GIVEN a floating-label text field that is empty and unfocused with its label rendered inside the container
  • WHEN the user focuses the field
  • THEN the label moves to the floated position above the container
  • AND the label remains visible while the input is focused

Scenario: Label stays floated when the field has a value and loses focus

  • GIVEN a floating-label text field that contains the value "hello"
  • WHEN the field loses focus
  • THEN the label stays in the floated position above the container
  • AND the label does not return inside the container

Requirement: Error state marks the input invalid and is announced {#req-text-fields-error-aria}

When a text field is in the error state, it SHALL apply the error border color, SHALL replace the helper text with the error message styled as body-small, SHALL set aria-invalid="true" on the input, and SHALL associate the error element to the input via aria-describedby.

Scenario: Entering the error state exposes the error to assistive tech

  • GIVEN a text field with helper text "Used for sign-in and password reset"
  • WHEN validation fails and the field enters the error state with message "Email must include @"
  • THEN the input carries aria-invalid="true"
  • AND the input's aria-describedby references the element that renders "Email must include @"
  • AND the helper text "Used for sign-in and password reset" is replaced by the error message
  • AND the container border uses the error color role

Requirement: Format validation is deferred, never on first keystroke {#req-text-fields-validation-timing}

A text field SHALL NOT run format-based validation on the first keystroke; format regex validation SHALL run on blur (after the user finishes typing) and required validation SHALL run on submit rather than while typing.

Scenario: Typing one character does not trigger a format error

  • GIVEN an empty email text field with a format regex rule
  • WHEN the user types the single character "a"
  • THEN no error message is shown
  • AND the field is not marked invalid

Scenario: Format is validated on blur

  • GIVEN an email text field containing "a" that has focus
  • WHEN the field loses focus (blur)
  • THEN the format regex validation runs
  • AND the error state is applied because "a" is not a valid email

Requirement: Trailing icons are real actionable controls {#req-text-fields-trailing-icon}

A trailing icon that performs an action (clear, show/hide password, voice input, open picker) SHALL be a <button> element carrying an aria-label, and SHALL expose a hit zone of at least 40 px even though the visible icon is 24 px.

Scenario: Password show/hide trailing icon is a labeled button

  • GIVEN a password text field rendered with a show/hide trailing icon
  • WHEN the trailing icon is inspected in the DOM
  • THEN it is a <button> element
  • AND it has a non-empty aria-label
  • AND its interactive hit zone measures at least 40 px while the visible glyph measures 24 px

Scenario: Activating the trailing button toggles password visibility

  • GIVEN a password text field with type="password" and a show/hide trailing button
  • WHEN the user activates the trailing button
  • THEN the input's type toggles to reveal the entered characters
  • AND activating it again returns the input to type="password"

Requirement: Every field has a programmatic label, never placeholder-as-label {#req-text-fields-accessible-label}

Every text field SHALL be associated with a programmatic label via <label for="id">, aria-label, or aria-labelledby, and SHALL NOT rely on placeholder text as the field's label.

Scenario: Input is connected to a label element

  • GIVEN a text field rendered with id "email" and a visible label "Email"
  • WHEN the field markup is inspected
  • THEN a <label for="email"> (or an aria-label/aria-labelledby on the input) associates the label to the input
  • AND a screen reader focusing the input announces the label text

Scenario: Placeholder alone does not satisfy the label requirement

  • GIVEN a text field that has a placeholder attribute but no <label>, aria-label, or aria-labelledby
  • WHEN the field is validated for accessibility conformance
  • THEN the field is reported as non-conformant
  • AND the placeholder is not treated as the accessible name

References