Skip to content

Form recipe

Compose a form with visible labels, native selection groups, associated errors and an application-owned request lifecycle. Preserve answers through failure and cancellation. The live example simulates completion locally; it does not store or transmit preferences.

01Preview

Interests (choose at least one)
Frequency

02Native selection groups

<script>
  import { Button } from "@dg/ui";
  let interests = $state([]);
  let frequency = $state("monthly");
  let attempted = $state(false);
  let status = $state("");
  const invalid = $derived(attempted && !interests.length);
</script>

<form
  class="stack"
  novalidate
  onsubmit={(event) => {
    event.preventDefault();
    attempted = true;
    status = interests.length ? "Example selections accepted." : "Choose at least one interest.";
    if (!interests.length) event.currentTarget.querySelector('input[type="checkbox"]').focus();
  }}
>
  <fieldset aria-describedby={invalid ? "interests-help interests-error" : "interests-help"}>
    <legend>Interests (choose at least one)</legend>
    <p id="interests-help">Multiple selections are allowed.</p>
    <label><input type="checkbox" value="Artworks" bind:group={interests} aria-invalid={invalid || undefined} /> Artworks</label>
    <label><input type="checkbox" value="Research" bind:group={interests} aria-invalid={invalid || undefined} /> Research</label>
    {#if invalid}<p class="feedback" data-tone="error" id="interests-error">Choose at least one interest.</p>{/if}
  </fieldset>
  <fieldset>
    <legend>Frequency</legend>
    <label><input type="radio" name="frequency" value="monthly" bind:group={frequency} /> Monthly</label>
    <label><input type="radio" name="frequency" value="quarterly" bind:group={frequency} /> Quarterly</label>
  </fieldset>
  <Button type="submit" label="Continue" />
  <p class="feedback" role="status">{status}</p>
</form>

Use a shared name for one radio group and distinct names for unrelated groups on the same page. A fieldset and legend name the group; each option retains its own label. Native Space toggles a checkbox, and arrow keys move through a radio group. Do not add custom ARIA checkbox/radio roles or duplicate native checked state. Requiring one of several checkboxes needs group validation: adding required to every checkbox would require every selection.

03Validation and requests

The complete runnable specimen adds native email validity, first-error focus, busy state, disabled inputs, cancellation, simulated failure, confirmation and preserved answers. Style the owning option label to provide a usable target without changing native keyboard behavior.

Prevent duplicate requests. Keep cancellation outside a disabled fieldset, and progress/results outside a region marked busy. Re-enable controls before restoring focus. A real application owns server validation, request identifiers and persistence; never replace server checks with the local example.

04Verification

Submit empty and invalid answers, then correct them without losing other selections. Start, fail, retry, cancel and complete the request. Check label clicks, native keyboard navigation, disabled tab order, first-error focus, status announcements and translated long labels. Follow accessibility verification before claiming a screen-reader pass.

Related subjects

distributed·gallery