Field
A label.field wrapper that stacks a label, any form
control, and an optional description. Works with any input type,
select, or progress.
Default
<label class="field">
<span>Email</span>
<input type="email" placeholder="you@example.com" />
<small>We'll never share your email.</small>
</label>
Required
Add required to the input and a * appears
automatically on the label.
<label class="field">
<span>Username</span>
<input type="text" placeholder="johndoe" required />
</label>
Error messages
Add one <small data-error="..."> per error type
and the right one shows automatically, no JavaScript involved. It
works off native validity state:
:user-invalid only matches after the user has
interacted with the field (so errors don't show on page load),
:placeholder-shown tells apart an empty required field
from one with a bad value, and :out-of-range catches
min/max violations. Each message
animates in with @starting-style. Try submitting the
form below empty, then with an out-of-range age, then with an
invalid email.
The novalidate attribute on the
<form> suppresses the browser's own validation
bubble, which can't be restyled. It only disables that
automatic popup and submit-blocking, the
:invalid/:user-invalid/:out-of-range
pseudo-classes above still work exactly the same, so this stays
entirely CSS-driven.
<form novalidate>
<label class="field">
<span>Email</span>
<input type="email" placeholder="you@example.com" required />
<small data-error="required">Email is required.</small>
<small data-error="invalid">Enter a valid email address.</small>
</label>
<label class="field">
<span>Age</span>
<input type="number" placeholder="18" min="18" max="99" required />
<small data-error="required">Age is required.</small>
<small data-error="range">Must be between 18 and 99.</small>
</label>
</form>
With textarea
<label class="field">
<span>Message</span>
<textarea placeholder="Write something..."></textarea>
<small>Max 500 characters.</small>
</label>
With progress
<label class="field">
<span>Uploading…</span>
<progress value="65" max="100"></progress>
<small>65%</small>
</label>
<label class="field">
<span>Processing</span>
<progress></progress>
</label>
With slider
<label class="field">
<span>Volume</span>
<input type="range" value="60" />
</label>
With checkbox
<div class="field">
<span>Notifications</span>
<label><input type="checkbox" checked /> Email</label>
<label><input type="checkbox" /> SMS</label>
<label><input type="checkbox" /> Push</label>
</div>
With radio
<div class="field">
<span>Plan</span>
<label><input type="radio" name="plan" checked /> Free</label>
<label><input type="radio" name="plan" /> Pro</label>
<label><input type="radio" name="plan" /> Enterprise</label>
<small>You can upgrade at any time.</small>
</div>
Form
<form>
<label class="field">
<span>Name</span>
<input type="text" placeholder="Evil Rabbit" required />
</label>
<label class="field">
<span>Email</span>
<input type="email" placeholder="john@example.com" required />
<small>We'll never share your email with anyone.</small>
</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem">
<label class="field">
<span>Phone</span>
<input type="tel" placeholder="+1 (555) 123-4567" />
</label>
<div class="field">
<span>Country</span>
<button
type="button"
class="outlined"
popovertarget="country-dropdown"
style="justify-content: space-between"
>
<span>United States</span>
<svg><!-- chevron --></svg>
</button>
<div
id="country-dropdown"
popover
onchange="
document.querySelector(
`[popovertarget=${this.id}] span`,
).textContent = event.target.closest('label').textContent.trim();
this.hidePopover();
"
>
<menu>
<li>
<label
><input type="radio" name="country" value="us" checked /> United
States</label
>
</li>
<li>
<label
><input type="radio" name="country" value="uk" /> United
Kingdom</label
>
</li>
<li>
<label
><input type="radio" name="country" value="ca" /> Canada</label
>
</li>
</menu>
</div>
</div>
</div>
<label class="field">
<span>Address</span>
<input type="text" placeholder="123 Main St" />
</label>
<div>
<button class="ghost">Cancel</button>
<button>Submit</button>
</div>
</form>