gray
radius
Forms

Field

Groups a label, a control, and whatever explains it. This is the component that most repays being compound, because everything it does is invisible when it is done wrong.

Everything wired

Field.Root holds the ids in context, so the label points at the control and the control points back at its description and error.

Lowercase, no spaces. This becomes the npm install target.

<Field.Root required>
<Field.Label>Package name</Field.Label>
<Field.Control>
{props => (
<Input
{...props}
placeholder="@scope/name"
defaultValue="@minuk-hwang-design-system/components-react"
/>
)}
</Field.Control>
<Field.Description>
Lowercase, no spaces. This becomes the npm install target.
</Field.Description>
</Field.Root>
Click the label above. Focus lands in the input, because htmlFor was filled in from context rather than left to whoever wrote the page.

Invalid

Field.Error renders nothing while the root is valid, and carries role="alert" when it appears.

<Field.Root invalid>
<Field.Label>Version</Field.Label>
<Field.Control>{props => <Input {...props} defaultValue="0.0.1" />}</Field.Control>
<Field.Error>A caret range does nothing below 0.1.0. Bump before publishing.</Field.Error>
</Field.Root>

Description and error

The description stays when the field goes invalid, because an error adds to the guidance rather than replacing it.

Markdown. Shown on the npm page.

<Field.Root invalid>
<Field.Label>Release notes</Field.Label>
<Field.Control>{props => <Textarea {...props} />}</Field.Control>
<Field.Description>Markdown. Shown on the npm page.</Field.Description>
<Field.Error>Cannot be empty for a minor release.</Field.Error>
</Field.Root>

What Field.Control passes

It hands the props over instead of wrapping the control, so the control can be an input, a textarea, a Select, or something the system has never seen.

<Field.Root>
<Field.Label>Anything at all</Field.Label>
<Field.Control>
{({ id, 'aria-describedby': describedBy, 'aria-invalid': invalid, disabled, required }) => (
<Input
id={id}
aria-describedby={describedBy}
aria-invalid={invalid}
disabled={disabled}
required={required}
placeholder="Your control, our wiring"
/>
)}
</Field.Control>
</Field.Root>

Parts

Import it as a namespace, and the parts work on either side of the server boundary.

import * as Field from '@minuk-hwang-design-system/components-react/field';
PartWhat it is
Field.RootGenerates the ids and holds the state. Everything else reads from it, and a part rendered outside one throws rather than pointing at nothing.
Field.LabelPoints at the control. Adds the required marker when the root is required.
Field.ControlRender prop. Receives id, aria-describedby, aria-invalid, disabled and required.
Field.DescriptionHelper text. Registers itself so aria-describedby only lists it when it rendered. A Text, so it takes every prop of one.
Field.ErrorValidation message. Renders only while the root is invalid, with role="alert". A Text, so it takes every prop of one.

Props

PropTypeDefaultNotes
invalidbooleanfalseSets aria-invalid on the control and lets Field.Error render. Drives the appearance too, so the two cannot disagree.
requiredbooleanfalseSets required on the control and adds the marker beside the label. The marker is aria-hidden, since the attribute already says it.
disabledbooleanfalsePassed to the control and dims the label.
idstringgeneratedOnly needed when something outside the field has to point at the control.