Glrk UI

Field with RHF

Reusable Field field wrappers built on top of shadcn/ui with react-hook-form

Gender
Interest

Installation

npx shadcn@latest add @glrk-ui/field-wrapper-rhf

If you haven't set up the prerequisites yet, check out Prerequest section.

Add following shadcn components: select, popover, calendar, textarea, input, radio-group, form, button, label, checkbox, switch.

Update same components from our site.

Copy and paste the following code into your project.

ui/field-wrapper-rhf.tsx
'use client'

import { Controller, type Control, type FieldValues, type Path } from 'react-hook-form'

import {
  OTPWrapper as OTP,
  InputWrapper as Input,
  RadioWrapper as Radio,
  SwitchWrapper as Switch,
  SelectWrapper as Select,
  SliderWrapper as Slider,
  NumberWrapper as NumberInput,
  TextareaWrapper as Textarea,
  CheckboxWrapper as Checkbox,
  ComboboxWrapper as Combobox,
  DatePickerWrapper as DatePicker,
  InputGroupWrapper as InputGroup,
  AutocompleteWrapper as Autocomplete,
} from './field-wrapper'

type BaseProps<T extends FieldValues> = {
  name: Path<T>
  control: Control<T>
  className?: string
  label?: React.ReactNode
}

type InputProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
export function InputWrapper<T extends FieldValues>({ name, control, ...props }: InputProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Input
          {...props}
          name={name}
          value={field.value ?? ''}
          onChange={field.onChange}
          onBlur={field.onBlur}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type TextareaProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
export function TextareaWrapper<T extends FieldValues>({
  name,
  control,
  ...props
}: TextareaProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Textarea
          {...props}
          name={name}
          value={field.value ?? ''}
          onChange={field.onChange}
          onBlur={field.onBlur}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type RadioProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
export function RadioWrapper<T extends FieldValues>({ name, control, ...props }: RadioProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Radio
          {...props}
          name={name}
          value={field.value}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type CheckboxProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
export function CheckboxWrapper<T extends FieldValues>({
  name,
  control,
  ...props
}: CheckboxProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Checkbox
          {...props}
          name={name}
          value={field.value ?? []}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type SwitchProps<T extends FieldValues> = BaseProps<T>
export function SwitchWrapper<T extends FieldValues>({ name, control, ...props }: SwitchProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Switch
          {...props}
          name={name}
          checked={field.value ?? false}
          onCheckedChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type SelectProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof Select>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
export function SelectWrapper<T extends FieldValues>({ name, control, ...props }: SelectProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Select
          {...props}
          name={name}
          value={field.value}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type DatePickerProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof DatePicker>, 'name' | 'value' | 'onSelect' | 'error' | 'invalid'>
export function DatePickerWrapper<T extends FieldValues>({
  name,
  control,
  ...props
}: DatePickerProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <DatePicker
          {...props}
          name={name}
          value={field.value}
          onSelect={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type ComboboxProps<T extends FieldValues> = BaseProps<T> & Omit<React.ComponentProps<typeof Combobox>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
export function ComboboxWrapper<T extends FieldValues>({
  name,
  control,
  ...props
}: ComboboxProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Combobox
          {...props}
          name={name}
          value={field.value}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type NumberProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof NumberInput>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
export function NumberWrapper<T extends FieldValues>({ name, control, ...props }: NumberProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <NumberInput
          {...props}
          name={name}
          value={field.value ?? null}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type SliderProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof Slider>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
export function SliderWrapper<T extends FieldValues>({ name, control, ...props }: SliderProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Slider
          {...props}
          name={name}
          value={field.value}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type AutocompleteProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof Autocomplete>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
export function AutocompleteWrapper<T extends FieldValues>({ name, control, ...props }: AutocompleteProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Autocomplete
          {...props}
          name={name}
          value={field.value ?? ''}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type OTPProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof OTP>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
export function OTPWrapper<T extends FieldValues>({ name, control, ...props }: OTPProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <OTP
          {...props}
          name={name}
          value={field.value ?? ''}
          onValueChange={(value) => field.onChange(value)}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type InputGroupProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof InputGroup>, 'name' | 'value' | 'onChange' | 'onBlur' | 'error' | 'invalid'>
export function InputGroupWrapper<T extends FieldValues>({ name, control, ...props }: InputGroupProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <InputGroup
          {...props}
          name={name}
          value={field.value ?? ''}
          onChange={field.onChange}
          onBlur={field.onBlur}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

These wrappers automatically handle:

  • Label
  • Form control wiring
  • Error messages
  • Placeholder generation
  • Value parsing (e.g., converting "1" → number)

Usage

Basic

import { FormProvider, useForm } from "react-hook-form"

export function Basic() {
  const form = useForm({
    defaultValues: {/* ... */},
  })

  return (
    <FormProvider {...form}>
      <form
        onSubmit={form.handleSubmit((d) => console.log(d))}
      >
      // Wrapper component
      </form>
    </FormProvider>
  )
}

Controlled

import { FormProvider, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"

const formSchema = z.object({
  // your schema
})

export function Controlled() {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {/* ... */},
  })

  return (
    <FormProvider {...form}>
      <form
        onSubmit={form.handleSubmit((d) => console.log(d))}
      >
      // Wrapper component
      </form>
    </FormProvider>
  )
}

Reference

import { Control, FieldValues, Path } from "react-hook-form";

type BaseProps<T extends FieldValues> = {
  name: Path<T>
  label?: React.ReactNode
  control: Control<T>
  className?: string
}

InputWrapper

type InputProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
<InputWrapper
  name="username"
  label="Username"
  control={form.control}
/>

TextareaWrapper

type TextareaProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
<TextareaWrapper
  name="bio"
  label="Bio"
  control={form.control}
/>

RadioWrapper

type RadioProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
<RadioWrapper
  name="gender"
  label="Gender"
  control={form.control}
  options={["male", "female", "other"]}
/>

CheckboxWrapper

type CheckboxProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
<CheckboxWrapper
  name="interest"
  label="Interest"
  control={form.control}
  options={["Book reading", "Music", "TV", "Movie"]}
/>

SwitchWrapper

type SwitchProps<T extends FieldValues> = BaseProps<T>
<SwitchWrapper
  name="isCompleted"
  label="Is completed"
  control={form.control}
/>

Note: Value need to be boolean.

SelectWrapper

type SelectProps<T extends FieldValues> =
  BaseProps<T> &
  Omit<selectProps, "value" | "onValueChange">
<SelectWrapper
  name="country"
  label="Country"
  control={form.control}
  options={[
    { value: "in", label: "India" },
    { value: "us", label: "USA" }
  ]}
/>

DatePickerWrapper

type DatePickerProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof DatePicker>, 'name' | 'value' | 'onSelect' | 'error' | 'invalid'>
<DatePickerWrapper
  name="dob"
  label="Date of Birth"
  control={form.control}
/>

ComboboxWrapper

type ComboboxProps<T extends FieldValues> = BaseProps<T> &
  Omit<comboboxProps, "value" | "onValueChange">
<ComboboxWrapper
  name="fruit"
  label="Favorite Fruit"
  control={form.control}
  options={["Apple", "Banana", "Mango"]}
/>

SliderWrapper

type SliderProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof Slider>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
<SliderWrapper name="volume" label="Volume" control={form.control} min={0} max={100} />

// Range
<SliderWrapper name="price-range" label="Price range" control={form.control} min={0} max={500} step={10} />

Note: value type is number | number[]. Use number[] for range sliders.

OTPWrapper

type OTPProps<T extends FieldValues> = BaseProps<T> & {
  length?: number               // default 6
  separator?: boolean | number
  slotClassName?: string
  mask?: boolean
  validationType?: 'numeric' | 'alphanumeric' | 'none'
  disabled?: boolean
  readOnly?: boolean
}
<OTPWrapper
  name="otp"
  label="Verification Code"
  control={form.control}
  length={6}
  separator
/>

InputGroupWrapper

type InputGroupProps<T extends FieldValues> = BaseProps<T> & {
  addonStart?: React.ReactNode
  addonEnd?: React.ReactNode
  placeholder?: string
  disabled?: boolean
  readOnly?: boolean
  type?: string
}
<InputGroupWrapper
  name="username"
  label="Username"
  control={form.control}
  placeholder="username"
  addonStart={<InputGroupText>@</InputGroupText>}
/>