Skip to content

Form Wrappers

react-hook-form integration for all Vacano UI form components.

Also known as: form bindings, controlled form fields, rhf wrappers

What are Form Wrappers?

Form Wrappers (Form* components) connect Vacano UI components with react-hook-form. Each wrapper:

  • Binds the component's value and onChange to a form field via Controller
  • Automatically shows validation errors (red variant + error message)
  • Keeps the same API as the base component, replacing value/onChange with name/control

Installation

Form wrappers require react-hook-form as a peer dependency. For schema validation, add a resolver (e.g. yup, zod):

bash
pnpm add react-hook-form @hookform/resolvers yup

Import

tsx
import { FormInput, FormSelect, FormCheckbox } from '@vacano/ui/form'

All 17 form wrappers are exported from the @vacano/ui/form entry point.

Quick Example

tsx
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as yup from 'yup'
import { FormInput, FormSelect } from '@vacano/ui/form'
import { Button } from '@vacano/ui'

const schema = yup.object({
  name: yup.string().required('Name is required'),
  role: yup.string().required('Role is required'),
})

type FormValues = yup.InferType<typeof schema>

const MyForm = () => {
  const { control, handleSubmit } = useForm<FormValues>({
    resolver: yupResolver(schema),
    defaultValues: { name: '', role: '' },
  })

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <FormInput control={control} name="name" label="Name" />
      <FormSelect
        control={control}
        name="role"
        label="Role"
        options={[
          { value: 'admin', label: 'Admin' },
          { value: 'user', label: 'User' },
        ]}
      />
      <Button type="submit">Submit</Button>
    </form>
  )
}

How It Works

Every Form Wrapper is a thin wrapper around Controller from react-hook-form:

FormInput  =  Controller  +  Input
FormSelect =  Controller  +  Select
FormCheckbox = Controller + Checkbox
...

The wrapper reads formState.errors[name] and automatically:

  1. Sets variant="error" when there's a validation error
  2. Passes the error message to the message prop (for components that support it)

Props Pattern

Every Form Wrapper accepts the same props as its base component, plus name and control, minus the controlled value props:

tsx
// Base component
<Input value={value} onChange={onChange} label="Email" />

// Form wrapper -- value/onChange replaced by name/control
<FormInput control={control} name="email" label="Email" />
PropTypeDescription
nameFieldPath<T>Field name in the form schema (type-safe)
controlControl<T>Form control from useForm()
...restAll props from the base component (except value/onChange or checked/onChange)

All Form Wrappers

Text Fields

These wrappers automatically display error messages via the message prop.

WrapperBase ComponentOmitted PropsType Name
FormInputInput-- (extends InputProps; value/onChange overridden at runtime)FormInputProps<T>
FormTextareaTextarea-- (extends TextareaProps; value/onChange overridden at runtime)FormTextareaProps<T>
FormSelectSelectvalue, onChangeFormSelectProps<T>
FormAutocompleteAutocompletevalue, onChangeFormAutocompleteProps<T>
FormDatePickerDatePickervalue, onChangeFormDatePickerProps<T>
FormMultiSelectMultiSelectvalue, onChangeFormMultiSelectProps<T>
FormOtpCodeOtpCodevalue, onChangeFormOtpCodeProps<T>
FormTagsTagsvalue, onChangeFormTagsProps<T>

Boolean Controls

These wrappers set variant="error" on validation errors (no message text).

WrapperBase ComponentOmitted PropsType Name
FormCheckboxCheckboxchecked, onChangeFormCheckboxProps<T>
FormCheckboxCardCheckboxCardchecked, onChangeFormCheckboxCardProps<T>
FormToggleTogglechecked, onChangeFormToggleProps<T>
FormToggleCardToggleCardchecked, onChangeFormToggleCardProps<T>

Group Controls

These wrappers set variant="error" on validation errors and manage array or nullable values.

WrapperBase ComponentOmitted PropsType Name
FormCheckboxGroupCheckboxGroupvalue, onChangeFormCheckboxGroupProps<T>
FormToggleGroupToggleGroupvalue, onChangeFormToggleGroupProps<T>
FormRadioGroupRadioGroupvalue, onChangeFormRadioGroupProps<T>

Radio Controls

These wrappers compare field.value === value to determine checked state. You must pass a value prop to each radio so it knows which option it represents.

WrapperBase ComponentOmitted PropsType Name
FormRadioRadiochecked, onChangeFormRadioProps<T>
FormRadioCardRadioCardchecked, onChangeFormRadioCardProps<T>

Type Definitions

All form wrapper types follow the same pattern:

tsx
import type { Control, FieldPath, FieldValues } from 'react-hook-form'

// Value-based wrappers (e.g., FormSelect)
type FormSelectProps<T extends FieldValues> = Omit<SelectProps, 'value' | 'onChange'> & {
  name: FieldPath<T>
  control: Control<T>
}

// Checked-based wrappers (e.g., FormCheckbox)
type FormCheckboxProps<T extends FieldValues> = Omit<CheckboxProps, 'checked' | 'onChange'> & {
  name: FieldPath<T>
  control: Control<T>
}

// Extended wrappers (e.g., FormInput -- includes all base props)
type FormInputProps<T extends FieldValues> = InputProps & {
  name: FieldPath<T>
  control: Control<T>
}

T extends FieldValues and defaults to FieldValues. It is inferred from the control prop.

Usage Examples

Login Form

tsx
const schema = yup.object({
  email: yup.string().email().required('Email is required'),
  password: yup.string().min(6, 'Min 6 characters').required('Password is required'),
})

type LoginValues = yup.InferType<typeof schema>

const LoginForm = () => {
  const { control, handleSubmit } = useForm<LoginValues>({
    resolver: yupResolver(schema),
    defaultValues: { email: '', password: '' },
  })

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormInput control={control} name="email" label="Email" />
      <FormInput control={control} name="password" label="Password" type="password" />
      <Button type="submit">Sign In</Button>
    </form>
  )
}

Settings Form with Toggles

tsx
const schema = yup.object({
  notifications: yup.boolean(),
  newsletter: yup.boolean(),
  theme: yup.string().required('Select a theme'),
})

type SettingsValues = yup.InferType<typeof schema>

const SettingsForm = () => {
  const { control, handleSubmit } = useForm<SettingsValues>({
    resolver: yupResolver(schema),
    defaultValues: { notifications: true, newsletter: false, theme: '' },
  })

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormToggleCard
        control={control}
        name="notifications"
        label="Notifications"
        description="Receive email notifications"
      />
      <FormToggleCard
        control={control}
        name="newsletter"
        label="Newsletter"
        description="Weekly product updates"
      />
      <FormSelect
        control={control}
        name="theme"
        label="Theme"
        options={[
          { value: 'light', label: 'Light' },
          { value: 'dark', label: 'Dark' },
        ]}
      />
      <Button type="submit">Save</Button>
    </form>
  )
}

Radio Selection

tsx
const schema = yup.object({
  plan: yup.string().required('Select a plan'),
})

type PlanValues = yup.InferType<typeof schema>

const PlanForm = () => {
  const { control, handleSubmit } = useForm<PlanValues>({
    resolver: yupResolver(schema),
    defaultValues: { plan: '' },
  })

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormRadioGroup
        control={control}
        name="plan"
        label="Select Plan"
        options={[
          { value: 'free', label: 'Free' },
          { value: 'pro', label: 'Pro' },
          { value: 'enterprise', label: 'Enterprise' },
        ]}
      />
      <Button type="submit">Continue</Button>
    </form>
  )
}

Radio Cards with FormRadioCard

tsx
const schema = yup.object({
  plan: yup.string().required('Select a plan'),
})

type PlanValues = yup.InferType<typeof schema>

const PlanCardForm = () => {
  const { control, handleSubmit } = useForm<PlanValues>({
    resolver: yupResolver(schema),
    defaultValues: { plan: '' },
  })

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormRadioCard
        control={control}
        name="plan"
        value="free"
        label="Free"
        description="Basic features"
      />
      <FormRadioCard
        control={control}
        name="plan"
        value="pro"
        label="Pro"
        description="All features"
      />
      <Button type="submit">Continue</Button>
    </form>
  )
}

Multi-Value Fields

tsx
const schema = yup.object({
  skills: yup.array().of(yup.string()).min(1, 'Select at least one skill'),
  features: yup.array().of(yup.string()).min(1, 'Enable at least one feature'),
})

type ProfileValues = yup.InferType<typeof schema>

const ProfileForm = () => {
  const { control, handleSubmit } = useForm<ProfileValues>({
    resolver: yupResolver(schema),
    defaultValues: { skills: [], features: [] },
  })

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormCheckboxGroup
        control={control}
        name="skills"
        label="Skills"
        options={[
          { value: 'react', label: 'React' },
          { value: 'vue', label: 'Vue' },
          { value: 'angular', label: 'Angular' },
        ]}
      />
      <FormToggleGroup
        control={control}
        name="features"
        label="Features"
        options={[
          { value: 'dark-mode', label: 'Dark Mode' },
          { value: 'notifications', label: 'Notifications' },
          { value: 'analytics', label: 'Analytics' },
        ]}
      />
      <Button type="submit">Save</Button>
    </form>
  )
}

Using with FieldRow

Form wrappers work seamlessly with FieldRow for horizontal layouts:

tsx
<FieldRow>
  <FormInput control={control} name="firstName" label="First Name" />
  <FormInput control={control} name="lastName" label="Last Name" />
</FieldRow>

Validation Libraries

Form wrappers work with any resolver supported by react-hook-form:

LibraryResolver
Yup@hookform/resolvers/yup
Zod@hookform/resolvers/zod
Joi@hookform/resolvers/joi
Superstruct@hookform/resolvers/superstruct

Released under the MIT License.