Skip to content

component :: Form Field

The .field component composes the bare form elements into label + control + hint + error message, with validation states driven entirely by CSS.

We'll never share it.That doesn't look like an email address.

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

We'll never share it. That doesn't look like an email address.

State

Content

Copied!
<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:

That doesn't look like an email address.
<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:

Contact us
Please fill out this field correctly.
That doesn't look like an email address.
Please fill out this field correctly.
---
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>