Build a server-rendered app
Assemble forms, actions, and paging from native HTML and CSS-only recipes. The server owns validation, permissions, and page boundaries—optional Datastar enhances transport without changing meaning.
Progressive ladder
1. Foundations
Load foundation and component CSS. Native form controls retain semantics and browser validation before any skin.
2. Field
Compose labelled controls with hints and optional inline validation feedback.
3. Button
Theme-driven presentation on native buttons and links; form ownership stays on the native child.
4. Form validation
Native constraints precede submission; the server revalidates and returns authoritative errors. Datastar is optional on one form when you need morph without changing the rules.
5. Pagination
Navigate result pages with durable links and a clearly marked current page—no client-side paging state.
Field
Label, control, hint, and optional inline native-validation explanation.
<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>Button
Theme-driven variants preserve native button semantics and form ownership.
<button class="thin-button" type="submit">
<svg class="thin-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
<span>Save changes</span>
</button>Show code
<button class="thin-button" type="submit">
<svg class="thin-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
<span>Save changes</span>
</button>Form validation
Server-rendered summary and field errors; native constraints run before ordinary submission.
<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>Pagination
Every destination is an ordinary link; only the current page uses aria-current=page.
<nav class="thin-pagination" aria-label="Pagination">
<a href="?page=1">Previous</a>
<a href="?page=1">1</a>
<a href="?page=2" aria-current="page">2</a>
<a href="?page=3">3</a>
<a href="?page=3">Next</a>
</nav>Show code
<nav class="thin-pagination" aria-label="Pagination">
<a href="?page=1">Previous</a>
<a href="?page=1">1</a>
<a href="?page=2" aria-current="page">2</a>
<a href="?page=3">3</a>
<a href="?page=3">Next</a>
</nav>