Skip to content

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.

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-errors is 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-field names the control (its name attribute). Radio/checkbox groups resolve to their shared field, skipping hidden inputs — the boolean-field idiom (hidden false + checkbox true under 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-key is optional client-side localization (below); the item’s text is the fallback. data-code is passed through for your own styling/tests.

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.

We never share it.

  1. htmx swaps the fragment in; the behavior reacts to htmx:afterSwap / htmx:oobAfterSwap (with a MutationObserver fallback, plus an install-time scan for full-page error renders).
  2. Previous server errors in the form are cleared.
  3. 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 no hc-field wrapper), sets aria-invalid="true" and aria-describedby on the control and data-invalid="true" on the field, and is marked data-distributed="true" (hidden in the summary so it isn’t read twice).
  4. The alert is stamped data-distributed="all | partial | none" and the first invalid control receives focus.
  5. A field’s server error clears on the user’s first input/change in that field, on submit/reset, and before the next fragment is distributed.
AttributeOnMeaning
data-hc-field-errorsalertOpt-in; empty = closest('form'), value = CSS selector for the form.
data-fielditemControl name to attach the error to.
data-codeitemOptional machine-readable code (available to the resolver as {code}).
data-message-keyitemOptional i18n catalog key; item text is the fallback.
data-message-paramsitemOptional JSON object of interpolation values for the catalog lookup; merged over the implicit {field}/{code}. Malformed JSON is ignored.
data-summary="auto"alertHide the whole alert once every item was distributed.
data-focus="none"alertDon’t focus the first invalid control.

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.

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.

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.

  • The summary uses role="alert", so the swap itself is announced; field messages use aria-live="polite" and are referenced from the control via aria-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.
  • 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.
  • Field — the label/control/message composition the errors land in.
  • Alert — the summary container.
  • Data region — the swap-driven counterpart for non-form content.