Field
A label, a control, a description and an error wired together, with native validation that never moves the layout.
Input, textarea, select
The field wires the label, the description and the error to the control by id.
Choose a unique username for your account.
Must be at least 8 characters long.
Share your thoughts about our service.
Select your department or area of work.
Code
<Field label="Username" description="Choose a unique username for your account.">
<Input value={account.username} placeholder="Max Leiter" />
</Field>
<Field label="Password" description="Must be at least 8 characters long.">
<Input type="password" value={account.password} attrs={{ minlength: 8 }} />
</Field>Usage
import { Field, FieldLabel, FieldDescription, FieldError, FieldContent, FieldTitle, FieldSet, FieldLegend, FieldGroup, FieldSeparator } from "@elements/ui/field";
<Field label="Email" description="We never share it." required>
<Input type="email" value={form.email} />
</Field>
<Field label="Username" validate={checkUsername} validationMode="onBlur">
<Input value={form.username} attrs={{ minlength: 3 }} />
<div slot="error">
<FieldError match="tooShort">At least 3 characters.</FieldError>
</div>
</Field>
<Field orientation="horizontal" label="Email me updates">
<Checkbox checked={form.updates} />
</Field>Examples
Validation
Native constraints plus validate(). Errors take the description's line, so nothing moves.
Code
<form onsubmit={() => signup(state)}>
<Field label="Email" required>
<Input type="email" value={state.email} />
<FieldError slot="error" match="valueMissing">Enter your email.</FieldError>
<FieldError slot="error" match="typeMismatch">That is not an email address.</FieldError>
</Field>
<Field label="Handle" validate={checkHandle} validationMode="onChange" validationDebounceTime={300}>
<Input value={state.handle} attrs={{ minlength: 3 }} />
</Field>
<Field label="Age" validate={checkAge} validationMode="onBlur" description="Whole years."
onValidityChange={(v) => log(v.valid, v.error, v.value, v.initialValue)}>
<Input type="number" value={state.age} />
</Field>
<Button type="submit" variant="primary">Sign up</Button>
</form>Async validation and form values
validate() may return a Promise, such as a server check; its message shows when it resolves. Its third argument holds the form's values by name.
Code
function checkAvailable(value: string): Promise<string | null> {
return fetch(`/api/handles/${value}`).then((r) => r.ok ? "That username is taken." : null);
}
<Field label="Username" validate={checkAvailable} validationMode="onBlur">
<Input value={state.username} />
</Field>
// formValues (third) has the form's values by name.
function checkConfirm(value: string, control: HTMLElement, formValues: FormValues): string | null {
return value && value !== formValues["new-password"] ? "Passwords do not match." : null;
}
<Field label="Confirm password" validate={checkConfirm} validationMode="onBlur">
<Input type="password" value={state.confirm} />
</Field>Fieldset
A native fieldset with a legend groups related fields for assistive tech.
Code
<FieldSet>
<FieldLegend>Address Information</FieldLegend>
<FieldDescription>We need your address to deliver your order.</FieldDescription>
<FieldGroup>
<Field label="Street Address"><Input value={address.street} /></Field>
<div class="grid">
<Field label="City"><Input value={address.city} /></Field>
<Field label="Postal Code"><Input value={address.zip} /></Field>
</div>
</FieldGroup>
</FieldSet>Payment method
Fieldsets, a separator, selects, a checkbox and a textarea in one form. A required Select shows its error like any other control.
Code
<form onsubmit={() => pay(payment)}>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>All transactions are secure and encrypted</FieldDescription>
<FieldGroup>
<Field label="Name on Card" required><Input value={payment.name} /></Field>
<Field label="Card Number" description="Enter your 16-digit card number" required>
<Input value={payment.card} attrs={{ inputmode: "numeric", pattern: "[0-9 ]{16,19}" }} />
</Field>
<div class="grid is-3">
<Field label="Month" required>
<Select value={payment.month} items={MONTHS} placeholder="MM" block />
<FieldError slot="error" match="valueMissing">Pick a month.</FieldError>
</Field>
<Field label="Year" required>
<Select value={payment.year} items={YEARS} placeholder="YYYY" block />
<FieldError slot="error" match="valueMissing">Pick a year.</FieldError>
</Field>
<Field label="CVV" required>
<Input value={payment.cvv} attrs={{ inputmode: "numeric" }} />
<FieldError slot="error" match="valueMissing">Enter the CVV.</FieldError>
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>The billing address associated with your payment method</FieldDescription>
<Field orientation="horizontal" label="Same as shipping address">
<Checkbox checked={payment.sameAsShipping} />
</Field>
</FieldSet>
<FieldSeparator />
<Field label="Comments"><Textarea value={payment.comments} /></Field>
<Field orientation="horizontal">
<Button type="submit" variant="primary">Submit</Button>
<Button type="reset">Cancel</Button>
</Field>
</FieldGroup>
</form>A control's own message
A control that checks itself, such as NumberInput's step check, keeps its message inside a Field, which shows it like any other.
Sold in packs of 5, starting at 1. Off a pack size, the message names the two nearest.
Code
<Field label="Quantity" description="Sold in packs of 5, starting at 1. Off a pack size, the message names the two nearest." validationMode="onBlur">
<NumberInput value={order.quantity} min={1} step={5} />
</Field>Checkbox, radio and switch
Horizontal fields put the label beside the control.
Your Desktop & Documents folders are synced with iCloud Drive.
Require a second step when you sign in.
Code
<FieldSet>
<FieldLegend variant="label">Show these items on the desktop</FieldLegend>
<Field orientation="horizontal" label="Hard disks"><Checkbox checked={prefs.hard} /></Field>
<Field orientation="horizontal" label="External disks"><Checkbox checked={prefs.external} /></Field>
</FieldSet>
<Field orientation="horizontal" label="Sync Desktop & Documents folders"
description="Your folders are synced to iCloud Drive.">
<Checkbox checked={prefs.sync} />
</Field>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel>Multi-factor authentication</FieldLabel>
<FieldDescription>Require a second step when you sign in.</FieldDescription>
</FieldContent>
<Switch checked={prefs.mfa} />
</Field>Choice cards
A FieldLabel wrapping a Field makes the whole card the label.
Code
<RadioGroup value={prefs.compute} attrs={{ "aria-label": "Compute environment" }}>
<FieldLabel>
<Field orientation="horizontal" reserveError={false}>
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>Run GPU workloads on a K8s cluster.</FieldDescription>
</FieldContent>
<RadioGroupItem value="kubernetes" />
</Field>
</FieldLabel>
...
</RadioGroup>Field group with separators
FieldSeparator divides sections, with optional text.
Get notified when a long task finishes.
Get notified when tasks you created have updates.
Code
<FieldGroup>
<Field orientation="horizontal" label="Responses" description="Get notified when ChatGPT responds."><Checkbox checked /></Field>
<FieldSeparator />
<Field orientation="horizontal" label="Tasks"><Checkbox /></Field>
<FieldSeparator>Or</FieldSeparator>
<Field label="Notes"><Textarea /></Field>
</FieldGroup>Responsive
responsive is a column in a narrow group and a row in a wide one (a container query).
Provide your full name for identification.
We use it for receipts.
Code
<FieldGroup>
<Field orientation="responsive" label="Name" description="Provide your full name for identification.">
<Input value={profile.name} />
</Field>
...
</FieldGroup>Disabled and server errors
disabled reaches every control; error shows a message from elsewhere, such as the server.
Shown on your profile.
Code
<Field label="Name" disabled={state.disabled} error={state.serverError}>
<Input value={state.name} />
</Field>Disabled choices
disabled on a field or a field set dims each checkbox, switch and radio with its label.
Errors from a schema
errors takes the issues a schema validator returns; several render as a list and mark the field invalid.
Checked as you type.
- Use at least 8 characters.
- Include a number.
Code
// issues from a schema validator, such as Zod's error.issues
<Field label="Password" errors={passwordIssues(state.password)}>
<Input type="password" value={state.password} />
</Field>Error parts
FieldError match picks a ValidityState key; always shows it whatever the state, and valid shows once the field passes.
Code
<Field label="Coupon" invalid>
<Input value="SPRING21" />
<FieldError slot="error" match="always">This coupon has expired.</FieldError>
</Field>
<Field label="Nickname" validationMode="onChange">
<Input value={state.nickname} attrs={{ minlength: 2 }} />
<FieldError slot="error" match="tooShort">Use at least two letters.</FieldError>
<FieldError slot="error" match="valid">Looks good.</FieldError>
</Field>Controlled dirty and touched
dirty and touched take the state from a form library; unset, the field tracks them.
Marked dirty and touched by the caller.
dirty is false, so typing does not mark it.
Right to left
Label, description and a horizontal field follow the inline direction.
يظهر في ملفك الشخصي.