Field

Field forms Primitive

The label + control + help + error triad as one accessible unit. Error state derives from aria-invalid; help/error bind via aria-describedby.

Layer: L1 surface · Recipe: field-triad — label + help + error triad. Curriculum: AGENTS.md; pick matrix: docs/agent/pick-a-surface.md; blast radius: CONSUMER_MAP.md.

Receipts and renewal notices go here.

Use lowercase letters, numbers and hyphens only.

Copy this

html
<div class="hm-stack hm-measure">
  <div class="form-field">
    <label class="form-label" for="hm-field-email">Billing email<span class="form-required">*</span></label>
    <input class="form-input" id="hm-field-email" type="email" required placeholder="you@company.com" aria-describedby="hm-field-email-hint">
    <p class="form-hint" id="hm-field-email-hint">Receipts and renewal notices go here.</p>
  </div>
  <div class="form-field">
    <label class="form-label" for="hm-field-slug">Workspace slug</label>
    <input class="form-input" id="hm-field-slug" value="Acme Corp" aria-invalid="true" aria-describedby="hm-field-slug-error">
    <p class="form-error" id="hm-field-slug-error">Use lowercase letters, numbers and hyphens only.</p>
  </div>
  <div class="form-field">
    <label class="form-label" for="hm-field-color">Brand colour</label>
    <div class="form-color-group" data-color-group><input class="form-color-input" id="hm-field-color" type="color" value="#3b82f6"><input class="form-color-hex" type="text" spellcheck="false" autocomplete="off" aria-label="Hex colour" value="#3b82f6"></div>
  </div>
  <div class="form-field">
    <label class="form-label" for="hm-field-due">Due time</label>
    <div class="form-time-group" data-time-group><input class="form-input" id="hm-field-due" type="time" value="14:30"><input data-time-iso class="form-time-iso" type="text" spellcheck="false" autocomplete="off" aria-label="ISO time" value="14:30"></div>
  </div>
  <div class="form-field">
    <label class="form-label" for="hm-field-on">On date</label>
    <div class="form-date-group" data-date-group><input class="form-input" id="hm-field-on" type="date" value="2026-06-01"><input data-date-iso class="form-date-iso" type="text" spellcheck="false" autocomplete="off" aria-label="ISO date" value="2026-06-01"></div>
  </div>
</div>

Server exchange

This Hyperpart has no server exchange — it is presentation or client chrome only. htmx does not issue a request on this part's behalf. If you put an affordance (hx-*) on a control that uses this markup, that action's exchange belongs to the action, not this part. See Swap contract for host-owned envelopes.

Swap contract

Agent-visible HTMX topology (ADR-0054 / decision 0012). exchange envelope = what the response may re-emit relative to the persistent slot (body_only | outer | none | host_owned | document). dual-lock validates part markup only — not this envelope. Stem: stems/morph-safe-hypermedia.md; decision: docs/decisions/0012-swap-identity-contract.md.

No host HTMX exchange on this part — presentation or client chrome only. exchange envelope: n/a.

If a host wraps this markup in hx-*, that host owns the swap contract (sole identity + envelope). Prefer innerMorph / outerMorph for stable slots; replacement for flash; body-only responses under inner swaps.

Envelope response examples

This part has no owned exchange (envelope n/a). If a host adds hx-*, that host’s envelope applies — typically body_only:

html
<!-- Host wraps this presentation part with hx-* (host owns envelope) -->
<!-- Prefer: hx-swap="innerMorph" hx-target="#panel-body" -->
<!-- Server returns body_only interior for #panel-body -->
<div class="dz-stack">content…</div>

Do not re-own the slot:

html
<!-- WRONG: server returns the presentation root with a new id every poll -->
<div id="field-root" data-dz-region>…</div>

How to use it

No extended guidance authored yet — start from Copy this and the dependency chips (Primitive = markup only; controller = load listed JS; Endpoint = implement Server exchange).

Seams

  • copy the partial under Copy this; keep root class and data-* modifiers so the CSS/JS bundle matches
  • no Server exchange on this part — pure presentation or client chrome
  • satisfy the DOM contract tables (CI stop-ship)

DOM contract

5 dual-lock modules for this part (contracts/form_field.py, contracts/color.py, contracts/time.py, contracts/date.py, contracts/number.py). Read top-to-bottom: core root first, then extensions. Each Exemplar render() live box is CI fixture output for that module — not a second demo of the whole Hyperpart. What the tables require is the emitted HTML; Python under contracts/ is package-internal dual-lock (not an app route). Request/response wiring: Server exchange.

contracts/form_field.py

Required in the DOM: root .form-field (part form-field). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).

NodeAttrConstraint
.form-field——

Module source

Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.

python
"""HYPERPART: form-field — plain form field wrapper.

Dual-lock unit is the field root. Label, input, help, and a11y attrs are
host-owned. Class ``.dz-form-field`` is the stable substrate root
(``_emit_field``). Distinct from specialized widgets (combobox/tags/…).
"""

from contracts._kit import DomContract, Node

DOM_CONTRACT = DomContract(
    part="form-field",
    root=".dz-form-field",
    nodes=(Node(".dz-form-field", attrs={}),),
)

__all__ = ["DOM_CONTRACT"]

contracts/color.py

Required in the DOM: root [data-color-group] (part color). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).

NodeAttrConstraint
[data-color-group]——

Module source

Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.

python
"""HYPERPART: field (extension: dz-color) — colour input group.

Leftover honesty (cycle 2133): the hex companion is an editable text
input (no ``name`` — the native ``type=color`` swatch is the submitted
value). Typed leftover junk (``#3b82f6zzz``, ``red``, ``rgb(…)``) must
not invent a colour — the swatch stays put and both controls fail
custom validity so submit cannot post the previous swatch as if the
leftover were accepted. Empty hex on blur restores from the swatch.
"""

from contracts._kit import DomContract, Node

DOM_CONTRACT = DomContract(
    part="color",
    root="[data-dz-color-group]",
    nodes=(Node("[data-dz-color-group]", attrs={}),),
)

__all__ = ["DOM_CONTRACT"]

contracts/time.py

Required in the DOM: root [data-time-group] (part time). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).

NodeAttrConstraint
[data-time-group]data-time-grouppresent (any value)
[data-time-iso]data-time-isopresent (any value)

Module source

Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.

python
"""HYPERPART: field (extension: dz-time) — time / datetime-local group.

Leftover honesty (cycle 2144): the ISO companion is an editable text
input (no ``name`` — the native ``type=time`` / ``datetime-local`` is
the submitted value). Typed leftover junk (``14:30zzz``, ``2pm``,
``2026-06-01T14:30zzz``) must not invent a clock — the native stays
put and both controls fail custom validity so submit cannot post the
previous time as if the leftover were accepted. Empty ISO on blur
restores from the native.
"""

from contracts._kit import DomContract, Node, Present

DOM_CONTRACT = DomContract(
    part="time",
    root="[data-dz-time-group]",
    nodes=(
        Node("[data-dz-time-group]", attrs={"data-dz-time-group": Present()}),
        Node("[data-dz-time-iso]", attrs={"data-dz-time-iso": Present()}),
    ),
)

__all__ = ["DOM_CONTRACT"]

contracts/date.py

Required in the DOM: root [data-date-group] (part date). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).

NodeAttrConstraint
[data-date-group]data-date-grouppresent (any value)
[data-date-iso]data-date-isopresent (any value)

Module source

Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.

python
"""HYPERPART: field (extension: dz-date) — standalone date group.

Leftover honesty (cycle 2145): the ISO companion is an editable text
input (no ``name`` — the native ``type=date`` is the submitted value).
Typed leftover junk (``2026-06-01zzz``, ``zzz``, ``June 1``) must not
invent a date — the native stays put and both controls fail custom
validity so submit cannot post the previous date as if the leftover
were accepted. Empty ISO on blur restores from the native.
"""

from contracts._kit import DomContract, Node, Present

DOM_CONTRACT = DomContract(
    part="date",
    root="[data-dz-date-group]",
    nodes=(
        Node("[data-dz-date-group]", attrs={"data-dz-date-group": Present()}),
        Node("[data-dz-date-iso]", attrs={"data-dz-date-iso": Present()}),
    ),
)

__all__ = ["DOM_CONTRACT"]

contracts/number.py

Required in the DOM: root [data-number-group] (part number). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).

NodeAttrConstraint
[data-number-group]data-number-grouppresent (any value)
[data-number-value]data-number-valuepresent (any value)

Module source

Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.

python
"""HYPERPART: field (extension: dz-number) — standalone number group.

Leftover honesty (cycle 2149): the companion is an editable text
input (no ``name`` — the native ``type=number`` is the submitted
value). Typed leftover junk (``12abc``, ``zzz``, ``1e2``) must not
invent a number — the native stays put and both controls fail custom
validity so submit cannot post the previous number as if the leftover
were accepted. Empty companion on blur restores from the native.
Out-of-[min,max] is invalid (do not invent by clamping).
"""

from contracts._kit import DomContract, Node, Present

DOM_CONTRACT = DomContract(
    part="number",
    root="[data-dz-number-group]",
    nodes=(
        Node("[data-dz-number-group]", attrs={"data-dz-number-group": Present()}),
        Node("[data-dz-number-value]", attrs={"data-dz-number-value": Present()}),
    ),
)

__all__ = ["DOM_CONTRACT"]

Notes

Reuses the form-* family (label / hint / input / error). The invalid field needs no modifier class — the red border keys off aria-invalid="true", the same attribute assistive tech reads. The colour group uses data-color-group so color.js can mirror the swatch and the hex companion (contract: contracts/color.py). Hex leftover junk must not invent a colour (cycle 2133). Time / datetime-local use data-time-group so time.js can mirror the native clock and the ISO companion (contract: contracts/time.py). Leftover ISO junk must not invent a time (cycle 2144). Standalone date uses data-date-group so date.js can mirror the native date and the ISO companion (contract: contracts/date.py). Leftover ISO junk must not invent a date (cycle 2145). Standalone number uses data-number-group so number.js can mirror the native number and the editable companion (contract: contracts/number.py). Leftover junk (12abc / zzz / 1e2) must not invent a number (cycle 2149). Dual-lock: form triad contracts/form_field.py + colour contracts/color.py + time contracts/time.py + date contracts/date.py + number contracts/number.py (HMC-139).

Source files

Canonical registration in the registry. No dedicated controller — CSS for this part lives in the layered bundle.

site/registry.py · contracts/form_field.py · contracts/color.py · contracts/time.py · contracts/date.py · contracts/number.py