Select Field

Select dropdown component integrated with React Hook Form for single-choice selections from key-value option pairs.

npx shadcn@latest add https://shuip.plvo.dev/r/rhf-select-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-select-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-select-field.json
import type { SelectProps } from '@radix-ui/react-select';
import type { FieldPath, FieldValues, UseFormRegisterReturn } from 'react-hook-form';
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
/**
* Key is the label, value is the value
* @example
* const options: SelectFieldOption = {
* 'First': '1',
* 'Second': '2',
* 'Third': '3',
* };
*/
export type SelectFieldOption = Record<string, string>;
export interface SelectFieldProps<TFieldValues extends FieldValues> extends SelectProps {
register: UseFormRegisterReturn<FieldPath<TFieldValues>>;
options: SelectFieldOption;
label?: string;
placeholder?: string;
description?: string;
defaultValue?: TFieldValues[FieldPath<TFieldValues>];
}
export function SelectField<TFieldValues extends FieldValues>({
register,
options,
label,
description,
placeholder,
defaultValue,
...props
}: SelectFieldProps<TFieldValues>) {
return (
<FormField
{...register}
defaultValue={defaultValue}
render={({ field, fieldState }) => (
<FormItem data-invalid={fieldState.invalid}>
{label && <FormLabel>{label}</FormLabel>}
<Select defaultValue={field.value} onValueChange={field.onChange} {...props}>
<FormControl>
<SelectTrigger aria-invalid={fieldState.invalid} className='w-full'>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(options).map(([label, value]) => (
<SelectItem key={label} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage className='text-xs text-left' />
{description && <FormDescription className='text-xs'>{description}</FormDescription>}
</FormItem>
)}
/>
);
}
Loading...

Select dropdowns provide single-choice selection from a list of options with labels and values. SelectField automatically renders select options from a key-value object, where keys become display labels and values become form data.

The component handles the complexity of connecting Radix UI's Select with React Hook Form, including proper value binding, change handlers, and validation state management.

Built-in features

  • Automatic option rendering: Pass a key-value object and get a fully functional select dropdown
  • Label-value mapping: Keys display to users, values are stored in form data
  • Placeholder support: Optional placeholder text for unselected state
  • Zod validation: Native integration with react-hook-form and Zod schemas
  • Accessibility support: Full keyboard navigation and screen reader compatibility

Less boilerplate

<FormField
  control={form.control}
  name="country"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Country</FormLabel>
      <Select onValueChange={field.onChange} defaultValue={field.value}>
        <FormControl>
          <SelectTrigger>
            <SelectValue placeholder="Select a country" />
          </SelectTrigger>
        </FormControl>
        <SelectContent>
          <SelectItem value="us">United States</SelectItem>
          <SelectItem value="ca">Canada</SelectItem>
          <SelectItem value="fr">France</SelectItem>
        </SelectContent>
      </Select>
      <FormDescription>
        Choose your country
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

With SelectField, this reduces to a single declarative component:

<SelectField 
  register={form.register('country')} 
  label="Country" 
  placeholder="Select a country"
  description="Choose your country"
  options={{ 'United States': 'us', 'Canada': 'ca', 'France': 'fr' }}
/>

Examples

Conditional

Loading...
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Form } from '@/components/ui/form';
import { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
import { SelectField } from '@/components/ui/shuip/react-hook-form/select-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const zodSchema = z.object({
accountType: z.enum(['personal', 'business']),
businessName: z.string().optional(),
companySize: z.string().optional(),
});
export default function RhfSelectFieldConditionalExample() {
const form = useForm({
defaultValues: {
accountType: undefined,
businessName: '',
companySize: '',
},
resolver: zodResolver(zodSchema),
});
const accountType = form.watch('accountType');
async function onSubmit(values: z.infer<typeof zodSchema>) {
try {
alert(JSON.stringify(values, null, 2));
} catch (error) {
console.error(error);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<SelectField
register={form.register('accountType')}
options={{
Personal: 'personal',
Business: 'business',
}}
label='Account Type'
placeholder='Select account type'
description='Choose the type of account you want to create'
/>
{/* Show business fields only when Business is selected */}
{accountType === 'business' && (
<>
<InputField
register={form.register('businessName')}
label='Business Name'
placeholder='Enter your business name'
description='Legal name of your business'
/>
<SelectField
register={form.register('companySize')}
options={{
'1-10 employees': '1-10',
'11-50 employees': '11-50',
'51-200 employees': '51-200',
'201-1000 employees': '201-1000',
'1000+ employees': '1000+',
}}
label='Company Size'
placeholder='Select company size'
description='Number of employees in your company'
/>
</>
)}
<SubmitButton>Create Account</SubmitButton>
</form>
</Form>
);
}

Dependent

Loading...
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Form } from '@/components/ui/form';
import { SelectField } from '@/components/ui/shuip/react-hook-form/select-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const COUNTRIES = {
'United States': 'us',
Canada: 'ca',
Mexico: 'mx',
};
const STATES_BY_COUNTRY: Record<string, Record<string, string>> = {
us: {
California: 'ca',
Texas: 'tx',
'New York': 'ny',
Florida: 'fl',
},
ca: {
Ontario: 'on',
Quebec: 'qc',
'British Columbia': 'bc',
Alberta: 'ab',
},
mx: {
'Mexico City': 'cdmx',
Jalisco: 'jal',
'Nuevo León': 'nl',
},
};
const zodSchema = z.object({
country: z.string().min(1, 'Please select a country'),
state: z.string().min(1, 'Please select a state'),
});
export default function RhfSelectFieldDependentExample() {
const [stateOptions, setStateOptions] = useState<Record<string, string>>({});
const form = useForm({
defaultValues: {
country: '',
state: '',
},
resolver: zodResolver(zodSchema),
});
const country = form.watch('country');
useEffect(() => {
if (country && STATES_BY_COUNTRY[country]) {
setStateOptions(STATES_BY_COUNTRY[country]);
} else {
setStateOptions({});
}
// Reset state field when country changes
form.setValue('state', '');
}, [country, form]);
async function onSubmit(values: z.infer<typeof zodSchema>) {
try {
alert(JSON.stringify(values, null, 2));
} catch (error) {
console.error(error);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<SelectField
register={form.register('country')}
options={COUNTRIES}
label='Country'
placeholder='Select a country'
description='Choose your country'
/>
<SelectField
register={form.register('state')}
options={stateOptions}
label='State / Province'
placeholder={Object.keys(stateOptions).length === 0 ? 'Select a country first' : 'Select a state'}
description='Choose your state or province'
/>
<SubmitButton>Submit</SubmitButton>
</form>
</Form>
);
}

Default

Loading...
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Form } from '@/components/ui/form';
import { SelectField, type SelectFieldOption } from '@/components/ui/shuip/react-hook-form/select-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const options: SelectFieldOption = {
First: '1',
Second: '2',
Third: '3',
Fourth: '4',
};
const zodSchema = z.object({
selection: z.enum(Object.values(options) as [string]),
});
export default function SelectFieldExample() {
const form = useForm({
defaultValues: {
selection: '1',
},
resolver: zodResolver(zodSchema),
});
async function onSubmit(values: z.infer<typeof zodSchema>) {
try {
alert(`Selection: ${values.selection}`);
} catch (error) {
console.error(error);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<SelectField
register={form.register('selection')}
placeholder='Select an option'
label='selection'
options={options}
defaultValue={'3'}
/>
<SubmitButton>Check</SubmitButton>
</form>
</Form>
);
}

Props

Prop

Type

On this page