Skip to content

Settings page

An hc-shell app frame — header, sidebar navigation, scrolling main — whose main region is the account-settings block wired up for real: an hc-card form of hc-field rows with persistent hints, submitting the mutating-form way. A 422 distributes the server’s field-errors fragment to the fields it names; success raises a toast.

Try all three branches: submit with a blank email (the required error lands under the Email field), with taken@example.com (the duplicate error), and with any other email (a “Member saved” toast — the empty 200 body clears the error summary).

The demo form posts to api/recipes/field-errors/members, whose contract — 422 + fragment on failure, 200 + hc:toast on success — keeps the whole round trip on this page. The mutating-form contract’s own success branch is a redirect (HX-Redirect, a full window navigation) — its recipe page demos that; a settings page that stays put on save uses exactly the toast variant shown here.

Acme Admin
Account settings

Used for sign-in and notifications — we never share it.

Shown to the rest of your team.

The complete page, with real-app-shaped URLs (/settings/profile — swap in your own endpoint). No toast markup is needed: installToast() creates its own region.

<body>
<div class="hc-shell">
<header class="hc-shell__header">
<button class="hc-button" data-variant="ghost"
data-hc-shell-toggle aria-label="Open navigation" type="button"></button>
<strong>Acme Admin</strong>
</header>
<nav class="hc-shell__sidebar" aria-label="Primary">
<a href="/dashboard">Dashboard</a>
<a href="/members">Members</a>
<a href="/settings" aria-current="page">Settings</a>
</nav>
<main class="hc-shell__main">
<!-- method/action keep the no-JS submit working (post/redirect/get). -->
<form class="hc-card" style="max-inline-size: 34rem;"
method="post" action="/settings/profile"
data-hx-post="/settings/profile"
data-hx-target="#settings-errors"
data-hx-swap="innerHTML"
data-hx-disabled-elt="find button[type=submit]"
data-hx-indicator="find .hc-spinner">
<div class="hc-card__header"><strong>Account settings</strong></div>
<div class="hc-card__body hc-stack" style="--hc-stack-gap: 1rem;">
<!-- The 422 field-errors fragment swaps in here; installFieldErrors
distributes each item to the field it names. -->
<div id="settings-errors"></div>
<div class="hc-field">
<label class="hc-field__label" for="settings-email">Email</label>
<input class="hc-input" id="settings-email" name="email" type="email"
aria-describedby="settings-email-hint" />
<!-- __hint stays muted even when the field is invalid. -->
<p class="hc-field__hint" id="settings-email-hint">
Used for sign-in and notifications — we never share it.
</p>
<p class="hc-field__error" aria-live="polite"></p>
</div>
<div class="hc-field">
<label class="hc-field__label" for="settings-display-name">Display name</label>
<input class="hc-input" id="settings-display-name" name="display_name" type="text"
aria-describedby="settings-display-name-hint" />
<p class="hc-field__hint" id="settings-display-name-hint">
Shown to the rest of your team.
</p>
</div>
<label class="hc-switch-label">
<input class="hc-switch" name="notify" type="checkbox" role="switch" checked />
Email notifications
</label>
</div>
<div class="hc-card__footer hc-cluster"
style="--hc-cluster-gap: 0.5rem; justify-content: flex-end;">
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
<button class="hc-button" data-variant="primary" type="submit">Save changes</button>
</div>
</form>
</main>
</div>
</body>
RegionComponent / blockRecipeContract
App framehc-shell— (pure CSS; installShell() powers mobile nav)
Settings cardhc-card + account-settings blockmutating-formmutating-form/contract.md
Fields + hintshc-field + hc-input
Error summary + distributionhc-alertfield-errorsfield-errors/contract.md
Success toasthc-toasttoasttoast/contract.md
  • Rename the endpoints. /settings/profile is a placeholder — the server just has to implement the mutating-form / field-errors contracts (422 + fragment, success + toast or HX-Redirect).
  • Real CSRF. Add <meta name="csrf-token"> and installCsrfHeader() for the htmx path, plus your framework’s hidden field for the no-JS form — see CSRF tokens.
  • Real auth. The demo API is anonymous; gate your endpoint with your framework’s session auth — the server integration guides show the idioms per stack.
  • Error copy. Keep messages human — see the writing guide.