Formsstable.field
Form field
A labelled input, select or textarea with optional hint and error message.
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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | required | — | Id of the control; also prefix for {id}-hint and {id}-error. Must be unique on the page.pattern ^[A-Za-z][\w-]*$ |
label | string | required | — | 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). |
name | string | optional | — | Form field name; defaults to id. |
value | string | number | optional | — | Current value. |
placeholder | string | optional | — | Example value ("z. B. 3011"). Never a substitute for the label. |
hint | string | optional | — | Help text shown between label and control. |
error | string | optional | — | Error message; when set the control is marked invalid. Say what to do: "Bitte gib … ein". |
required | boolean | optional | false | Field must be filled. Optional fields are marked "(optional)" instead of marking required ones. |
optional | boolean | optional | false | Append "(optional)" to the label (use in forms where most fields are required). |
disabled | boolean | optional | false | Read-only for now; explain why in the hint. |
autocomplete | string | optional | — | 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. |
maxlength | integer | optional | — | Maximum characters. ≥ 1 |
rows | integer | optional | 4 | Visible lines (control = textarea). ≥ 2 |
options | array of objects | optional | — | Choices (control = select). Put a sensible default first or add an empty "Bitte wählen" option. |
options[].value | string | required | — | Submitted value. |
options[].label | string | required | — | Shown text. |
options[].disabled | boolean | optional | — | 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
| Attribute | Values | Default | Description |
|---|---|---|---|
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
| Part | Description |
|---|---|
.field__label | <label for> — always visible. |
.field__optional | "(optional)" suffix inside the label. |
.field__hint | Help text (id {id}-hint). |
.input / .select / .textarea | The control (16 px text so iOS doesn't zoom). |
.field__error | Error message with icon (id {id}-error). |
States
| State | Description |
|---|---|
[aria-invalid="true"] | --color-danger border. |
:focus-visible | Global focus ring (3 px --color-focus, 2 px offset); the border stays. |
[readonly] | No border, --color-surface-2 fill. |
:disabled | Muted, 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
.form-row.Related: Error summary · Checkbox / radio group · Search field