Privacy-friendly analytics only with consent. No ads, no cross-site tracking. Read the privacy policy.
Slipstream controls work with react-hook-form 7.x (tested with 7.90). The short version: register() for native inputs, Controller when you want the transformed value. FormField owns the label, hint, error and aria-invalid wiring either way.
| Control | Pattern | Why |
|---|---|---|
Input, Textarea | register() | Native input, ref forwarded. |
Select, Checkbox | register() or Controller | Both forward the native change event. Use Controller for the value callback. |
DatePicker, TimePicker | Controller | The ISO string contract is clearer than reading the DOM value. |
FormField clones its child and sets id, aria-describedby and aria-invalid. Pass the field id and the resolved error message; register() supplies the name, ref and native handler.
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({defaultValues: { email: '', notes: '' },});<FormField id="email" label="Email" required error={errors.email?.message}><Input id="email" type="email" autoComplete="email" {...register('email')} /></FormField><FormField id="notes" label="Notes" hint="Optional" error={errors.notes?.message}><Textarea id="notes" rows={4} {...register('notes')} /></FormField>
Select exposes both the native onChange event and onValueChange(value). Use one, not both. With Controller, bind field.value and field.onChange to the value callback.
<Controllercontrol={control}name="game"rules={{ required: 'Pick a game' }}render={({ field, fieldState }) => (<FormField id="game" label="Game" required error={fieldState.error?.message}><Selectid="game"options={games}value={field.value}onValueChange={field.onChange}onBlur={field.onBlur}placeholder="Choose a game"/></FormField>)}/><Controllercontrol={control}name="startsAt"render={({ field, fieldState }) => (<FormField id="startsAt" label="Starts" required error={fieldState.error?.message}><DatePickerid="startsAt"value={field.value}onValueChange={field.onChange}onBlur={field.onBlur}clearable/></FormField>)}/><Controllercontrol={control}name="terms"rules={{ required: 'Accept the terms' }}render={({ field, fieldState }) => (<div><Checkboxid="terms"label="I accept the terms"checked={field.value}onCheckedChange={field.onChange}onBlur={field.onBlur}/>{fieldState.error?.message ? (<p role="alert" className="mt-1 text-xs text-destructive">{fieldState.error.message}</p>) : null}</div>)}/>
If you use the same controls everywhere, a small typed adapter keeps the forms honest without a dependency on react-hook-form inside the library.
import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form';import { FormField, Select } from '@slipstream-ui/core';type SelectFieldProps<T extends FieldValues> = {control: Control<T>;name: FieldPath<T>;label: string;options: { value: string; label: string }[];hint?: string;};export function SelectField<T extends FieldValues>({control,name,label,options,hint,}: SelectFieldProps<T>) {return (<Controllercontrol={control}name={name}render={({ field, fieldState }) => (<FormField id={name} label={label} hint={hint} error={fieldState.error?.message}><Selectid={name}options={options}value={field.value}onValueChange={field.onChange}onBlur={field.onBlur}/></FormField>)}/>);}
Select is built on the native <select>. Its onChange forwards the React change event, so register() works; onValueChange is the value convenience.Checkbox forwards the native change event and also provides onCheckedChange(checked).DatePicker and TimePicker use ISO strings and HH:MM; prefer onValueChange over reading the DOM.defaultValues to useForm so controlled selects and checkboxes start defined. An undefined value can flip a checkbox from uncontrolled to controlled.react, react-dom, and react/jsx-runtime in the consumer's bundler. A second React copy breaks Radix Slot (used by asChild) because React 19 elements are not recognised by React 18. Published packages dedupe through peer dependencies, so the override is only needed while the packages are linked.FormField is a client component; forms using it are client components too. Server actions can still receive the submitted values.