Field
A label, description, error and required marker around any control, with the ids and aria attributes wired for you.
View as Markdown for coding assistants, or see llms.txt.
- New
- Group: Inputs
- Added in @azmr/ui 0.5.0
Usage
import { Field } from "@azmr/ui/field";
import { Input, Textarea } from "@azmr/ui/input";Nine digits, as on your IRD letter.
Printed at the bottom of every invoice.
<div className="flex w-full max-w-sm flex-col gap-4">
<Field label="Business name" required>
<Input autoComplete="organization" />
</Field>
<Field label="GST number" description="Nine digits, as on your IRD letter.">
<Input inputMode="numeric" placeholder="123-456-789" />
</Field>
<Field label="Notes" description="Printed at the bottom of every invoice.">
<Textarea placeholder="Thanks for your business." />
</Field>
</div>Field wraps one control. It gives the control an id, points aria-labelledby at the label and aria-describedby at the description, and keeps an id you set yourself. It works with Input, Textarea and any Base UI control.
Errors
<form noValidate onSubmit={onSubmit} className="flex w-full max-w-sm flex-col gap-4">
<Field label="Email" description="We send receipts here." required error={error}>
<Input
type="email"
autoComplete="email"
value={email}
onValueChange={(value) => {
setEmail(value);
setSent(false);
if (error) setError(emailError(value));
}}
/>
</Field>
<div className="flex items-center gap-3">
<Button type="submit">Save</Button>
<p role="status" className="text-sm text-muted-foreground">
{sent ? "Saved." : ""}
</p>
</div>
</form>Pass the message as error. The field is then invalid: the control gets aria-invalid="true", aria-errormessage and aria-describedby point at the message, and the border turns red. undefined, null, false and "" all mean no error.
The message sits in a polite live region that is in the page before any error arrives, so screen readers announce it when it appears without moving focus. Submit the form above while it is empty, then type a valid address.
required shows a marker after the label and sets aria-required on the control. The marker is hidden from screen readers, which hear "required" from the control instead. It doesn't turn on the browser's own validation, so add noValidate to the form and validate it yourself, or use the field's validate prop.
With react-hook-form
Input and Textarea forward the real element's ref, so register() works without a Controller. Pass errors.<name>?.message as error:
import { useForm } from "react-hook-form";
import { Button } from "@azmr/ui/button";
import { Field } from "@azmr/ui/field";
import { Input, Textarea } from "@azmr/ui/input";
type Values = { email: string; notes: string };
export function InvoiceForm({ onSave }: { onSave: (values: Values) => void }) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Values>();
return (
<form noValidate onSubmit={handleSubmit(onSave)}>
<Field label="Email" required error={errors.email?.message}>
<Input type="email" {...register("email", { required: "Enter your email address" })} />
</Field>
<Field label="Notes" description="Printed on the invoice.">
<Textarea {...register("notes")} />
</Field>
<Button type="submit">Save</Button>
</form>
);
}Built-in validation
Without an error prop, the field shows its own messages: the result of validate, or the browser's message for constraints such as type="email". validationMode sets when it checks (onSubmit by default, or onBlur or onChange).
<Field
label="Discount code"
validationMode="onBlur"
validate={(value) => (String(value).length < 4 ? "Codes are at least 4 characters" : null)}
>
<Input />
</Field>Any control
Field wires its label, description and error to Input, Textarea and the other Base UI controls by itself. For any other control, such as a Radix (shadcn) Select trigger, a native <select> or a date picker from another library, pass a function as the child. It gets the id, aria-labelledby, aria-describedby, aria-invalid, aria-required and aria-errormessage to spread on the element that takes focus:
<Field label="Country" description="Where the invoice is sent." error={errors.country?.message}>
{(control) => (
<select {...control} {...register("country")}>
<option>New Zealand</option>
<option>Australia</option>
</select>
)}
</Field>With React Hook Form, FormField's render passes the same props as control. See Forms.
Custom layouts
At least 12 characters.
<div className="w-full max-w-sm">
<FieldRoot>
<div className="flex items-baseline justify-between">
<FieldLabel required>Password</FieldLabel>
<a
href="#forgot-password"
className="text-sm text-primary underline-offset-4 hover:underline"
>
Forgot password?
</a>
</div>
<Input type="password" autoComplete="current-password" />
<FieldDescription>At least 12 characters.</FieldDescription>
</FieldRoot>
</div>For a layout Field doesn't cover, compose the parts: FieldRoot, FieldLabel, FieldDescription and FieldError. The wiring is the same. Pass match to FieldError to show it whenever the field is invalid, for example when another library owns validation.
Props
Prop
Type
disabled on the field disables its control. name takes precedence over the control's own name when the form is submitted.
Accessibility
- The label is a real
<label>, so clicking it focuses the control. - Errors are exposed through
aria-invalidandaria-errormessage, and read with the field througharia-describedby, never by colour alone. - New errors are announced politely through a live region, without moving focus.
- The required marker is decorative; the control itself carries
aria-required.
| Key | Action |
|---|---|
| Tab | Moves focus to the control. The label, description and error are not focusable. |
| ShiftorTab | Moves focus to the previous control. |