component :: Form Field
The .field component composes the bare form elements into label + control + hint + error message, with validation states driven entirely by CSS.
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.
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). 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
| 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. |
Astro component
| 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. |
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:
<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:
<form class="fieldRow" method="post" action="/subscribe"> <div class="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 for the two-column row:
---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>