Skip to content

← Guides

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. 1. Field

    Start with explicit labels, native controls, and persistent hints. The labelled control remains usable without CSS or JavaScript.

    Field

  2. 2. Native constraints

    Use type, required, min, max, minlength, maxlength, and pattern only for rules the browser can express truthfully.

    Form validation

  3. 3. Server validation

    The server revalidates every value and returns submitted values plus specific errors in a complete response.

    Form validation recipe

  4. 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.

    Advanced Datastar recipes

Field with hints

Associate hints with aria-describedby; the enhancement adds aria-invalid without removing authored descriptions.

Use an address like name@example.com. We’ll send order updates there.

Read the complete Field contract

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.

Used for account notices. Enter an email address in the correct format.

Read the complete Form validation contract

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.

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>