Field Wrapper
Opinionated form field wrappers combining Field layout, label, error display, and form controls. Available as library-agnostic, React Hook Form, and TanStack Form variants.
Generic
React Hook Form
TanStack Form
Installation
npx shadcn@latest add @glrk-ui/field-wrapperIf 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 { useState } from 'react'
import { CalendarIcon } from 'lucide-react'
import { format } from 'date-fns'
import { cn, parseAllowedPrimitive } from '@/lib/utils'
import { Field, FieldLabel, FieldSet, FieldLegend, FieldError } from './field'
import { Popover, PopoverContent, PopoverTrigger } from './popover'
import { AutocompleteWrapper as Autocomplete } from './autocomplete'
import { InputGroupWrapper as InputGroup } from './input-group'
import { InputOTPWrapper as InputOTP } from './input-otp'
import { ComboboxWrapper as Combobox } from './combobox'
import { CheckboxWrapper as Checkbox } from './checkbox'
import { SelectWrapper as Select } from './select'
import { RadioWrapper as Radio } from './radio'
import { NumberFieldWrapper } from './number-field'
import { Calendar } from './calendar'
import { Textarea } from './textarea'
import { Slider } from './slider'
import { Button } from './button'
import { Switch } from './switch'
import { Input } from './input'
type BaseProps = {
name: string
label?: React.ReactNode
error?: { message?: string }
invalid?: boolean
className?: string
}
function labelString(label: React.ReactNode): string | undefined {
return typeof label === 'string' ? label : undefined
}
type InputProps = BaseProps & React.InputHTMLAttributes<HTMLInputElement>
export function InputWrapper({
name,
label,
error,
invalid,
className,
type = 'text',
placeholder,
...props
}: InputProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Input
id={name}
name={name}
type={type}
placeholder={placeholder || (labelString(label) && `Enter ${labelString(label)}`)}
aria-invalid={isInvalid}
{...props}
/>
<FieldError errors={[error]} />
</Field>
)
}
type TextareaProps = BaseProps & React.TextareaHTMLAttributes<HTMLTextAreaElement>
export function TextareaWrapper({
name,
label,
error,
invalid,
className,
placeholder,
...rest
}: TextareaProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Textarea
id={name}
name={name}
placeholder={placeholder || (labelString(label) && `Enter ${labelString(label)}`)}
aria-invalid={isInvalid}
{...rest}
/>
<FieldError errors={[error]} />
</Field>
)
}
type RadioProps = BaseProps &
Omit<React.ComponentProps<typeof Radio>, 'value' | 'onValueChange' | 'as'> & {
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}
export function RadioWrapper({
name,
label,
error,
invalid,
className,
value,
onValueChange,
...props
}: RadioProps) {
const isInvalid = invalid || !!error
return (
<FieldSet className={cn(className)}>
{label && <FieldLegend variant="label">{label}</FieldLegend>}
<Radio
{...props}
value={value != null ? String(value) : undefined}
onValueChange={val => onValueChange?.(parseAllowedPrimitive(val))}
aria-invalid={isInvalid}
/>
<FieldError errors={[error]} />
</FieldSet>
)
}
type CheckboxProps = BaseProps &
Omit<React.ComponentProps<typeof Checkbox>, 'value' | 'onValueChange' | 'as'> & {
value?: allowedPrimitiveT[]
onValueChange?: (value: allowedPrimitiveT[]) => void
}
export function CheckboxWrapper({
name,
label,
error,
invalid,
className,
value = [],
onValueChange,
...props
}: CheckboxProps) {
const isInvalid = invalid || !!error
return (
<FieldSet className={cn(className)}>
{label && <FieldLegend variant="label">{label}</FieldLegend>}
<Checkbox
orientation="horizontal"
{...props}
value={value.map(String)}
onValueChange={vals => onValueChange?.(vals.map(parseAllowedPrimitive))}
aria-invalid={isInvalid}
/>
<FieldError errors={[error]} />
</FieldSet>
)
}
type SwitchProps = BaseProps & {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
}
export function SwitchWrapper({
name,
label,
error,
invalid,
className,
checked,
onCheckedChange,
}: SwitchProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
<div className="flex items-center justify-between gap-4">
{label && (
<FieldLabel htmlFor={name} className="font-normal">
{label}
</FieldLabel>
)}
<Switch
id={name}
checked={checked}
onCheckedChange={onCheckedChange}
aria-label={typeof label === 'string' ? label : name}
aria-invalid={isInvalid}
/>
</div>
<FieldError errors={[error]} />
</Field>
)
}
type SelectProps = BaseProps &
Omit<React.ComponentProps<typeof Select>, 'value' | 'onValueChange'> & {
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}
export function SelectWrapper({
name,
label,
error,
invalid,
className,
options,
placeholder,
value,
onValueChange,
...props
}: SelectProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Select
{...props}
id={name}
options={options}
value={value != null ? String(value) : undefined}
placeholder={placeholder ?? (labelString(label) && `Select ${labelString(label)}`)}
onValueChange={val => onValueChange?.(parseAllowedPrimitive(val as any))}
aria-invalid={isInvalid}
/>
<FieldError errors={[error]} />
</Field>
)
}
type DatePickerProps = BaseProps &
Omit<React.ComponentProps<typeof Calendar>, 'selected' | 'onSelect'> & {
value?: Date
onSelect?: (date: Date | undefined) => void
}
export function DatePickerWrapper({
name,
label,
error,
invalid,
className,
value,
onSelect,
...calendarProps
}: DatePickerProps) {
const [open, setOpen] = useState(false)
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={
<Button
id={name}
variant={'outline'}
className={cn('w-full pl-3 text-left font-normal shadow-xs', !value && 'text-muted-foreground')}
aria-invalid={isInvalid}
>
{value ? format(value, 'dd/MM/yyyy') : <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
}
/>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
autoFocus
mode="single"
captionLayout="dropdown"
selected={value}
onSelect={date => {
onSelect?.(date)
setOpen(false)
}}
defaultMonth={value}
{...(calendarProps as any)}
/>
</PopoverContent>
</Popover>
<FieldError errors={[error]} />
</Field>
)
}
type ComboboxProps = BaseProps & React.ComponentProps<typeof Combobox>
export function ComboboxWrapper({
name,
label,
error,
invalid,
className,
placeholder,
value,
onValueChange,
...rest
}: ComboboxProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Combobox
{...rest}
id={name}
value={value}
placeholder={placeholder || (labelString(label) && `Select ${labelString(label)}`)}
onValueChange={onValueChange}
aria-invalid={isInvalid}
/>
<FieldError errors={[error]} />
</Field>
)
}
type NumberProps = BaseProps &
Omit<React.ComponentProps<typeof NumberFieldWrapper>, 'name' | 'id' | 'className'>
export function NumberWrapper({
name,
label,
error,
invalid,
className,
...props
}: NumberProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<NumberFieldWrapper id={name} name={name} aria-invalid={isInvalid} {...props} />
<FieldError errors={[error]} />
</Field>
)
}
type SliderProps = BaseProps &
Omit<React.ComponentProps<typeof Slider>, 'name'>
export function SliderWrapper({
name,
label,
error,
invalid,
className,
...props
}: SliderProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel>{label}</FieldLabel>}
<Slider name={name} aria-invalid={isInvalid} {...props} />
<FieldError errors={[error]} />
</Field>
)
}
type AutocompleteProps = BaseProps &
Omit<React.ComponentProps<typeof Autocomplete>, 'className'>
export function AutocompleteWrapper({
name,
label,
error,
invalid,
className,
...props
}: AutocompleteProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Autocomplete aria-invalid={isInvalid} {...props} />
<FieldError errors={[error]} />
</Field>
)
}
type OTPProps = BaseProps & Omit<React.ComponentProps<typeof InputOTP>, 'className'>
export function OTPWrapper({
name,
label,
error,
invalid,
className,
...props
}: OTPProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel>{label}</FieldLabel>}
<InputOTP name={name} aria-invalid={isInvalid} {...props} />
<FieldError errors={[error]} />
</Field>
)
}
type InputGroupFieldProps = BaseProps & Omit<React.ComponentProps<typeof InputGroup>, 'wrapperClassName'>
export function InputGroupWrapper({
name,
label,
error,
invalid,
className,
...props
}: InputGroupFieldProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<InputGroup
id={name}
name={name}
aria-invalid={isInvalid}
{...props}
/>
<FieldError errors={[error]} />
</Field>
)
}
For React Hook Form integration:
'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}
/>
)}
/>
)
}
For TanStack Form integration:
'use client'
import { createFormHookContexts, createFormHook } from '@tanstack/react-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'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
type inputFieldProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange' | 'onBlur'
> & {
label?: React.ReactNode
}
function InputField(props: inputFieldProps) {
const field = useFieldContext<string>()
return (
<Input
{...props}
name={field.name}
value={field.state.value ?? ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type textareaFieldProps = Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
'value' | 'onChange' | 'onBlur'
> & {
label?: React.ReactNode
}
function TextareaField(props: textareaFieldProps) {
const field = useFieldContext<string>()
return (
<Textarea
{...props}
name={field.name}
value={field.state.value ?? ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type radioFieldProps = {
label?: React.ReactNode
options: (allowedPrimitiveT | optionT)[]
className?: string
}
function RadioField(props: radioFieldProps) {
const field = useFieldContext<allowedPrimitiveT>()
return (
<Radio
{...props}
name={field.name}
value={field.state.value}
onValueChange={value => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
function CheckboxField(props: radioFieldProps) {
const field = useFieldContext<allowedPrimitiveT[]>()
return (
<Checkbox
{...props}
name={field.name}
value={field.state.value ?? []}
onValueChange={value => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type switchFieldProps = {
label?: React.ReactNode
className?: string
}
function SwitchField(props: switchFieldProps) {
const field = useFieldContext<boolean>()
return (
<Switch
{...props}
name={field.name}
checked={field.state.value ?? false}
onCheckedChange={checked => field.handleChange(checked)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type selectFieldProps = Omit<React.ComponentProps<typeof Select>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
function SelectField(props: selectFieldProps) {
const field = useFieldContext<allowedPrimitiveT>()
return (
<Select
{...props}
name={field.name}
value={field.state.value}
onValueChange={value => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type datePickerFieldProps = Omit<
React.ComponentProps<typeof DatePicker>,
'name' | 'value' | 'onSelect' | 'error' | 'invalid'
> & {
label?: React.ReactNode
}
function DatePickerField(props: datePickerFieldProps) {
const field = useFieldContext<Date | undefined>()
return (
<DatePicker
{...props}
name={field.name}
value={field.state.value}
onSelect={date => field.handleChange(date)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type comboboxFieldProps = Omit<React.ComponentProps<typeof Combobox>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
function ComboboxField(props: comboboxFieldProps) {
const field = useFieldContext<allowedPrimitiveT | allowedPrimitiveT[]>()
return (
<Combobox
{...props}
name={field.name}
value={field.state.value}
onValueChange={value => field.handleChange(value as allowedPrimitiveT | allowedPrimitiveT[])}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type numberFieldProps = Omit<
React.ComponentProps<typeof NumberInput>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function NumberField(props: numberFieldProps) {
const field = useFieldContext<number | null>()
return (
<NumberInput
{...props}
name={field.name}
value={field.state.value}
onValueChange={val => field.handleChange(val)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type sliderFieldProps = Omit<
React.ComponentProps<typeof Slider>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function SliderField(props: sliderFieldProps) {
const field = useFieldContext<number | number[]>()
return (
<Slider
{...props}
name={field.name}
value={field.state.value}
onValueChange={val => field.handleChange(val as number | number[])}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type autocompleteFieldProps = Omit<
React.ComponentProps<typeof Autocomplete>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function AutocompleteField(props: autocompleteFieldProps) {
const field = useFieldContext<string>()
return (
<Autocomplete
{...props}
name={field.name}
value={field.state.value ?? ''}
onValueChange={val => field.handleChange(val)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type otpFieldProps = Omit<
React.ComponentProps<typeof OTP>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function OTPField(props: otpFieldProps) {
const field = useFieldContext<string>()
return (
<OTP
{...props}
name={field.name}
value={field.state.value ?? ''}
onValueChange={(value) => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type inputGroupFieldProps = Omit<
React.ComponentProps<typeof InputGroup>,
'name' | 'value' | 'onChange' | 'onBlur' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function InputGroupField(props: inputGroupFieldProps) {
const field = useFieldContext<string>()
return (
<InputGroup
{...props}
name={field.name}
value={field.state.value ?? ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
export const { useAppForm, withForm, withFieldGroup } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
InputField,
TextareaField,
RadioField,
CheckboxField,
SwitchField,
SelectField,
DatePickerField,
ComboboxField,
NumberField,
SliderField,
AutocompleteField,
OTPField,
InputGroupField,
},
formComponents: {},
})
These wrappers automatically handle:
- Label
- Form control wiring
- Error messages
- Placeholder generation
- Value parsing (e.g., converting
"1"→ number)
Available wrappers
All wrappers are available in three flavours:
| Wrapper | Generic import | RHF import | TanStack Form |
|---|---|---|---|
InputWrapper | field-wrapper | field-wrapper-rhf | field.InputField |
TextareaWrapper | field-wrapper | field-wrapper-rhf | field.TextareaField |
RadioWrapper | field-wrapper | field-wrapper-rhf | field.RadioField |
CheckboxWrapper | field-wrapper | field-wrapper-rhf | field.CheckboxField |
SwitchWrapper | field-wrapper | field-wrapper-rhf | field.SwitchField |
SelectWrapper | field-wrapper | field-wrapper-rhf | field.SelectField |
DatePickerWrapper | field-wrapper | field-wrapper-rhf | field.DatePickerField |
ComboboxWrapper | field-wrapper | field-wrapper-rhf | field.ComboboxField |
AutocompleteWrapper | field-wrapper | field-wrapper-rhf | field.AutocompleteField |
NumberWrapper | field-wrapper | field-wrapper-rhf | field.NumberField |
SliderWrapper | field-wrapper | field-wrapper-rhf | field.SliderField |
OTPWrapper | field-wrapper | field-wrapper-rhf | field.OTPField |
InputGroupWrapper | field-wrapper | field-wrapper-rhf | field.InputGroupField |
Usage
Generic (library-agnostic)
import {
InputWrapper,
SelectWrapper,
ComboboxWrapper,
NumberWrapper,
SliderWrapper,
} from "@/components/ui/field-wrapper"
<InputWrapper
name="email"
label="Email"
type="email"
value={value}
onChange={e => setValue(e.target.value)}
/>
<SelectWrapper
name="country"
label="Country"
options={[
{ value: "us", label: "United States" },
{ value: "uk", label: "United Kingdom" },
]}
value={country}
onValueChange={setCountry}
/>
<NumberWrapper
name="qty"
label="Quantity"
value={qty}
onValueChange={setQty}
min={0}
step={1}
/>
<SliderWrapper
name="volume"
label="Volume"
value={[volume]}
onValueChange={([v]) => setVolume(v)}
min={0}
max={100}
/>Validation state
All wrappers accept invalid and error props:
<InputWrapper
name="email"
label="Email"
invalid={!!errors.email}
error={errors.email}
/>React Hook Form
import {
InputWrapper,
SelectWrapper,
NumberWrapper,
SliderWrapper,
AutocompleteWrapper,
} from "@/components/ui/field-wrapper-rhf"
import { FormProvider, useForm } from "react-hook-form"
const form = useForm({ defaultValues: { email: "", country: "" } })
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<InputWrapper name="email" label="Email" type="email" control={form.control} />
<SelectWrapper name="country" label="Country" options={countryOptions} control={form.control} />
<NumberWrapper name="qty" label="Quantity" control={form.control} min={0} />
<SliderWrapper name="volume" label="Volume" control={form.control} min={0} max={100} />
<AutocompleteWrapper name="city" label="City" items={cities} control={form.control} />
<button type="submit">Submit</button>
</form>
</FormProvider>TanStack Form
import { useAppForm } from "@/components/ui/field-wrapper-tf"
const form = useAppForm({
defaultValues: { email: "", qty: null, volume: [50] },
onSubmit: async ({ value }) => console.log(value),
})
<form onSubmit={e => { e.preventDefault(); form.handleSubmit() }}>
<form.AppField
name="email"
validators={{
onChange: ({ value }) =>
!value.includes("@") ? "Invalid email" : undefined,
}}
>
{field => <field.InputField label="Email" type="email" />}
</form.AppField>
<form.AppField name="qty">
{field => <field.NumberField label="Quantity" min={0} />}
</form.AppField>
<form.AppField name="volume">
{field => <field.SliderField label="Volume" min={0} max={100} />}
</form.AppField>
<button type="submit">Submit</button>
</form>Combobox multi-select
// Generic
<ComboboxWrapper
multiple
name="hobbies"
label="Hobbies"
items={["Music", "Sports", "Travel", "Gaming"]}
value={hobbies}
onValueChange={setHobbies}
/>
// RHF
<ComboboxWrapper multiple name="hobbies" label="Hobbies" items={items} control={form.control} />
// TanStack Form
<form.AppField name="hobbies">
{field => <field.ComboboxField multiple label="Hobbies" items={items} />}
</form.AppField>DatePicker
// Generic
<DatePickerWrapper
name="dob"
label="Date of Birth"
value={date}
onSelect={setDate}
/>
// RHF
<DatePickerWrapper name="dob" label="Date of Birth" control={form.control} />
// TanStack Form
<form.AppField name="dob">
{field => <field.DatePickerField label="Date of Birth" />}
</form.AppField>OTP
import { OTPWrapper } from "@/components/ui/field-wrapper"
// Generic
<OTPWrapper
name="otp"
label="Verification Code"
length={6}
separator
value={value}
onValueChange={setValue}
/>
// RHF
<OTPWrapper name="otp" label="Verification Code" control={form.control} length={6} separator />
// TanStack Form
<form.AppField name="otp">
{field => <field.OTPField label="Verification Code" length={6} separator />}
</form.AppField>Input Group
import { InputGroupWrapper } from "@/components/ui/field-wrapper"
import { InputGroupText } from "@/components/ui/input-group"
import { AtSignIcon } from "lucide-react"
// Generic
<InputGroupWrapper
name="username"
label="Username"
placeholder="username"
addonStart={<InputGroupText><AtSignIcon /></InputGroupText>}
value={value}
onChange={e => setValue(e.target.value)}
/>
// RHF
<InputGroupWrapper
name="username"
label="Username"
control={form.control}
placeholder="username"
addonStart={<InputGroupText><AtSignIcon /></InputGroupText>}
/>
// TanStack Form
<form.AppField name="username">
{field => (
<field.InputGroupField
label="Username"
placeholder="username"
addonStart={<InputGroupText><AtSignIcon /></InputGroupText>}
/>
)}
</form.AppField>Reference
BaseProps (shared by all wrappers)
Prop
Type
InputWrapper
type InputProps = BaseProps &
React.InputHTMLAttributes<HTMLInputElement><InputWrapper
name="username"
label="Username"
value={value}
onChange={e => onChange(e.target.value)}
/>TextareaWrapper
type TextareaProps = BaseProps &
React.TextareaHTMLAttributes<HTMLTextAreaElement><TextareaWrapper
name="bio"
label="Bio"
value={value}
onChange={e => onChange(e.target.value)}
/>RadioWrapper
type RadioProps = BaseProps & {
options: (allowedPrimitiveT | optionT)[]
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}<RadioWrapper
name="gender"
label="Gender"
options={["male", "female", "other"]}
value={value}
onValueChange={val => onChange(val)}
/>CheckboxWrapper
type CheckboxProps = BaseProps & {
options: (allowedPrimitiveT | optionT)[]
value?: allowedPrimitiveT[]
onValueChange?: (value: allowedPrimitiveT[]) => void
}<CheckboxWrapper
name="interest"
label="Interest"
options={["Book reading", "Music", "TV", "Movie"]}
value={value}
onValueChange={val => onChange(val)}
/>SwitchWrapper
type SwitchProps = BaseProps & {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
}<SwitchWrapper
name="isCompleted"
label="Is completed"
checked={value}
onCheckedChange={val => onChange(val)}
/>SelectWrapper
type SelectProps = BaseProps & Omit<selectProps, "value" | "onValueChange"> & {
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}<SelectWrapper
name="country"
label="Country"
options={[
{ value: "in", label: "India" },
{ value: "us", label: "USA" }
]}
value={value}
onValueChange={val => onChange(val)}
/>DatePickerWrapper
type DatePickerProps = BaseProps & Omit<React.ComponentProps<typeof Calendar>, "selected" | "onSelect"> & {
value?: Date
onSelect?: (date: Date | undefined) => void
}<DatePickerWrapper
name="dob"
label="Date of Birth"
value={value}
onSelect={val => onChange(val)}
/>ComboboxWrapper
type ComboboxProps = BaseProps & comboboxProps<ComboboxWrapper
name="fruit"
label="Favorite Fruit"
options={["Apple", "Banana", "Mango"]}
value={value}
onValueChange={val => onChange(val)}
/>OTPWrapper
type OTPProps = BaseProps & {
length?: number // default 6
separator?: boolean | number // true = split in half, number = split after Nth slot
slotClassName?: string
value?: string
onValueChange?: (value: string, eventDetails: object) => void
mask?: boolean
validationType?: 'numeric' | 'alphanumeric' | 'none'
disabled?: boolean
readOnly?: boolean
required?: boolean
}<OTPWrapper
name="otp"
label="Verification Code"
length={6}
separator
value={value}
onValueChange={setValue}
/>InputGroupWrapper
type InputGroupFieldProps = BaseProps & {
addonStart?: React.ReactNode // inline-start addon (text, icon, button)
addonEnd?: React.ReactNode // inline-end addon (text, icon, button)
placeholder?: string
value?: string
onChange?: React.ChangeEventHandler<HTMLInputElement>
disabled?: boolean
readOnly?: boolean
type?: string
}<InputGroupWrapper
name="amount"
label="Amount"
placeholder="0.00"
addonStart={<InputGroupText><DollarSignIcon /></InputGroupText>}
addonEnd={<InputGroupText>USD</InputGroupText>}
value={value}
onChange={e => setValue(e.target.value)}
/>