Skip to content

← Guides

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

    Load foundation and component CSS. Native form controls retain semantics and browser validation before any skin.

    ResetComponents catalog

  2. 2. Field

    Compose labelled controls with hints and optional inline validation feedback.

    Field

  3. 3. Button

    Theme-driven presentation on native buttons and links; form ownership stays on the native child.

    Button

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

    Form validationSubmit and validate a form

  5. 5. Pagination

    Navigate result pages with durable links and a clearly marked current page—no client-side paging state.

    Pagination

Field

Label, control, hint, and optional inline native-validation explanation.

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>

Button

Theme-driven variants preserve native button semantics and form ownership.

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.

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>

Pagination

Every destination is an ordinary link; only the current page uses aria-current=page.

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>