Plain children
The short form: an Input or TextArea inside is tied to the label, the help and the error by itself.
import { Field, Input, TextArea } from '@arbol-ai/ui';
<div className="grid max-w-xs gap-4">
<Field description="The patient sees it in the reminder." label="Reason">
<Input defaultValue="Follow-up visit" />
</Field>
<Field errorMessage="Write at most 500 characters." isInvalid label="Note for the front desk">
<TextArea defaultValue="Bring the authorization." />
</Field>
</div>A native select or text area
nativeFieldBox gives a native control the look of the fields beside it, from the theme tokens; the Field’s id goes on the control.
import { Field, nativeFieldBox } from '@arbol-ai/ui';
<div className="grid max-w-xs gap-4">
<Field description="Only active agendas are listed." id="example-agenda" label="Agenda">
<select className={nativeFieldBox()} defaultValue="ruiz" id="example-agenda">
<option value="ruiz">Dr. Ruiz</option>
<option value="room-2">Room 2</option>
</select>
</Field>
<Field id="example-instructions" label="Instructions">
<textarea className={nativeFieldBox({ control: 'textarea' })} id="example-instructions" />
</Field>
</div>A control of your own
Field gives the control the ids that tie it to its label, help and error. Here an input with suggestions validates what was typed.
import { useId, useState } from 'react';
import { Field, Input } from '@arbol-ai/ui';
const USUAL_DURATIONS = ['15m', '30m', '45m', '1h', '1h30', '2h'];
const DurationExample = () => {
const [text, setText] = useState('30m');
const suggestions = useId();
const isValid = text === '' || /^\d+(?:m|h(?:\d+)?)$/u.test(text);
return (
<div className="max-w-xs">
<Field
description="Like 30m, 1h or 1h30."
errorMessage="Write the length like 30m, 1h or 1h30."
isInvalid={!isValid}
label="Appointment length"
>
{(control) => (
<>
<Input
{...control}
list={suggestions}
onChange={(event) => {
setText(event.target.value);
}}
value={text}
/>
<datalist id={suggestions}>
{USUAL_DURATIONS.map((duration) => (
<option key={duration} value={duration}>
{duration}
</option>
))}
</datalist>
</>
)}
</Field>
</div>
);
};
<DurationExample />An input with a button
FieldGroup draws one box around the input and its button, with one focus ring.
import { EyeIcon, EyeSlashIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { TextField as AriaTextField } from 'react-aria-components';
import { Button, FieldGroup, FieldGroupInput, FieldHelp, Label } from '@arbol-ai/ui';
const PasswordExample = () => {
const [isVisible, setIsVisible] = useState(false);
const label = 'Password';
return (
<AriaTextField className="flex max-w-xs flex-col gap-1.5" defaultValue="clinic-2026">
<Label>{label}</Label>
<FieldGroup>
<FieldGroupInput type={isVisible ? 'text' : 'password'} />
<Button
aria-label={isVisible ? 'Hide password' : 'Show password'}
className="me-0.5"
onPress={() => {
setIsVisible((visible) => !visible);
}}
size="icon-sm"
variant="ghost"
>
{isVisible ? <EyeSlashIcon aria-hidden /> : <EyeIcon aria-hidden />}
</Button>
</FieldGroup>
<FieldHelp description="At least 12 characters." />
</AriaTextField>
);
};
<PasswordExample />Source
The examples above are src/components/field/field.examples.tsx, which the library’s tests render too.