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.
| File | Description | Source |
|---|---|---|
component.field.css |
Field structure and states | Github |
component.fieldRow.css |
fieldRow composition (own block) | Github |
elements.form.css |
Bare element styles + :user-invalid border |
Github |
Field.astro |
The Astro component | Github |
--field-* block in settings.ui.css |
Interface tokens (see table below) | Github |
Playground
State
Content
<div class="field">
<label for="pg-email">Email</label>
<input id="pg-email" name="email" type="email" required aria-describedby="pg-email-hint" />
<small class="field_hint" id="pg-email-hint">We'll never share it.</small>
<small class="field_error">That doesn't look like an email address.</small>
</div>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
| Property | Description |
|---|---|
--field-spacing |
Gap between label, control, and messages. |
--field-hint-color |
Hint text color. |
--field-error-color |
Error text color. |
--field-message-position |
static (default) keeps messages in flow under the control; absolute overlays them below the field so they can’t move siblings. .fieldRow sets absolute. |
--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. Mix .fieldRow_field onto each field that should stretch. The row sets --field-message-position: absolute, so hints and errors overlay below the row instead of pushing the button out of line with the input (try it: type a bad address and tab away). Leave a little vertical room under the row for them:
<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 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>