Inline validation (422 swap)
Form validation errors render inline under each field without a full page reload. The server returns HTTP 422 with the failed form partial; htmx swaps it back into the form's slot.
#When to use
Every form in your app. This is the default: on a non-htmx request the handler return Page() still re-renders with errors, so the same code powers both flows.
#Markup
<form id="product_form"
hx-post="/products?handler=Save"
hx-target="#products_table"
hx-swap="outerHTML"
hx-disabled-elt="find button[type=submit]">
<input name="Name" />
<span class="text-red-500" data-error-for="Name"></span>
<input name="Price" type="number" step="0.01" />
<span class="text-red-500" data-error-for="Price"></span>
<button type="submit">Save</button>
</form>
By default htmx swaps the response into hx-target only on 2xx. To swap the same target on 422 too — so the validation-failed form replaces the original — add this to your _Layout.cshtml once:
<script>
document.body.addEventListener('htmx:beforeSwap', function(e) {
if (e.detail.xhr.status === 422) {
e.detail.shouldSwap = true;
// Re-target to the form itself, not the success target.
e.detail.target = e.detail.requestConfig.elt;
}
});
</script>
That snippet is included in the Studio starter's _Layout.cshtml already.
#Handler
public IActionResult OnPostSave(ProductEditForm form, CancellationToken ct)
{
if (!ModelState.IsValid)
{
return this.PageOrPartial("_ProductForm", form, AjaxExtensions.UnprocessableEntity);
}
_products.Save(form);
this.AddToast("Saved.", ToastLevel.Success);
return this.PageOrPartial("_ProductsTable", _products.List());
}
The third argument to PageOrPartial is the status code. 422 is the canonical "validation error" — semantically distinct from 400 (malformed request).
#ASP.NET Core's asp-validation-for works as-is
<input asp-for="Name" />
<span asp-validation-for="Name" class="text-red-500"></span>
The standard tag helper renders the inline error message from ModelState. When the 422 swap brings in the new form HTML, the error spans come pre-populated.
#Related
- Modal form with validation — same pattern scoped inside a dialog.
- Toast after action — raise a toast on successful save without touching the form.