Ties a label, instructions and an error message to one form control.

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

<Field required invalid={invalid}>
  <FieldLabel>Email</FieldLabel>
  <FieldControl>
    <Input type="email" />
  </FieldControl>
  <FieldDescription>We send receipts here.</FieldDescription>
  <FieldError>Enter an email like name@example.com.</FieldError>
</Field>

Examples

Example source
var P = window.PUI, h = React.createElement;
  function Demo() {
    var s = React.useState('maya@'), v = s[0];
    var invalid = !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v);
    return h('div', { className: 'pui-stage', style: { padding: 0, maxWidth: 360 } },
      h(P.Field, { required: true, invalid: invalid },
        h(P.FieldLabel, null, 'Email'),
        h(P.FieldControl, null, h(P.Input, { type: 'email', value: v, onChange: function (e) { s[1](e.target.value); } })),
        h(P.FieldDescription, null, 'We send receipts here.'),
        h(P.FieldError, null, 'Enter an email like name@example.com.')),
      h(P.Field, null,
        h(P.FieldLabel, null, 'Company'),
        h(P.FieldControl, null, h(P.Input, { placeholder: 'E.g. Acme Inc.' }))));
  }
  ReactDOM.createRoot(document.getElementById('preview')).render(h(Demo));

Usage

What you provide

  • Field with required and invalid (set invalid after validation finds a problem, not before the person had a chance to type). controlId is optional; one is generated.
  • FieldLabel: the visible label. Optional fields get "(optional)" automatically.
  • FieldControl: wraps exactly one control (Input, Textarea, PasswordInput, SelectTrigger) and gives it id, required, aria-invalid and aria-describedby.
  • FieldDescription: instructions, formats and limits, shown before the person types.
  • FieldError: what is wrong and how to fix it. Rendered only while invalid is true.

Usage

  • Error messages are concrete: "Enter an email like name@example.com", not "Invalid value". Tell empty, wrong format and business rules apart; a server failure is not a field error.
  • Keep the values after an invalid submit. Move focus to an error summary or to the first invalid field, and never put passwords in messages.
  • Don't announce the same error through focus and a live region at once.

Accessibility

  • The label stays visible and is associated with the control; instructions and the error are linked through aria-describedby, and aria-invalid is set only while an error is detected.
  • Required fields get required; errors are written in text, never only as a red border or an icon.
  • After a rejected submit, focus goes to an error summary (with links to each field) or to the first invalid field. When fixed, the message and aria-invalid are removed without clearing other errors.
  • Validating while typing keeps focus and input intact and doesn't announce on every key.

Adapted from Magoo 1.6.0 by Marcelo Sales: Form validation (validacao-formularios), Input "text" (input-text). Magoo is educational guidance; test the behavior in your implementation.

API

Install with:

npx @aline-aroucha/pui add field
<Field>
  <FieldLabel />
  <FieldControl>
    <Input />
  </FieldControl>
  <FieldDescription />
  <FieldError />
</Field>

Field

NameTypeDefault
requiredbooleanfalse
Adds required to the control; optional fields show "(optional)" in the label.
invalidbooleanfalse
Set after validation finds a problem; shows FieldError and sets aria-invalid.
controlIdstringgenerated
id given to the control. An id set on the control itself wins.

FieldControl

NameType
childrenRequiredReactElement
Exactly one control: Input, Textarea, PasswordInput or SelectTrigger.

FieldDescription

NameType
childrenReactNode
Instructions, formats and limits, linked through aria-describedby.

FieldError

NameType
childrenReactNode
What is wrong and how to fix it; rendered only while invalid.