Skip to content
UI

Field

Lays out a form control with its label, description and error message, and groups fields into sections.

@zenoxai/ui/components/field Markdown

Shown in reports and alert emails.

<Field className="max-w-sm">
  <FieldLabel htmlFor="tenant-name">Tenant name</FieldLabel>
  <Input id="tenant-name" placeholder="Acme Corporation" />
  <FieldDescription>Shown in reports and alert emails.</FieldDescription>
</Field>

Usage

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@zenoxai/ui/components/field"
import { Input } from "@zenoxai/ui/components/input"

<Field>
  <FieldLabel htmlFor="tenant-name">Tenant name</FieldLabel>
  <Input id="tenant-name" />
  <FieldDescription>Shown in reports and alert emails.</FieldDescription>
</Field>

Wrap every form control in a Field: Input, Select, Checkbox, Radio Group, Slider and the rest. The field sets the spacing between label, control and help text. With data-disabled it dims the label; with data-invalid it colors the label and help text red.

Field is layout only. It does not create ids or validate: give the control an id, point FieldLabel at it with htmlFor, and decide in your form code when a value is invalid.

Part What it is
Field One control with its label and help text.
FieldLabel The <label> of the control.
FieldDescription Help text under the control, or under a legend.
FieldError The error message. Renders nothing when it has no content.
FieldContent Stacks a label and a description next to a checkbox or radio.
FieldTitle A label-styled heading for a field that is not a single control.
FieldSet A <fieldset> that groups related fields.
FieldLegend The title of a FieldSet. variant="label" makes it label-sized.
FieldGroup Stacks fields with the standard gap. Needs a definite width.
FieldSeparator A divider between groups of fields, with optional text.

Examples

Invalid

Set data-invalid on the Field and aria-invalid on the control. The label turns red, the control gets the error ring, and FieldError shows the message. Write the message as the fix (“Enter a domain without the protocol”), not as the fault.

<Field data-invalid className="max-w-sm">
  <FieldLabel htmlFor="monitored-domain">Monitored domain</FieldLabel>
  <Input
    id="monitored-domain"
    defaultValue="https://acme"
    aria-invalid
    aria-describedby="monitored-domain-error"
  />
  <FieldError id="monitored-domain-error">
    Enter a domain without the protocol, such as acme.com.
  </FieldError>
</Field>

FieldError also takes errors, an array of { message } objects in the shape form libraries return. It removes duplicates and renders a list when more than one message remains.

Horizontal

orientation="horizontal" puts the control and the label on one line, for checkboxes, radios and switches. Put the label and description in a FieldContent so they stack. orientation="responsive" is vertical by default and turns horizontal when the surrounding FieldGroup is at least 28rem wide (a container query, not the viewport).

Scan every subdomain found in DNS and certificate logs.

<Field orientation="horizontal" className="max-w-sm">
  <Checkbox id="include-subdomains" defaultChecked />
  <FieldContent>
    <FieldLabel htmlFor="include-subdomains">Include subdomains</FieldLabel>
    <FieldDescription>
      Scan every subdomain found in DNS and certificate logs.
    </FieldDescription>
  </FieldContent>
</Field>

A full form

FieldSet and FieldLegend name a group of fields; FieldGroup spaces them; FieldSeparator divides them. A horizontal Field also lays out the form buttons.

FieldGroup is a size container (the container that orientation="responsive" queries), so it takes no width from its content. Give it a definite width: the width of the form column, or a class such as w-full, w-56 or max-w-sm. With w-fit it collapses to zero and every label wraps one word per line. For the same reason, a Field placed in a row next to other fields (a flex row of radios, for example) needs className="w-auto": by default it is w-full and shares the row instead of fitting its label.

Notification rule

Sends a message when an alert matches the rule.

One message at 09:00 instead of one per alert.

function FieldForm() {
  const channels = [
    { label: "Email", value: "email" },
    { label: "Slack", value: "slack" },
    { label: "Webhook", value: "webhook" },
  ]

  return (
    <form
      className="w-full max-w-md"
      onSubmit={(event) => event.preventDefault()}
    >
      <FieldSet>
        <FieldLegend>Notification rule</FieldLegend>
        <FieldDescription>
          Sends a message when an alert matches the rule.
        </FieldDescription>
        <FieldGroup>
          <Field>
            <FieldLabel htmlFor="rule-name">Name</FieldLabel>
            <Input id="rule-name" placeholder="Critical alerts to on-call" />
          </Field>
          <Field>
            <FieldLabel htmlFor="rule-channel">Channel</FieldLabel>
            <Select items={channels} defaultValue="email">
              <SelectTrigger id="rule-channel" className="w-full">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {channels.map((channel) => (
                  <SelectItem key={channel.value} value={channel.value}>
                    {channel.label}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </Field>
          <FieldSeparator />
          <Field orientation="horizontal">
            <Checkbox id="rule-digest" />
            <FieldContent>
              <FieldLabel htmlFor="rule-digest">Send a daily digest</FieldLabel>
              <FieldDescription>
                One message at 09:00 instead of one per alert.
              </FieldDescription>
            </FieldContent>
          </Field>
          <Field orientation="horizontal">
            <Button type="submit">Save rule</Button>
            <Button type="button" variant="outline">
              Cancel
            </Button>
          </Field>
        </FieldGroup>
      </FieldSet>
    </form>
  )
}

For selectable cards, wrap a horizontal Field in a FieldLabel. See the choice cards in Radio Group.

Accessibility

  • FieldLabel is a native <label>. It names the control only through htmlFor pointing at the control’s id, or by wrapping the control.
  • FieldDescription and FieldError are not linked to the control automatically. Give them an id and list it in the control’s aria-describedby so screen readers read them with the control.
  • FieldError has role="alert", so it is announced as soon as it appears. Render it only after the user submits or leaves the field, not on every keystroke.
  • data-invalid on the Field only changes styles. aria-invalid on the control is what assistive technology reads.
  • Every Field renders role="group". Use FieldSet and FieldLegend when a group of fields needs a name.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

Field

Field lays out one form control with its FieldLabel, FieldDescription and FieldError, stacked or on one row. FieldSet with a FieldLegend groups related fields, and FieldGroup stacks fields with the standard spacing.

PropTypeDefault
orientation"horizontal" | "vertical" | "responsive" | null

`vertical` (default) puts the label above the control; `horizontal` puts them on one row, as for a checkbox or a switch; `responsive` is vertical until its FieldGroup is 28rem (448px) wide, then horizontal.

vertical

FieldLabel

FieldLabel is the label of a field. Wrapped around a whole Field, it makes a selectable card: a bordered option that highlights when the checkbox or radio inside it is checked. Only an unchecked card takes the hover fill, so hover never hides the checked tint.

No props of its own. Accepts the props of the element it renders.

FieldDescription

No props of its own. Accepts the props of the element it renders.

FieldError

FieldError shows the validation messages of a field with role="alert", and renders nothing when there are none. Pass the message as children, or the `errors` of a form library.

PropTypeDefault
errors({ message?: string; })[] | undefined

Error objects, such as react-hook-form's. Duplicate messages are dropped; one message renders as text, several as a list. Children win over it.

-

FieldGroup

FieldGroup stacks fields with the standard gap. It is also the container whose width `orientation="responsive"` responds to, so it takes no width from its content: give it a definite width (`w-full`, `w-56`, the form column), never `w-fit`, or its labels wrap one word per line.

No props of its own. Accepts the props of the element it renders.

FieldLegend

FieldLegend is the caption of a FieldSet.

PropTypeDefault
variant"label" | "legend"

`legend` (default) is a section heading at text-base; `label` has the size of a FieldLabel, for a set that reads as one question, such as a radio group.

legend

FieldSeparator

FieldSeparator is a horizontal rule between fields or groups, with optional centered text such as "Or".

No props of its own. Accepts the props of the element it renders.

FieldSet

FieldSet groups related fields under a FieldLegend, as a <fieldset>.

No props of its own. Accepts the props of the element it renders.

FieldContent

FieldContent stacks a label and a description beside the control of a horizontal Field, such as the text next to a checkbox.

No props of its own. Accepts the props of the element it renders.

FieldTitle

FieldTitle is text styled as a label that is not a <label>, such as the title of a FieldLabel card whose control is labeled otherwise.

No props of its own. Accepts the props of the element it renders.