Skip to content

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.

The <Field> component acts as an accessibility and layout wrapper for form controls. When an error prop is passed to <Field>, it automatically:

  1. Renders an error message below the control styled with defaults.errorColor.
  2. Generates an aria-describedby reference pointing to the error message element ID.
  3. Passes invalid: true via context to child controls, setting aria-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.

Define a type-safe schema with Zod 4. Use the error option to customize validation messages:

src/schemas/settingsSchema.ts
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>;

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:

src/RHFSettingsForm.tsx
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>
);
}

For native inputs or uncontrolled components, pass name, defaultValue (or defaultChecked), or use React Hook Form’s register():

src/UncontrolledForm.tsx
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 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.

src/TanStackSettingsForm.tsx
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>
);
}

yarcl controls seamlessly support both programmatic form resets and native HTML form resets.

When using controlled bindings with React Hook Form or TanStack Form:

  1. Calling form.reset() restores the form state to the initial default values.
  2. Form libraries automatically clear validation errors and reset the isDirty / isSubmitted flags.
  3. Passing custom values, such as form.reset({ plan: 'enterprise', ... }), immediately updates all controlled @yarcl/react components to reflect the new state.
// Reset to initial values and clear errors
form.reset();
// Populate form with newly fetched data
form.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 (or defaultChecked).
  • Hidden inputs rendered by Select, Combobox, or RadioGroup update accordingly.
  • No manual event handlers are required.
src/NativeResetExample.tsx
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 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} />