Form validation
Preview Available in release assets
Present native constraint validation and authoritative server errors without moving business rules into a design-system script.
Server contract
POST /api/advanced/form-validation accepts the canonical native form and returns a complete document; GET the same route renders the initial document. Add .thin-form-validation to the form and .thin-validation-summary to a server-rendered summary. A Datastar request to the same action returns one patch replacing the stable form, while server business validation remains application code.
With no CSS, JavaScript, or Datastar, the form posts to its action and the server returns a complete document containing submitted values and errors. Native constraint validation remains available, while server validation is always authoritative.
Form validation primary example
<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>Variant gallery
Each authored example uses the production assets and includes copyable markup.
Native constraints
Browser-owned required and email checks precede ordinary submission.
<form class="thin-form-validation" action="/subscribe" method="post">
<div class="thin-field">
<label for="subscribe-email">Email</label>
<input class="thin-input" id="subscribe-email" name="email" type="email" required autocomplete="email" />
</div>
<button class="thin-button">Subscribe</button>
</form>Show code
<form class="thin-form-validation" action="/subscribe" method="post">
<div class="thin-field">
<label for="subscribe-email">Email</label>
<input class="thin-input" id="subscribe-email" name="email" type="email" required autocomplete="email" />
</div>
<button class="thin-button">Subscribe</button>
</form>Returned server errors
A complete response preserves the submitted value and explains the error.
<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 may request and morph the same server-authored form; the native action remains the fallback.
<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</label>
<input class="thin-input" id="enhanced-email" name="email" type="email" required />
</div>
<button class="thin-button">Save</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</label>
<input class="thin-input" id="enhanced-email" name="email" type="email" required />
</div>
<button class="thin-button">Save</button>
</form>Anatomy
- Ordinary method and action
- Optional linked error summary
- Labelled native controls
- Persistent hints
- Field-level server errors
- Submit button
Variants and sizes
- Native constraint validation — shown above
- Server-rendered field errors — shown above
- Linked error summary — shown above
- Datastar-enhanced server response — shown above
States and difficult content
Rendered fixtures exercise authored content and constrained layouts with the same production assets.
- Pristine
- User-invalid native constraint
- Server-rejected submission
- Multiple errors
- Corrected resubmission
- Long localized message
- Narrow container
- Network or enhancement failure
Structured state fixtures are not published for this item yet.
Interaction and keyboard behavior
The browser checks authored type, required, min, max, minlength, maxlength, and pattern constraints before ordinary submission. The server revalidates every value and returns the submitted values plus specific errors. An optional Datastar flow may morph the same server-authored form, then focus the summary; it must not change the rules or response meaning.
Accessibility
Keep errors in text and associate field errors through aria-describedby without discarding hints. Set aria-invalid only for a known invalid value. Give a returned summary a heading, link each message to its control, and move focus to it only after a submitted response—not during typing. Do not rely on color, live validation, or disabled submit buttons.
- Contract
LIVE- Basis
- Native form constraint validation plus authored summary/status; no APG widget
- Evidence
tests/unit/library/date-time-validation.test.ts
API
POST /api/advanced/form-validation accepts the canonical native form and returns a complete document; GET the same route renders the initial document. Add .thin-form-validation to the form and .thin-validation-summary to a server-rendered summary. A Datastar request to the same action returns one patch replacing the stable form, while server business validation remains application code.
Behavior tier: datastar-optional
Structured host, property, and example tables are not authored for this item yet. Use the summary above and the primary example.
Fallback
The native form posts to its server action and returns complete errors without JavaScript.
Assets and dependencies
/components.css
Dependencies: None
Source: packages/ui/src/items/form-validation/style.ts
Composition
Derived from the examples and style specs, not authored.