# Native forms

Native select, text fields, choice groups, labels and feedback, with no form-field component.

## Native select

There is no exported Select component. A native `select` receives the `--background-prominent` fill, the default text color, a transparent border that keeps its size, square corners, both control minimum dimensions, control padding and a maximum width of 100%. Like [labels](#native-fields-and-feedback), it takes the [interface role](typography.md#classes) wherever it sits, uppercase included, so a wrapping label or a parent's typography does not change it. Under a coarse pointer its font is at least `1rem`, like text fields, so touch browsers do not zoom on focus.

```svelte
<script>
  let view = $state("collection");
</script>

<label class="stack">
  View
  <select bind:value={view}>
    <option value="collection">Collection</option>
    <option value="archive">Archive</option>
  </select>
</label>
<p>Selected view: {view}</p>
```

Dropdowns without `multiple` and with no `size` or `size="1"` replace the system arrow with a CSS chevron. Two 5px gradients form a 10 × 5px triangle; the right padding reserves the 10px drawing plus `--space-2` beyond the control inset. These are drawing dimensions, not extra public tokens. Visible lists (`size` greater than one) and multiple selects retain native presentation. Forced-colors mode restores the system arrow and removes the CSS reserve.

Select focus uses the field corner marks described under [native fields](#native-fields-and-feedback); dropdowns keep their chevron. Disabled selects use `--disabled-opacity` and rely on native HTML for activation and tab-order exclusion. Keep an associated label and test keyboard selection, long option labels, disabled state and list variants. The rendered specimen is in the [DS app](../../../../apps/ds/README.md).

## Native fields and feedback

There is no TextField or form-state component. Native text, email, password, search, tel, URL, number, date, time, datetime-local, month and week inputs and textareas share a square field by default, like select. Checkbox and radio use the [choice box](#native-choice-groups); color, file and range inputs keep their native rendering. Labels and legends need no class, and the classes below cover help and feedback; the application owns validation, network requests and submission. Layout comes from `.stack`, `.row` and their spacing modifiers, not from a field wrapper.

| Name               | Description                                                                                                                                                                                               |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.feedback`        | Help, validation and status text in metadata typography and muted color; `data-tone="error\|success\|warning\|info"` selects one functional color. Semantics and announcements belong to the application. |
| `.visually-hidden` | Visually hides noninteractive text while preserving accessibility, for example an existing live region. Never use it to hide focusable controls.                                                          |

Every `label` and `legend` takes the [interface role](typography.md#classes): mono family, UI size, weight, line height and tracking, normal style and uppercase, in the inherited color. Field labels, group legends, option labels beside a checkbox or radio, and select therefore read as one interface layer, whether a label wraps its control or names it through `for`. Screen readers announce the source text. Where casing carries meaning, such as an address, keep that text outside the label or set `text-transform: none` on it in app styles.

Text fields fill their owner, follow control insets and minimum target dimensions, use body typography and keep square corners. They keep body text because uppercase would display typed text differently from the value submitted. They sit on the `--background-prominent` fill with no visible border; the transparent border keeps their size and becomes visible in forced-colors mode. Under a coarse pointer their font is at least `1rem`, so touch browsers do not zoom on focus. Textarea remains vertically resizable. `readonly` drops the fill and preserves editing restrictions without dimming or excluding focus; `disabled` keeps the fill, dims and removes native interaction. `aria-invalid="true"` draws the border in the error color but does not validate the value. In Chromium and WebKit, the clear button of a search field draws a thin 10px cross in `--text-subtle`, turning `--accent` on hover; Firefox shows none.

Browsers show focus on text fields after every click, not only from the keyboard. Instead of an outer outline, a focused field or select draws the [`.half-crossed`](surfaces.md#classes) marks in `--focus`: at each corner, two `--cross-size` strokes of `--border-width` pointing along the field edges. The marks follow `:focus-within`, so they remain while focus sits on an internal part such as a date segment or a picker button, where browsers stop matching `:focus-visible`. An invalid field keeps its error border while focused, with the marks drawn over its corners. Forced-colors mode removes the marks and shows a system-colored outline.

Help, errors and status messages always use `.feedback`, so they share the [metadata role](typography.md#classes) across apps; do not restyle them as body text. Like `.metadata`, they display uppercase: keep messages short and put text whose casing matters, such as a URL, outside them. Attributes, not the class, tie a message to its role: link help through the field's `aria-describedby`; give a validation error `data-tone="error"` and add it to the invalid field's `aria-describedby`, since it has no automatic alert role; mark a status region with `role="status"` or `aria-live`.

Use a visible label, a unique ID, appropriate input type and autocomplete. A placeholder is not a label. Describe required fields in text as well as with `required`; explain errors without relying on color. Include both help and error IDs in `aria-describedby` when both apply. Do not show a blank untouched required field as invalid before validation is relevant.

Keep a status region mounted before updating its text. Use `role="status"` for ordinary progress or success; reserve `role="alert"` for important new errors. `aria-busy` describes an updating region but does not disable controls. Prevent repeat submissions through application state and native `disabled`, retain any useful cancellation action, and report completion or failure. Do not nest `ChatComposer`'s form inside another form.

```svelte
<script>
  import { Button } from "@dg/ui";
  let email = $state("");
  let attempted = $state(false);
  let status = $state("");
  const invalid = $derived(attempted && !email.trim());
</script>

<form
  class="stack"
  novalidate
  onsubmit={(event) => {
    event.preventDefault();
    attempted = true;
    status = email.trim() ? "Address accepted for this example." : "Please correct the email field.";
  }}
>
  <div class="stack control-gap">
    <label for="contact-email">Email (required)</label>
    <input
      id="contact-email"
      type="email"
      autocomplete="email"
      required
      bind:value={email}
      aria-invalid={invalid || undefined}
      aria-describedby={invalid ? "email-help email-error" : "email-help"}
    />
    <p class="feedback" id="email-help">Use an address where we can reply.</p>
    {#if invalid}<p class="feedback" data-tone="error" id="email-error">Enter your email address.</p>{/if}
  </div>
  <Button type="submit" label="Continue" />
  <p class="feedback" data-tone={invalid ? "error" : "success"} role="status">{status}</p>
</form>
```

This example demonstrates presence validation only, not email syntax checking or a real submission. Native constraints or app validation must implement the actual policy. For asynchronous work, keep progress and result announcements outside the region marked busy so they are not deferred by that region.

## Native choice groups

Keep checkbox and radio controls native. Their appearance is replaced by a 17px box on the `--background-prominent` fill with a 1px `--border-prominent` boundary. The faint boundary stays below the 3:1 contrast of a control boundary; keep a visible label beside each box. Checkboxes are square and radios round. Checked controls fill a 9px accent square or dot, and an indeterminate checkbox shows a 9 × 3px accent bar. Keyboard focus draws a `--border-width` ring in `--focus`, 2px outside the box; `disabled` dims the control and `aria-invalid="true"` draws the boundary in the error color. Forced-colors mode restores native rendering.

Group related options in a fieldset with a visible legend, label every option, and associate group help and errors with aria-describedby. Option labels can use local spacing and the shared control minimum to provide a usable click target.

Use native checked state and keyboard behavior, without custom roles or duplicate ARIA state. A radio group shares one name; names must be distinct across unrelated forms on the same page. For a group requiring at least one checkbox, validate the selected array rather than requiring each checkbox. See the [complete form recipe](../guides/recipe-form.md) for working groups and request states.
