Field
Field — the wrapper around one form control: its label, the control itself, a help text and an error message.
Examples
The resting state: a label, a box, nothing else.
The asterisk is read from the attribute, not from a class.
Enter an email address in the format name@example.com.
<div class="field">
<label class="field-label" for="state-idle">Idle</label>
<input class="input" id="state-idle" name="idle" type="text" />
<p class="field-help">The resting state: a label, a box, nothing else.</p>
</div>
<div class="field">
<label class="field-label" for="state-filled">Filled</label>
<input class="input" id="state-filled" name="filled" type="text" value="ada@example.com" />
</div>
<div class="field">
<label class="field-label" for="state-required">Required</label>
<input class="input" id="state-required" name="req" type="text" required
aria-describedby="state-required-help" />
<p class="field-help" id="state-required-help">The asterisk is read from the attribute, not from a class.</p>
</div>
<div class="field">
<label class="field-label" for="state-invalid">Invalid</label>
<input class="input" id="state-invalid" name="bad" type="email" value="ada@"
aria-invalid="true" aria-describedby="state-invalid-error" />
<p class="field-error" id="state-invalid-error">Enter an email address in the format name@example.com.</p>
</div>
<div class="field">
<label class="field-label" for="state-readonly">Read-only</label>
<input class="input" id="state-readonly" name="ro" type="text" value="acme-4831-ff09" readonly />
</div>
<div class="field">
<label class="field-label" for="state-disabled">Disabled</label>
<input class="input" id="state-disabled" name="off" type="text" value="Not available on this plan" disabled />
</div>All six on the page at once, because the alternative is a page that documents an error state you have to produce yourself to see.
Only one of them is faked: the invalid field carries aria-invalid="true", which is what a server would send back. Client-side, the state you want is :user-invalid — it fires when the user has finished with the field rather than while they are still typing it, which is the difference between a form that helps and a form that shouts. The example below is the live one.
We only use it to reply.
Enter a valid email address.
<div class="field">
<label class="field-label" for="demo-email">Email</label>
<input class="input" id="demo-email" name="email" type="email" required
aria-describedby="demo-email-help demo-email-error" />
<p class="field-help" id="demo-email-help">We only use it to reply.</p>
<p class="field-error" id="demo-email-error">Enter a valid email address.</p>
</div>Type something invalid and leave the field. The error element is in the DOM at all times and CSS reveals it, which is why aria-describedby can point at it unconditionally: a display: none description is not announced, so the association is inert exactly while the field is valid and live the moment it is not.
At least 12 characters. A passphrase is fine.
<div class="field">
<label class="field-label" for="demo-pw">Password</label>
<div class="field-control" data-ui="password">
<input class="input input-password" id="demo-pw" name="password" type="password"
value="correct-horse" autocomplete="current-password"
aria-describedby="demo-pw-help" />
<button class="btn btn-quiet btn-icon field-action" type="button"
aria-pressed="false" aria-controls="demo-pw">
<svg class="icon field-action-off" aria-hidden="true"><use href="#icon-eye"></use></svg>
<svg class="icon field-action-on" aria-hidden="true"><use href="#icon-eye-off"></use></svg>
<span class="sr-only">Show password</span>
</button>
</div>
<p class="field-help" id="demo-pw-help">At least 12 characters. A passphrase is fine.</p>
</div>.input-password is a real variant and not decoration. Every engine masks a password with the font’s own bullet glyph, and in most text faces that glyph sits low in the line — so the row of dots paints against the bottom of the box while a placeholder in the same field is centred, which reads as a broken control.
A monospace face centres it, and it is the right family for the revealed value too. The button’s accessible name never changes — aria-pressed carries the state, the glyph follows it in CSS, and the module writes nothing but the type attribute and that one boolean.
<div class="field">
<label class="field-label" for="type-date">Start date</label>
<input class="input" id="type-date" name="start" type="date" min="2026-01-01" />
</div>
<div class="field">
<label class="field-label" for="type-time">Doors open</label>
<input class="input" id="type-time" name="doors" type="time" />
</div>
<div class="field">
<label class="field-label" for="type-month">Expiry</label>
<input class="input" id="type-month" name="expiry" type="month" />
</div>
<div class="field">
<label class="field-label" for="type-tel">Phone</label>
<input class="input" id="type-tel" name="phone" type="tel" autocomplete="tel" />
</div>
<div class="field">
<label class="field-label" for="type-url">Website</label>
<input class="input" id="type-url" name="url" type="url" placeholder="https://" />
</div>There is no date-picker component and there never should have been.
The browser ships the calendar, localises it, wires the arrow keys and renders a native wheel on a phone; date-picker.css was two hundred lines of ::-webkit-datetime-edit-* styling behind a class you had to remember to add, so it moved into input.css and the class went. <input class="input" type="date"> is the whole API, and a date field now measures exactly what the text field beside it measures in all three engines. The one honest limit: the popup is not yours, and color-scheme — which the mode axis already sets — is the only lever over it.
<form class="form" autocomplete="on">
<div class="field">
<label class="field-label" for="autofill-name">Full name</label>
<input class="input" id="autofill-name" name="name" type="text" autocomplete="name" />
</div>
<div class="field">
<label class="field-label" for="autofill-email">Email</label>
<input class="input" id="autofill-email" name="email" type="email" autocomplete="email" />
</div>
</form>An autofilled field paints its own background, and Chromium's is a yellow that owes nothing to any skin and is unreadable in dark mode — background-color does not reach it.
input.css covers it with an inset box-shadow and sets -webkit-text-fill-color, which are the only two levers there are. The field is deliberately left looking like every other one: "the browser filled this" is already conveyed, and a second signal would be one more colour role no skin defines. The part only you can write is autocomplete — without a correct token the browser has nothing to fill.
Tokens 11
Level 2, declared on .field itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --field-gap | var(--space-2) |
| --field-error-color | var(--color-danger) |
| --field-error-font-size | var(--font-size-xs) |
| --field-help-color | var(--color-on-surface-muted) |
| --field-help-font-size | var(--font-size-xs) |
| --field-inline-gap | var(--space-4) |
| --field-label-color | var(--color-on-surface) |
| --field-label-font-family | var(--font-secondary) |
| --field-label-font-size | var(--font-size-sm) |
| --field-label-font-weight | var(--font-weight-medium) |
| --field-required-color | var(--color-danger) |
Variants and states
Variants
None. It is one shape, and the page does the rest.
Inside it
.field-error.field-help.field-label.field-action.field-inline.field-control.field-action-off.field-action-on.field-set.input.select
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
:user-invalid[required]
Before you ship it
What you have to do 3 requirements
- Associate the label:
<label for>pointing at the control'sid, or wrap the control in the<label>. A placeholder is not a label. - Associate the help and error text with
aria-describedbyon the control, listing both ids, space separated. Without it the message is visible and silent — a screen reader user reaches the field, hears the label, and is never told why it was rejected. Nothing in CSS can do this for you. - On a
<fieldset>, keep the<legend>as the first child; it is what names the group. The error element stays in the DOM at all times and is revealed by CSS. Pointaria-describedbyat it unconditionally: adisplay: nonedescription is not announced, so the association is inert exactly while the field is valid, and live the moment it is not.
src/css/components/field.css · npx mostlycss add field