Form field

The building block of every form: label above the control, optional hint below the label, error below the control. The label is always visible — placeholders are examples, never labels.

Hint and error are linked to the control with aria-describedby; an error sets aria-invalid="true". Validate on submit and on blur of a field that already showed an error; show all errors at once in an error summary.

Choose type, inputmode and autocomplete so phones show the right keyboard and browsers can fill in (e.g. email, tel, bday, postal-code, one-time-code).

Examples

Text with autocomplete

Data
render({
  "id": "first-name",
  "label": "Vorname",
  "autocomplete": "given-name",
  "required": true
})
Markup
<div class="field">
<label class="field__label" for="first-name">Vorname</label>
<input class="input" id="first-name" name="first-name" required type="text" autocomplete="given-name">
</div>

With hint and error

An diese Adresse schicken wir die Bestätigung.

Bitte gib eine vollständige E-Mail-Adresse ein, z. B. [email protected].

Data
render({
  "id": "email",
  "label": "E-Mail",
  "type": "email",
  "autocomplete": "email",
  "inputmode": "email",
  "hint": "An diese Adresse schicken wir die Bestätigung.",
  "value": "lea.mueller@",
  "error": "Bitte gib eine vollständige E-Mail-Adresse ein, z. B. [email protected]."
})
Markup
<div class="field">
<label class="field__label" for="email">E-Mail</label>
<p class="field__hint" id="email-hint">An diese Adresse schicken wir die Bestätigung.</p>
<input class="input" id="email" name="email" aria-describedby="email-hint email-error" aria-invalid="true" type="email" value="lea.mueller@" autocomplete="email" inputmode="email">
<p class="field__error" id="email-error"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-circle"/></svg>Bitte gib eine vollständige E-Mail-Adresse ein, z. B. [email protected].</p>
</div>

Select

Data
render({
  "id": "group",
  "label": "Trainingsgruppe",
  "control": "select",
  "value": "kids",
  "options": [
    {
      "value": "tigers",
      "label": "Tigers (4–6)"
    },
    {
      "value": "kids",
      "label": "Kinder 7–10"
    },
    {
      "value": "youth",
      "label": "Jugend 11–15",
      "disabled": true
    }
  ]
})
Markup
<div class="field">
<label class="field__label" for="group">Trainingsgruppe</label>
<select class="select" id="group" name="group"><option value="tigers">Tigers (4–6)</option><option value="kids" selected>Kinder 7–10</option><option value="youth" disabled>Jugend 11–15</option></select>
</div>

Optional textarea

Data
render({
  "id": "note",
  "label": "Bemerkung",
  "control": "textarea",
  "optional": true,
  "rows": 3
})
Markup
<div class="field">
<label class="field__label" for="note">Bemerkung <span class="field__optional">(optional)</span></label>
<textarea class="textarea" id="note" name="note" rows="3"></textarea>
</div>

Data contract

What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.

FieldTypeRequiredDefaultDescription
idstringrequired—Id of the control; also prefix for {id}-hint and {id}-error. Must be unique on the page.
pattern ^[A-Za-z][\w-]*$
labelstringrequired—Visible label ("E-Mail", "Geburtsdatum").
min 1 chars
control"input" | "select" | "textarea"optional"input"Which control to render.
type"text" | "email" | "tel" | "date" | "time" | "number" | "url" | "search" | "password"optional"text"Input type (control = input).
namestringoptional—Form field name; defaults to id.
valuestring | numberoptional—Current value.
placeholderstringoptional—Example value ("z. B. 3011"). Never a substitute for the label.
hintstringoptional—Help text shown between label and control.
errorstringoptional—Error message; when set the control is marked invalid. Say what to do: "Bitte gib … ein".
requiredbooleanoptionalfalseField must be filled. Optional fields are marked "(optional)" instead of marking required ones.
optionalbooleanoptionalfalseAppend "(optional)" to the label (use in forms where most fields are required).
disabledbooleanoptionalfalseRead-only for now; explain why in the hint.
autocompletestringoptional—HTML autocomplete token, e.g. email, given-name, bday, one-time-code.
inputmode"text" | "numeric" | "decimal" | "tel" | "email" | "url" | "search"optional—Virtual keyboard hint, e.g. numeric for PLZ.
maxlengthintegeroptional—Maximum characters.
≥ 1
rowsintegeroptional4Visible lines (control = textarea).
≥ 2
optionsarray of objectsoptional—Choices (control = select). Put a sensible default first or add an empty "Bitte wählen" option.
options[].valuestringrequired—Submitted value.
options[].labelstringrequired—Shown text.
options[].disabledbooleanoptional—Not selectable (e.g. full group).
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "label"
  ],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[A-Za-z][\\w-]*$",
      "description": "Id of the control; also prefix for `{id}-hint` and `{id}-error`. Must be unique on the page."
    },
    "label": {
      "type": "string",
      "minLength": 1,
      "description": "Visible label (\"E-Mail\", \"Geburtsdatum\")."
    },
    "control": {
      "type": "string",
      "enum": [
        "input",
        "select",
        "textarea"
      ],
      "default": "input",
      "description": "Which control to render."
    },
    "type": {
      "type": "string",
      "enum": [
        "text",
        "email",
        "tel",
        "date",
        "time",
        "number",
        "url",
        "search",
        "password"
      ],
      "default": "text",
      "description": "Input type (control = input)."
    },
    "name": {
      "type": "string",
      "description": "Form field name; defaults to `id`."
    },
    "value": {
      "type": [
        "string",
        "number"
      ],
      "description": "Current value."
    },
    "placeholder": {
      "type": "string",
      "description": "Example value (\"z. B. 3011\"). Never a substitute for the label."
    },
    "hint": {
      "type": "string",
      "description": "Help text shown between label and control."
    },
    "error": {
      "type": "string",
      "description": "Error message; when set the control is marked invalid. Say what to do: \"Bitte gib … ein\"."
    },
    "required": {
      "type": "boolean",
      "default": false,
      "description": "Field must be filled. Optional fields are marked \"(optional)\" instead of marking required ones."
    },
    "optional": {
      "type": "boolean",
      "default": false,
      "description": "Append \"(optional)\" to the label (use in forms where most fields are required)."
    },
    "disabled": {
      "type": "boolean",
      "default": false,
      "description": "Read-only for now; explain why in the hint."
    },
    "autocomplete": {
      "type": "string",
      "description": "HTML autocomplete token, e.g. `email`, `given-name`, `bday`, `one-time-code`."
    },
    "inputmode": {
      "type": "string",
      "enum": [
        "text",
        "numeric",
        "decimal",
        "tel",
        "email",
        "url",
        "search"
      ],
      "description": "Virtual keyboard hint, e.g. `numeric` for PLZ."
    },
    "maxlength": {
      "type": "integer",
      "minimum": 1,
      "description": "Maximum characters."
    },
    "rows": {
      "type": "integer",
      "minimum": 2,
      "default": 4,
      "description": "Visible lines (control = textarea)."
    },
    "options": {
      "type": "array",
      "description": "Choices (control = select). Put a sensible default first or add an empty \"Bitte wählen\" option.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "value",
          "label"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "Submitted value."
          },
          "label": {
            "type": "string",
            "description": "Shown text."
          },
          "disabled": {
            "type": "boolean",
            "description": "Not selectable (e.g. full group)."
          }
        }
      }
    }
  }
}

Markup & states

Root: div.field — usable without render() by writing the markup directly.

Attributes

AttributeValuesDefaultDescription
aria-describedby (on control)"{id}-hint {id}-error"—Links hint and error to the control.
aria-invalid (on control)true | false—Error state; drives the red border.

Parts

PartDescription
.field__label<label for> — always visible.
.field__optional"(optional)" suffix inside the label.
.field__hintHelp text (id {id}-hint).
.input / .select / .textareaThe control (16 px text so iOS doesn't zoom).
.field__errorError message with icon (id {id}-error).

States

StateDescription
[aria-invalid="true"]--color-danger border.
:focus-visibleGlobal focus ring (3 px --color-focus, 2 px offset); the border stays.
[readonly]No border, --color-surface-2 fill.
:disabledMuted, not-allowed cursor.

Accessibility

  • Label is a real <label for>; clicking it focuses the control.
  • Errors are announced via aria-describedby; after a failed submit move focus to the error summary.
  • Don't rely on placeholder text; it disappears while typing and often fails contrast.

Guidance

DoGroup related short fields with .form-row.
DoAsk only for data the task needs (especially for minors).
Don'tDon't validate on every keystroke.
Don'tDon't show raw server errors ("422 Unprocessable") — map them to field messages.

Related: Error summary · Checkbox / radio group · Search field