Field with RHF
Reusable Field field wrappers built on top of shadcn/ui with react-hook-form
Installation
npx shadcn@latest add @glrk-ui/field-wrapper-rhfIf 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.
'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>}
/>