# Form Field

> Part of mCSS (mcss.dev). Rendered page: https://mcss.dev/components/field

The `.field` component composes the bare [form elements](/docs/elements) into label + control + hint + error message, with validation states driven entirely by CSS.

<form>
  <Field label="Email" name="demo-email" type="email" required hint="We'll never share it." placeholder="you@example.com" error="That doesn't look like an email address." />
</form>

Type something that isn't an email address and tab away: the hint swaps for the error and the border turns red, no JavaScript involved.

<div class="docs_oversizedTable">

| File                                | Description                             | Source      |
| ----------------------------------- | ---------------------------------------- | ----------- |
| `component.field.css`               | Field structure and states               | [Github][1] |
| `component.fieldRow.css`            | fieldRow composition (own block)         | [Github][5] |
| `elements.form.css`                 | Bare element styles + `:user-invalid` border | [Github][2] |
| `Field.astro`                       | The Astro component                      | [Github][3] |
| `--field-*` block in `settings.ui.css`  | Interface tokens (see table below)           | [Github][4] |

</div>

[1]: https://github.com/minimaldesign/mCSS/blob/main/src/styles/framework/component.field.css
[2]: https://github.com/minimaldesign/mCSS/blob/main/src/styles/framework/elements.form.css
[3]: https://github.com/minimaldesign/mCSS/blob/main/src/components/Field.astro
[4]: https://github.com/minimaldesign/mCSS/blob/main/src/styles/framework/settings.ui.css
[5]: https://github.com/minimaldesign/mCSS/blob/main/src/styles/framework/component.fieldRow.css

## Playground

<Playground
  client:visible
  template={`<div class="{classes}">
  <label for="pg-email">{label}</label>
  <input id="pg-email" name="email" type="email"{attrs} aria-describedby="pg-email-hint" />
  <small class="field_hint" id="pg-email-hint">{hint}</small>
  <small class="field_error">{error}</small>
</div>`}
  baseClasses="field"
  controls={[
    { heading: "State", items: [
      { type: "checkbox", name: "required", label: "Required", attr: "required", default: true },
      { type: "checkbox", name: "disabled", label: "Disabled", attr: "disabled", default: false },
      { type: "checkbox", name: "invalid", label: "Invalid", value: "is-invalid", default: false },
    ]},
    { heading: "Content", items: [
      { type: "text", name: "label", label: "Label", default: "Email" },
      { type: "text", name: "hint", label: "Hint", default: "We'll never share it." },
      { type: "text", name: "error", label: "Error", default: "That doesn't look like an email address." },
    ]},
  ]}
/>

## HTML

```html
<div class="field">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required aria-describedby="email-hint" />
  <small class="field_hint" id="email-hint">We'll never share it.</small>
  <small class="field_error">That doesn't look like an email address.</small>
</div>
```

Errors use `:user-invalid`, so a pristine form never lights up red: only fields the user actually touched and got wrong. The error message is always in the markup and revealed by `.field:has(:user-invalid)`. Add `.is-invalid` on the `.field` wrapper to force the same presentation from markup, for server-side validation results, tests, and demos (that's what the playground's Invalid checkbox does). It is deliberately **not** referenced by `aria-describedby`: screen readers get the browser's native validation message on submit, so wiring the visual error in as a description would double-announce it.

### Custom properties

<div class="docs_oversizedTable">

| Property                       | Description                                |
| ------------------------------ | ------------------------------------------ |
| `--field-spacing`              | Gap between label, control, and messages.  |
| `--field-hint-color`           | Hint text color.                           |
| `--field-error-color`          | Error text color.                          |
| `--input-border-color-invalid` | Border color of a `:user-invalid` control. |

</div>

## Astro component

<div class="docs_oversizedTable">

| Prop       | Type      | Default                | Description                                                        |
| ---------- | --------- | ---------------------- | ------------------------------------------------------------------ |
| `label`    | `string`  | —                      | The visible label. Required.                                       |
| `hint`     | `string`  | `undefined`            | Help text under the control.                                       |
| `error`    | `string`  | `"Please fill out…"`   | Message revealed when the control is `:user-invalid`.              |
| `class`    | `string`  | `undefined`            | Additional CSS classes on the `.field` wrapper.                    |

</div>

Everything else (`type`, `name`, `placeholder`, `required`, `pattern`, `minlength`…) is passed through to the `<input>`. The label's `for` uses `id`, falling back to `name` — set at least one.

To use another control (textarea, select…), put it in the default slot and give it the matching `id`:

```astro
<Field label="Message" id="message">
  <textarea id="message" name="message" required></textarea>
</Field>
```

## Recipes

### Newsletter signup

`.fieldRow` lines fields and buttons up on one wrapping row. Mix `.fieldRow_field` onto each field that should stretch:

<form class="fieldRow">
  <Field class="fieldRow_field" label="Email" name="newsletter-email" type="email" required placeholder="you@example.com" error="That doesn't look like an email address." />
  <button class="bt bt-primary" type="submit">Subscribe</button>
</form>

```html
<form class="fieldRow" method="post" action="/subscribe">
  <div class="field fieldRow_field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />
  </div>
  <button class="bt bt-primary" type="submit">Subscribe</button>
</form>
```

### Contact form

Fieldset + the [grid system](/docs/global#grid) for the two-column row:

<form>
  <fieldset>
    <legend>Contact us</legend>
    <p aria-hidden="true">Contact us</p>
    <div class="grid" col="1" col-md="2" style="margin-bottom: var(--sm1);">
      <Field label="Name" name="contact-name" required />
      <Field label="Email" name="contact-email" type="email" required error="That doesn't look like an email address." />
    </div>
    <Field label="Message" id="contact-message" class="mb-sm1">
      <textarea id="contact-message" name="message" required></textarea>
    </Field>
    <button class="bt bt-primary" type="submit">Send</button>
  </fieldset>
</form>

```astro
---
import Field from "../components/Field.astro";
---
<form method="post" action="/contact">
  <fieldset>
    <legend>Contact us</legend>
    <p aria-hidden="true">Contact us</p>
    <div class="grid" col="1" col-md="2">
      <Field label="Name" name="name" required />
      <Field label="Email" name="email" type="email" required />
    </div>
    <Field label="Message" id="message">
      <textarea id="message" name="message" required></textarea>
    </Field>
    <button class="bt bt-primary" type="submit">Send</button>
  </fieldset>
</form>
```
