Form Libraries Integration
yarcl form controls (Input, Textarea, Checkbox, Switch, Select, Combobox, RadioGroup, NumberInput, Slider, DatePicker) are built on standard HTML element semantics. They work out of the box with popular React form libraries such as React Hook Form and TanStack Form, support schema validation through Zod and the Standard Schema specification, and participate in both programmatic and native HTML form reset lifecycles.
How Field Handles Validation Errors
Section titled “How Field Handles Validation Errors”The <Field> component acts as an accessibility and layout wrapper for form controls. When an error prop is passed to <Field>, it automatically:
- Renders an error message below the control styled with
defaults.errorColor. - Generates an
aria-describedbyreference pointing to the error message element ID. - Passes
invalid: truevia context to child controls, settingaria-invalid="true"on the underlying<input>or<button>.
<Field label="Email" error={errors.email?.message} required> <Input type="email" placeholder="you@example.com" /></Field>Schema Validation with Zod & Standard Schema
Section titled “Schema Validation with Zod & Standard Schema”Both React Hook Form and TanStack Form support schema-based validation using Zod.
Shared Validation Schema
Section titled “Shared Validation Schema”Define a type-safe schema with Zod 4. Use the error option to customize validation messages:
import { z } from 'zod';
export const settingsSchema = z.object({ organization: z.string().min(2, 'Organization must be at least 2 characters'), plan: z.string().min(1, 'Please select a subscription plan'), region: z.string().min(1, 'Please select a deployment region'), billingCycle: z.enum(['monthly', 'annual'], { error: 'Please choose a billing cycle', }), seats: z .number({ error: 'Seats must be a number' }) .min(1, 'Must have at least 1 seat') .max(50, 'Maximum 50 seats per organization'), notifications: z.boolean(), agreeToTerms: z.literal(true, { error: 'You must accept the terms and conditions', }),});
export type SettingsFormValues = z.infer<typeof settingsSchema>;React Hook Form Integration
Section titled “React Hook Form Integration”Controlled Controls with Controller and zodResolver
Section titled “Controlled Controls with Controller and zodResolver”Use React Hook Form’s <Controller> for custom components like Select, Combobox, RadioGroup, Switch, and NumberInput. Pass zodResolver(settingsSchema) to useForm for automatic validation:
import { useForm, Controller } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';import { Button, Combobox, Field, Input, Inline, NumberInput, Radio, RadioGroup, Select, Stack, Switch,} from '@yarcl/react';import { settingsSchema, type SettingsFormValues } from './schemas/settingsSchema';
const planOptions = [ { value: 'starter', label: 'Starter Plan' }, { value: 'pro', label: 'Pro Plan' }, { value: 'enterprise', label: 'Enterprise Plan' },];
const regionOptions = [ { value: 'us-east', label: 'US East (N. Virginia)' }, { value: 'eu-central', label: 'Europe (Frankfurt)' }, { value: 'ap-southeast', label: 'Asia Pacific (Tokyo)' },];
export function RHFSettingsForm() { const { control, handleSubmit, reset, formState: { errors, isSubmitting }, } = useForm<SettingsFormValues>({ resolver: zodResolver(settingsSchema), defaultValues: { organization: '', plan: '', region: '', billingCycle: 'monthly', seats: 5, notifications: true, agreeToTerms: false as unknown as true, }, });
const onSubmit = (data: SettingsFormValues) => { console.log('Valid submission data:', data); };
return ( <Stack as="form" onSubmit={handleSubmit(onSubmit)}> {/* Input */} <Controller name="organization" control={control} render={({ field, fieldState }) => ( <Field label="Organization" error={fieldState.error?.message} required> <Input {...field} placeholder="Acme Inc." /> </Field> )} />
{/* Select */} <Controller name="plan" control={control} render={({ field, fieldState }) => ( <Field label="Plan" error={fieldState.error?.message} required> <Select placeholder="Choose a plan" options={planOptions} value={field.value || null} onValueChange={(val) => field.onChange(val ?? '')} /> </Field> )} />
{/* Combobox */} <Controller name="region" control={control} render={({ field, fieldState }) => ( <Field label="Region" error={fieldState.error?.message} required> <Combobox placeholder="Search regions..." options={regionOptions} value={field.value || null} onValueChange={(val) => field.onChange(val ?? '')} /> </Field> )} />
{/* RadioGroup */} <Controller name="billingCycle" control={control} render={({ field, fieldState }) => ( <RadioGroup label="Billing Cycle" value={field.value ?? null} onValueChange={field.onChange} error={fieldState.error?.message} > <Radio value="monthly">Monthly Billing</Radio> <Radio value="annual">Annual Billing (Save 20%)</Radio> </RadioGroup> )} />
{/* NumberInput */} <Controller name="seats" control={control} render={({ field, fieldState }) => ( <Field label="Team Seats" error={fieldState.error?.message}> <NumberInput min={1} max={50} value={field.value} onValueChange={(val) => field.onChange(val ?? 1)} /> </Field> )} />
{/* Switch */} <Controller name="notifications" control={control} render={({ field }) => ( <Field label="Preferences"> <Switch checked={field.value} onChange={(e) => field.onChange(e.target.checked)} > Email deployment alerts </Switch> </Field> )} />
{/* Terms Checkbox */} <Controller name="agreeToTerms" control={control} render={({ field, fieldState }) => ( <Field label="Terms and conditions" error={fieldState.error?.message}> <Switch checked={Boolean(field.value)} onChange={(e) => field.onChange(e.target.checked)} > I agree to the Terms of Service </Switch> </Field> )} />
<Inline> <Button type="submit" disabled={isSubmitting}> Save Settings </Button> <Button type="button" onClick={() => reset()}> Reset </Button> </Inline> </Stack> );}Uncontrolled Usage
Section titled “Uncontrolled Usage”For native inputs or uncontrolled components, pass name, defaultValue (or defaultChecked), or use React Hook Form’s register():
import { useForm } from 'react-hook-form';import { Button, Combobox, Field, Radio, RadioGroup, Select, Stack, Switch } from '@yarcl/react';
export function UncontrolledForm() { const { register, handleSubmit, formState: { errors } } = useForm();
return ( <Stack as="form" onSubmit={handleSubmit((data) => console.log(data))}> <Field label="Email Notifications" error={errors.notifications?.message as string}> <Switch {...register('notifications')}> Receive product updates </Switch> </Field>
<Field label="Plan"> <Select name="plan" defaultValue="free" options={[{ value: 'free', label: 'Free' }]} /> </Field>
<RadioGroup label="Frequency" name="frequency" defaultValue="monthly"> <Radio value="monthly">Monthly</Radio> <Radio value="annual">Annual</Radio> </RadioGroup>
<Button type="submit">Submit</Button> </Stack> );}TanStack Form Integration
Section titled “TanStack Form Integration”TanStack Form uses form.Field to bind individual components. It supports Standard Schema validation out of the box with Zod schemas passed to validators.onChange or validators.onSubmit.
import { useForm } from '@tanstack/react-form';import { Button, Combobox, Field, Input, Inline, Radio, RadioGroup, Select, Stack, Switch,} from '@yarcl/react';import { z } from 'zod';
const emailSchema = z.string().email('Please enter a valid email address');const planSchema = z.string().min(1, 'Please select a plan');
const planOptions = [ { value: 'free', label: 'Free Tier' }, { value: 'pro', label: 'Pro Tier' },];
export function TanStackSettingsForm() { const form = useForm({ defaultValues: { email: '', plan: '', frequency: 'monthly', agree: false, }, onSubmit: async ({ value }) => { console.log('Submitted values:', value); }, });
return ( <form onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }} > <Stack> {/* Email Field with Standard Schema / Zod Validator */} <form.Field name="email" validators={{ onChange: emailSchema, onSubmit: emailSchema, }} children={(field) => { const errorMsg = field.state.meta.errors .map((err) => (typeof err === 'object' && err !== null && 'message' in err ? String((err as { message: unknown }).message) : String(err))) .filter(Boolean) .join(', '); return ( <Field label="Email" error={errorMsg || undefined} required> <Input name={field.name} value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} onBlur={field.handleBlur} placeholder="name@company.com" /> </Field> ); }} />
{/* Plan Select Field */} <form.Field name="plan" validators={{ onChange: planSchema, onSubmit: planSchema, }} children={(field) => { const errorMsg = field.state.meta.errors .map((err) => (typeof err === 'object' && err !== null && 'message' in err ? String((err as { message: unknown }).message) : String(err))) .filter(Boolean) .join(', '); return ( <Field label="Plan" error={errorMsg || undefined} required> <Select name={field.name} placeholder="Select a plan" options={planOptions} value={field.state.value || null} onValueChange={(val) => field.handleChange(val ?? '')} /> </Field> ); }} />
{/* RadioGroup Field */} <form.Field name="frequency" children={(field) => ( <RadioGroup label="Billing Frequency" name={field.name} value={field.state.value} onValueChange={(val) => field.handleChange(val)} > <Radio value="monthly">Monthly</Radio> <Radio value="annual">Annual</Radio> </RadioGroup> )} />
{/* Switch Field */} <form.Field name="agree" children={(field) => ( <Field label="Terms"> <Switch name={field.name} checked={field.state.value} onChange={(e) => field.handleChange(e.target.checked)} onBlur={field.handleBlur} > Accept terms and conditions </Switch> </Field> )} />
<Inline> <Button type="submit">Submit</Button> <Button type="button" onClick={() => form.reset()}> Reset Form </Button> </Inline> </Stack> </form> );}Form Reset Integration Lifecycle
Section titled “Form Reset Integration Lifecycle”yarcl controls seamlessly support both programmatic form resets and native HTML form resets.
Programmatic Reset (form.reset())
Section titled “Programmatic Reset (form.reset())”When using controlled bindings with React Hook Form or TanStack Form:
- Calling
form.reset()restores the form state to the initial default values. - Form libraries automatically clear validation errors and reset the
isDirty/isSubmittedflags. - Passing custom values, such as
form.reset({ plan: 'enterprise', ... }), immediately updates all controlled@yarcl/reactcomponents to reflect the new state.
// Reset to initial values and clear errorsform.reset();
// Populate form with newly fetched dataform.reset({ organization: 'Acme Corp', plan: 'pro', seats: 12,});Native HTML Form Reset (<button type="reset">)
Section titled “Native HTML Form Reset (<button type="reset">)”Uncontrolled @yarcl/react components (Select, Combobox, RadioGroup, NumberInput, Slider, Switch, Checkbox, DatePicker) automatically listen for the native HTML form 'reset' event.
When a native form reset occurs:
- The component reverts its internal state back to its initial
defaultValue(ordefaultChecked). - Hidden inputs rendered by
Select,Combobox, orRadioGroupupdate accordingly. - No manual event handlers are required.
import { Button, Combobox, Field, Inline, NumberInput, Radio, RadioGroup, Select, Slider, Stack, Switch } from '@yarcl/react';
export function NativeResetExample() { return ( <Stack as="form"> <Field label="Plan"> <Select name="plan" defaultValue="pro" options={[ { value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }, ]} /> </Field>
<Field label="Seats"> <NumberInput name="seats" defaultValue={5} min={1} max={20} /> </Field>
<Field label="Volume"> <Slider name="volume" defaultValue={50} min={0} max={100} /> </Field>
<Field label="Notifications"> <Switch name="notifications" defaultChecked> Enable notifications </Switch> </Field>
<Inline> <Button type="submit">Submit</Button> <Button type="reset">Reset to Defaults</Button> </Inline> </Stack> );}Component Binding Reference
Section titled “Component Binding Reference”| Component | Controlled Props | Uncontrolled Props | Form Submit Behavior |
|---|---|---|---|
<Input> |
value, onChange |
defaultValue, name |
Native <input> submits value |
<Textarea> |
value, onChange |
defaultValue, name |
Native <textarea> submits text |
<Checkbox> |
checked, onChange |
defaultChecked, name |
Native <input type="checkbox"> submits value |
<Switch> |
checked, onChange |
defaultChecked, name |
Native <input type="checkbox" role="switch"> submits value |
<Select> |
value, onValueChange |
defaultValue, name |
Renders <input type="hidden" name={name} value={value} /> |
<Combobox> |
value, onValueChange |
defaultValue, name |
Renders <input type="hidden" name={name} value={value} /> (single or multiple) |
<RadioGroup> |
value, onValueChange |
defaultValue, name |
Radios share name; native radio input submits value |
<NumberInput> |
value, onValueChange |
defaultValue, name |
Native <input type="text" inputMode="decimal"> submits value |
<Slider> |
value, onValueChange |
defaultValue, name |
Hidden inputs submit single or range values |
<DatePicker> |
value, onValueChange |
defaultValue, name |
Renders <input type="hidden" name={name} value={isoDate} /> |