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.

Submit with the email left blank to see the required error, or use taken@example.com to see the duplicate item distributed into the field (message, aria-invalid, focus). Any other email succeeds: the server answers with an empty body that clears the errors, plus an HX-Trigger toast. The endpoint is a demo implementation of the server contract below, namespaced under api/recipes/field-errors/ — your app would use its own URL such as /members.

We never share it.

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">Please fix the errors below.</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.

The title and item texts are app-chosen copy — see Writing UI copy for how to phrase them.

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.

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.