Field errors
field-errors is the kit’s contract for server-side validation
errors over htmx. The server answers a failed submission (typically a
422) with a small alert fragment naming the offending fields; the
installFieldErrors() behavior distributes each error to the matching
field — message in the field’s error slot, aria-invalid +
aria-describedby on the control — and clears them again as the user
fixes things. The fragment is a documented wire format, so template
engines and code generators can emit it verbatim.
The fragment
Section titled “The fragment”This is the canonical shape the server returns; htmx swaps it into a container inside (or pointed at) the form:
<div class="hc-alert" data-variant="error" role="alert" data-hc-field-errors> <p class="hc-alert__title">Unprocessable Entity</p> <ul class="hc-alert__errors"> <li class="hc-alert__error" data-field="email" data-code="duplicate" data-message-key="members.email.duplicate">email: duplicate</li> </ul> <p class="hc-alert__body">optional hint line</p></div>data-hc-field-errorsis the behavior opt-in. Empty: distribute into the closest<form>. With a value: a CSS selector for the form (for out-of-band swaps, or an alert rendered outside the form).data-fieldnames the control (itsnameattribute). Radio/checkbox groups resolve to their shared field, skipping hidden inputs — the boolean-field idiom (hiddenfalse+ checkboxtrueunder one name) wires to the visible checkbox. An item naming no known control stays visible in the summary — use that for form-level errors.data-message-keyis optional client-side localization (below); the item’s text is the fallback.data-codeis passed through for your own styling/tests.
Try it
Section titled “Try it”Clicking Simulate 422 inserts exactly the fragment above next to the form (standing in for an htmx swap). The email error lands inline on its field with full ARIA wiring; the unknown-field item stays in the summary. Edit the email field and its error clears.
<form data-hx-post="/members" data-hx-target="#form-errors" data-hx-swap="innerHTML"> <div id="form-errors"></div>
<div class="hc-field"> <label class="hc-field__label" for="email">Email</label> <input class="hc-input" id="email" name="email" type="email"> </div>
<button type="submit" class="hc-button" data-variant="primary">Save</button></form>What happens
Section titled “What happens”- htmx swaps the fragment in; the behavior reacts to
htmx:afterSwap/htmx:oobAfterSwap(with aMutationObserverfallback, plus an install-time scan for full-page error renders). - Previous server errors in the form are cleared.
- Each
.hc-alert__error[data-field]matching a control writes its message into the field’s.hc-field__error(created automatically — even after a bare control with nohc-fieldwrapper), setsaria-invalid="true"andaria-describedbyon the control anddata-invalid="true"on the field, and is markeddata-distributed="true"(hidden in the summary so it isn’t read twice). - The alert is stamped
data-distributed="all | partial | none"and the first invalid control receives focus. - A field’s server error clears on the user’s first
input/changein that field, on submit/reset, and before the next fragment is distributed.
Fragment attributes
Section titled “Fragment attributes”| Attribute | On | Meaning |
|---|---|---|
data-hc-field-errors | alert | Opt-in; empty = closest('form'), value = CSS selector for the form. |
data-field | item | Control name to attach the error to. |
data-code | item | Optional machine-readable code (available to the resolver as {code}). |
data-message-key | item | Optional i18n catalog key; item text is the fallback. |
data-message-params | item | Optional JSON object of interpolation values for the catalog lookup; merged over the implicit {field}/{code}. Malformed JSON is ignored. |
data-summary="auto" | alert | Hide the whole alert once every item was distributed. |
data-focus="none" | alert | Don’t focus the first invalid control. |
Localizing message keys
Section titled “Localizing message keys”data-message-key resolves through the same
i18n catalog as every
other kit string — configure it once at startup:
import { setMessages } from '@hypermedia-components/core';
setMessages({ 'members.email.duplicate': 'このメールアドレスは既に登録されています', // `{field}` / `{code}` interpolate if you want generic messages: 'errors.required': '{field} is required',});When a translation needs values beyond {field}/{code} — constraint
parameters, validation row columns — the server sends them as a JSON
object in data-message-params; the client resolver merges them into
the interpolation params (item values win over the implicit ones):
<li class="hc-alert__error" data-field="qty" data-code="stock" data-message-key="orders.qty.exceeds" data-message-params='{"stock": 5}'>在庫 5 を超えています。</li>setMessages({ 'orders.qty.exceeds': '在庫 {stock} を超えています。' });A key missing from the catalog falls back to the item’s own text — so
when your server starts localizing, emit final text and drop the keys;
nothing on the client changes. Malformed data-message-params JSON is
ignored the same way: the item text still renders.
Server response contract
Section titled “Server response contract”htmx ≥ 2 does not swap non-2xx responses by default. Allow the 422
swap once, globally:
document.body.addEventListener('htmx:beforeSwap', (event) => { if (event.detail.xhr.status === 422) { event.detail.shouldSwap = true; event.detail.isError = false; }});Alternatively answer 200 with the fragment, or steer any response
into the error container with HX-Retarget: #form-errors +
HX-Reswap: innerHTML. The full contract (required markup, all
attributes, behavior steps) lives in
recipes/field-errors/contract.md.
Interplay with client-side validation
Section titled “Interplay with client-side validation”installValidation()
surfaces native constraint errors into the same .hc-field__error
slot with the same ARIA wiring. The two compose: a native message
outranks a server error on the same control (it reflects the current
value), and neither clears the other’s state prematurely.
Accessibility
Section titled “Accessibility”- The summary uses
role="alert", so the swap itself is announced; field messages usearia-live="polite"and are referenced from the control viaaria-describedby. - The first invalid control is focused — the standard
post-submit error pattern. Disable with
data-focus="none"if your flow manages focus itself. - Distributed items are only visually removed from the summary; an item that matches no field remains visible, so no error is ever silently dropped.
Progressive enhancement
Section titled “Progressive enhancement”- Without JavaScript (full-page re-render), the alert renders all errors as a plain list — distribution is an enhancement, nothing is lost.
- Without htmx, fragments present in the initial HTML are still distributed by the install-time scan.
Related
Section titled “Related”- Field — the label/control/message composition the errors land in.
- Alert — the summary container.
- Data region — the swap-driven counterpart for non-form content.