AZMARA PLATFORM
Components

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.

Usage

import { Field } from "@azmr/ui/field";
import { Input, Textarea } from "@azmr/ui/input";
Preview theme
Preview density

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>
Open as text

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

Preview theme
Preview density

We send receipts here.

<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>
Open as text

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

Preview theme
Preview density

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>
Open as text

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-invalid and aria-errormessage, and read with the field through aria-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.
Keyboard interactions
KeyAction
TabMoves focus to the control. The label, description and error are not focusable.
ShiftorTabMoves focus to the previous control.