form-field
Accessible form field web component with label, input, validation timing, custom error messages, and error summaries. Progressive enhancement from CSS-only validation to full JS coordination.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
data-type |
checkbox, radio, file |
- | Special styling for non-text inputs |
data-no-icon |
boolean | - | Disable validation status icons |
data-required |
boolean | - | Visual indicator (auto-detected from input) |
data-valid |
boolean | - | Force valid state styling |
data-invalid |
boolean | - | Force invalid state styling |
Input Attributes (for JS enhancements)
| Attribute | Values | Default | Description |
|---|---|---|---|
data-type (on input) |
otp, pin |
- | Enable OTP/PIN multi-box enhancement |
data-length (on input) |
4, 6, 8 |
6 |
Number of digits for OTP/PIN input |
Element Structure
The form-field element groups a label, input control, and optional validation message.
| Child Element | Purpose | Key Attributes |
|---|---|---|
<label> |
Accessible field label | for (matches input id) |
<input> / <textarea> / <select> |
Form control | id, name, validation attrs, aria-describedby |
<output> |
Validation/help message | id, for, aria-live="polite" |
<form-field> <label for="name">Full Name</label> <input type="text" id="name" name="name" required minlength="2" autocomplete="name" aria-describedby="name-msg" /> <output id="name-msg" for="name" aria-live="polite"> At least 2 characters required </output></form-field>
Input Types
Works with all standard form controls: text, email, password, textarea, and select.
<form-field> <label for="category">Category</label> <select id="category" name="category" required> <option value="" disabled selected>Select a category...</option> <option value="general">General Inquiry</option> <option value="support">Technical Support</option> <option value="sales">Sales Question</option> </select></form-field>
Password Toggle
Password inputs are automatically enhanced with a show/hide toggle button when JavaScript is available. No extra markup needed.
- Appears automatically when JavaScript loads
- Uses
aria-pressedandaria-labelfor accessibility - Positions adjust alongside validation icons
<form-field> <label for="password">Password</label> <input type="password" id="password" name="password" required minlength="8" autocomplete="current-password" aria-describedby="password-msg" /> <output id="password-msg" class="error" for="password" aria-live="polite"> At least 8 characters required. </output></form-field><!-- JS auto-adds show/hide toggle button -->
Validation Status Icons
Form fields automatically display a checkmark (valid) or X (invalid) after user interaction. Icons are positioned inside the input's padding. Use data-no-icon to disable them.
<!-- Default: validation icons appear --><form-field> <label for="email">Email</label> <input type="email" id="email" name="email" required /></form-field> <!-- Opt-out: no validation icons --><form-field data-no-icon> <label for="search">Search</label> <input type="search" id="search" name="q" /></form-field>
OTP/PIN Input
Use data-type="otp" on the input for verification codes. Progressive enhancement: works as a standard text input without JS, enhances to multi-box with JS.
- Auto-focus moves to the next box after entering a digit
- Backspace moves focus to the previous box
- Arrow keys navigate between boxes
- Paste support fills multiple boxes at once
<form-field data-no-icon> <label for="code">Verification code</label> <input type="text" id="code" name="code" data-type="otp" data-length="6" inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code" required aria-describedby="code-msg" /> <output id="code-msg" class="hint" for="code" aria-live="polite"> Enter the 6-digit code from your authenticator app. </output></form-field>
Output Message Classes
Use class="hint" and class="error" on output elements for different message types.
| Class | Visibility | Use Case |
|---|---|---|
.hint |
Always visible (hides when valid) | Format requirements, help text |
.error |
Only when invalid | Error messages |
<form-field> <label for="username">Username</label> <input type="text" id="username" name="username" required minlength="3" maxlength="20" aria-describedby="username-hint username-error" /> <output id="username-hint" class="hint" for="username"> 3-20 characters, letters and numbers only. </output> <output id="username-error" class="error" for="username" aria-live="polite"> Please enter a valid username. </output></form-field>
Checkbox and Radio
Use data-type="checkbox" or data-type="radio" for inline label+input layout. Wrap radio groups in a <fieldset> with a <legend>.
<!-- Single Checkbox --><form-field data-type="checkbox"> <label> <input type="checkbox" id="terms" name="terms" required /> I agree to the Terms of Service </label></form-field> <!-- Radio Group --><fieldset> <legend>Preferred Contact Method</legend> <layout-stack data-layout-gap="xs"> <form-field data-type="radio"> <label> <input type="radio" name="contact" value="email" required /> Email </label> </form-field> <form-field data-type="radio"> <label> <input type="radio" name="contact" value="phone" /> Phone </label> </form-field> </layout-stack></fieldset>
CSS-Only Validation
Uses :user-valid and :user-invalid pseudo-classes to show validation states only after user interaction, preventing premature error states on page load.
/* Valid state - green border after user interacts */form-field:has(input:user-valid) { & input { border-color: var(--color-success); } & output { color: var(--color-success); }} /* Invalid state - red border after user interacts */form-field:has(input:user-invalid) { & input { border-color: var(--color-error); } & output { color: var(--color-error); }}
JS-Enhanced Validation
Add data-validate to a <form> to enable features CSS can't handle: custom error messages, cross-field validation, checkbox group constraints, and error summaries. Without it, everything works as pure CSS validation.
Form Attributes
| Attribute | On | Description |
|---|---|---|
data-validate |
<form> |
Opt-in. No value = field-level errors (+ summary if present). "summary" = summary only. |
data-submit |
<form> |
Submit mode: "native" (default), "event" (dispatches vb:submit), "fetch" (async with loading/success/error states). |
Custom Error Message Attributes
Attribute names map directly to ValidityState keys. Place these on the input element.
| Attribute | ValidityState Key | When Triggered |
|---|---|---|
data-message-valuemissing |
valueMissing |
required field is empty |
data-message-typemismatch |
typeMismatch |
type="email" / type="url" format wrong |
data-message-patternmismatch |
patternMismatch |
pattern regex fails |
data-message-tooshort |
tooShort |
Below minlength |
data-message-toolong |
tooLong |
Above maxlength |
data-message-rangeunderflow |
rangeUnderflow |
Below min |
data-message-rangeoverflow |
rangeOverflow |
Above max |
data-message-stepmismatch |
stepMismatch |
Not on step interval |
Cross-Field and Group Attributes
| Attribute | On | Description |
|---|---|---|
data-match |
input | ID of field that must have the same value |
data-message-match |
input | Custom message when data-match fails |
data-min-checked |
fieldset | Minimum number of checked checkboxes |
data-max-checked |
fieldset | Maximum number of checked checkboxes |
Web Component API
<form-field> upgrades to a web component when JS loads. It provides these methods for programmatic use:
| Method / Property | Returns | Description |
|---|---|---|
validate() |
boolean |
Validate the field and update visual state. Returns validity. |
setError(message) |
- | Set a programmatic error (e.g. from server validation). |
clearError() |
- | Clear a programmatic error and reset state. |
fieldName |
string |
Human-readable field name from the label. |
input |
HTMLElement |
The controlled input element. |
<form data-validate> <form-field> <label for="email">Email</label> <input type="email" id="email" name="email" required minlength="5" data-message-valuemissing="Email is required" data-message-typemismatch="Please enter a valid email address" data-message-tooshort="Email seems too short" aria-describedby="email-error" /> <output id="email-error" class="error" for="email" aria-live="polite"></output> </form-field> <button type="submit">Submit</button></form>
<form data-validate> <form-field> <label for="password">Password</label> <input type="password" id="password" name="password" required minlength="8" data-message-valuemissing="Password is required" data-message-tooshort="At least 8 characters" autocomplete="new-password" aria-describedby="password-error" /> <output id="password-error" class="error" for="password" aria-live="polite"></output> </form-field> <form-field> <label for="confirm">Confirm Password</label> <input type="password" id="confirm" name="confirm" required data-match="password" data-message-valuemissing="Please confirm your password" data-message-match="Passwords do not match" autocomplete="new-password" aria-describedby="confirm-error" /> <output id="confirm-error" class="error" for="confirm" aria-live="polite"></output> </form-field> <button type="submit">Create Account</button></form>
<form data-validate> <fieldset data-min-checked="1" data-max-checked="3" data-message-min-checked="Select at least one" data-message-max-checked="Maximum 3 allowed"> <legend>Interests</legend> <form-field data-type="checkbox"> <label><input type="checkbox" name="interests" value="music" /> Music</label> </form-field> <form-field data-type="checkbox"> <label><input type="checkbox" name="interests" value="sport" /> Sport</label> </form-field> <form-field data-type="checkbox"> <label><input type="checkbox" name="interests" value="tech" /> Tech</label> </form-field> <output aria-live="polite"></output> </fieldset> <button type="submit">Save</button></form>
Error Summary
Add an <output class="error-summary"> inside your form for a consolidated list of all errors on submit.
| Mode | Attribute | Behavior |
|---|---|---|
| Field-level only | data-validate (no summary output) |
Errors appear inline next to each field |
| Summary only | data-validate="summary" |
Summary at top, field-level errors hidden |
| Both | data-validate + summary output present |
Summary at top AND inline errors on each field |
<!-- Authored summary pattern (recommended) --><form data-validate> <div data-form-summary role="alert" tabindex="-1" hidden> <h2>There are problems with your submission</h2> <ul data-summary-list></ul> </div> <form-field> <label for="name">Name</label> <input id="name" type="text" required data-message-valuemissing="Name is required" aria-describedby="name-error" /> <output id="name-error" class="error" for="name" aria-live="polite"></output> </form-field> <button type="submit">Submit</button></form> <!-- Summary only (field-level errors hidden) --><form data-validate="summary"> <output class="error-summary" role="alert" aria-live="polite" tabindex="-1"></output> <!-- ... fields ... --></form>
Complete Contact Form
<form action="/api/contact" method="POST"> <layout-stack data-layout-gap="m"> <form-field> <label for="name">Name</label> <input type="text" id="name" name="name" required minlength="2" autocomplete="name" aria-describedby="name-msg" /> <output id="name-msg" for="name" aria-live="polite"> Please enter your name </output> </form-field> <form-field> <label for="email">Email</label> <input type="email" id="email" name="email" required autocomplete="email" aria-describedby="email-msg" /> <output id="email-msg" for="email" aria-live="polite"> Please enter a valid email </output> </form-field> <form-field> <label for="subject">Subject</label> <select id="subject" name="subject" required> <option value="" disabled selected>Select a subject...</option> <option value="general">General Inquiry</option> <option value="support">Technical Support</option> <option value="feedback">Feedback</option> </select> </form-field> <form-field> <label for="message">Message</label> <textarea id="message" name="message" required minlength="10" rows="5" aria-describedby="message-msg"></textarea> <output id="message-msg" for="message" aria-live="polite"> At least 10 characters </output> </form-field> <button type="submit">Send Message</button> </layout-stack></form>
Why Use <output> for Messages?
- Purpose-built: Represents the result of a calculation or user action
- Native association: Has
forattribute to link to input(s) - Accessible: Works naturally with
aria-livefor screen readers - No JavaScript required: Can show different states via CSS
Accessibility Checklist
- Every
<input>has a<label>with matchingfor/id - Required fields use the
requiredattribute - Validation messages use
<output>witharia-live="polite" - Inputs link to messages via
aria-describedby - Form controls have appropriate
autocompletevalues - Error states don't rely on color alone (include text)
Related
<layout-stack>— Stack form fields vertically<layout-cluster>— Group buttons horizontally<form>— Native form element<input>— Native input element<select>— Native select element