Submit and validate a form
Keep business rules on the server. Native constraint validation runs first; returned errors preserve submitted values. Optional Datastar POST morphing uses the same response without changing its meaning.
Progressive ladder
1. Field
Start with explicit labels, native controls, and persistent hints. The labelled control remains usable without CSS or JavaScript.
2. Native constraints
Use type, required, min, max, minlength, maxlength, and pattern only for rules the browser can express truthfully.
3. Server validation
The server revalidates every value and returns submitted values plus specific errors in a complete response.
4. Optional Datastar POST morph
When enhancement is available, the application may request and morph the same server-authored form with a Datastar submit handler. The native action, method, and values remain the fallback.
Field with hints
Associate hints with aria-describedby; the enhancement adds aria-invalid without removing authored descriptions.
<div class="thin-field">
<label for="field-email">Email address</label>
<small class="thin-hint" id="field-email-hint">Use an address like name@example.com. We’ll send order updates there.</small>
<input class="thin-input" id="field-email" name="email" type="email" autocomplete="email" required aria-describedby="field-email-hint">
</div>Show code
<div class="thin-field">
<label for="field-email">Email address</label>
<small class="thin-hint" id="field-email-hint">Use an address like name@example.com. We’ll send order updates there.</small>
<input class="thin-input" id="field-email" name="email" type="email" autocomplete="email" required aria-describedby="field-email-hint">
</div>Server-rendered errors
A linked error summary and field-level errors preserve submitted values after rejection.
<form class="thin-form-validation" action="/account/profile" method="post">
<div class="thin-validation-summary" role="alert" aria-labelledby="profile-errors-title">
<h2 id="profile-errors-title">There is a problem</h2>
<ul>
<li><a href="#profile-email">Enter an email address in the correct format.</a></li>
</ul>
</div>
<div class="thin-field">
<label for="profile-name">Display name</label>
<input class="thin-input" id="profile-name" name="display_name" required minlength="2" maxlength="80" autocomplete="name" value="A very long submitted name remains available for correction" />
</div>
<div class="thin-field">
<label for="profile-email">Email address</label>
<input class="thin-input" id="profile-email" name="email" type="email" required autocomplete="email" value="not-an-email" aria-invalid="true" aria-describedby="profile-email-hint profile-email-error" />
<small class="thin-hint" id="profile-email-hint">Used for account notices.</small>
<small class="thin-error" id="profile-email-error">Enter an email address in the correct format.</small>
</div>
<button class="thin-button" type="submit">Save profile</button>
</form>Show code
<form class="thin-form-validation" action="/account/profile" method="post">
<div class="thin-validation-summary" role="alert" aria-labelledby="profile-errors-title">
<h2 id="profile-errors-title">There is a problem</h2>
<ul>
<li><a href="#profile-email">Enter an email address in the correct format.</a></li>
</ul>
</div>
<div class="thin-field">
<label for="profile-name">Display name</label>
<input class="thin-input" id="profile-name" name="display_name" required minlength="2" maxlength="80" autocomplete="name" value="A very long submitted name remains available for correction" />
</div>
<div class="thin-field">
<label for="profile-email">Email address</label>
<input class="thin-input" id="profile-email" name="email" type="email" required autocomplete="email" value="not-an-email" aria-invalid="true" aria-describedby="profile-email-hint profile-email-error" />
<small class="thin-hint" id="profile-email-hint">Used for account notices.</small>
<small class="thin-error" id="profile-email-error">Enter an email address in the correct format.</small>
</div>
<button class="thin-button" type="submit">Save profile</button>
</form>Optional Datastar transport
The application adds data-on:submit with a Datastar POST handler to morph the same server-authored form. Without enhancement, the native POST remains canonical.
<form class="thin-form-validation" action="/account/profile" method="post" data-on:submit="@post('/account/profile')">
<div class="thin-field">
<label for="enhanced-email">Email address</label>
<input class="thin-input" id="enhanced-email" name="email" type="email" required autocomplete="email" />
</div>
<button class="thin-button" type="submit">Save profile</button>
</form>Show code
<form class="thin-form-validation" action="/account/profile" method="post" data-on:submit="@post('/account/profile')">
<div class="thin-field">
<label for="enhanced-email">Email address</label>
<input class="thin-input" id="enhanced-email" name="email" type="email" required autocomplete="email" />
</div>
<button class="thin-button" type="submit">Save profile</button>
</form>