Select Field
Dropdown select component integrated with TanStack Form. Options are defined as Record<string, string> where keys are labels.
npx shadcn@latest add https://shuip.plvo.dev/r/tsf-select-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-select-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-select-field.json
import type {DeepKeys,DeepValue,FieldAsyncValidateOrFn,FieldOptions,FieldValidateOrFn,FormAsyncValidateOrFn,FormValidateOrFn,ReactFormApi,} from '@tanstack/react-form';import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';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<TFormData,TName extends DeepKeys<TFormData>,TData extends DeepValue<TFormData, TName> = DeepValue<TFormData, TName>,> {form: ReactFormApi<TFormData,undefined | FormValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,undefined | FormAsyncValidateOrFn<TFormData>,any>;name: TName;options: SelectFieldOption;label?: string;placeholder?: string;description?: string;formProps?: Partial<FieldOptions<TFormData,TName,TData,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>,undefined | FieldValidateOrFn<TFormData, TName, TData>,undefined | FieldAsyncValidateOrFn<TFormData, TName, TData>>>;fieldProps?: React.ComponentProps<typeof Field>;props?: React.ComponentProps<typeof Select>;}export function SelectField<TFormData,TName extends DeepKeys<TFormData>,TData extends DeepValue<TFormData, TName> = DeepValue<TFormData, TName>,>({form,name,options,label,description,placeholder,formProps,fieldProps,props,}: SelectFieldProps<TFormData, TName, TData>) {return (<form.Field name={name} {...formProps}>{(field) => {const { isValid, errors } = field.state.meta;return (<Field className='gap-2' data-invalid={!isValid} {...fieldProps}>{label && <FieldLabel>{label}</FieldLabel>}<Selectname={field.name}value={field.state.value as string}onValueChange={(value) => field.handleChange(value as TData)}{...props}><SelectTrigger aria-invalid={!isValid}><SelectValue placeholder={placeholder} /></SelectTrigger><SelectContent>{Object.entries(options).map(([label, value]) => (<SelectItem key={value} value={value}>{label}</SelectItem>))}</SelectContent></Select>{!isValid && (<FieldError className='text-xs text-left' errors={errors.map((error) => ({ message: error }))} />)}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}}</form.Field>);}
Loading...
Dropdowns present a list of options in a compact UI. SelectField uses Radix UI's Select primitive wrapped with TanStack Form state management. The options prop accepts a Record<string, string> where keys are the display labels and values are what gets stored in the form state.
This format makes it simple to define options inline or map from API responses. For dependent selects (e.g., country → states), use useStore to watch one field and update another field's options dynamically with setFieldValue.
Built-in features
- Record-based options for clean label/value mapping
- Dynamic options via state or API data
- Dependent selects with
form.useStoreandsetFieldValue - Empty state placeholder via
placeholderprop
Options format
const options = {
'Display Label 1': 'value1',
'Display Label 2': 'value2',
}
<SelectField
form={form}
name='choice'
options={options}
label='Select an option'
placeholder='Choose...'
/>Dependent selects
const [states, setStates] = React.useState<Record<string, string>>({})
// Watch country changes
form.useStore((state) => state.values.country, {
onChange: (country) => {
setStates(country === 'us' ? { 'California': 'ca', 'Texas': 'tx' } : {})
form.setFieldValue('state', '') // Reset state field
}
})
<SelectField form={form} name='country' options={{ 'United States': 'us' }} label='Country' />
<SelectField form={form} name='state' options={states} label='State' placeholder='Select country first' />Examples
Conditional
Loading...
'use client';import { useForm, useStore } from '@tanstack/react-form';import { InputField } from '@/components/ui/shuip/tanstack-form/input-field';import { SelectField } from '@/components/ui/shuip/tanstack-form/select-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfSelectFieldConditionalExample() {const form = useForm({defaultValues: {accountType: '',businessName: '',companySize: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});const accountType = useStore(form.store, (state) => state.values.accountType);return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><SelectFieldform={form}name='accountType'options={{Personal: 'personal',Business: 'business',}}label='Account Type'placeholder='Select account type'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select an account type' : undefined),},}}/>{/* Show business fields only when Business is selected */}{accountType === 'business' && (<><InputFieldform={form}name='businessName'label='Business Name'formProps={{validators: {onChange: ({ value }) => (!value ? 'Business name is required' : undefined),},}}/><SelectFieldform={form}name='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'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select company size' : undefined),},}}/></>)}<SubmitButton form={form}>Create Account</SubmitButton></form>);}
Dependent
Loading...
'use client';import { useForm } from '@tanstack/react-form';import React from 'react';import { SelectField } from '@/components/ui/shuip/tanstack-form/select-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/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',},};export default function TsfSelectFieldDependentExample() {const [stateOptions, setStateOptions] = React.useState<Record<string, string>>({});const form = useForm({defaultValues: {country: '',state: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><SelectFieldform={form}name='country'options={COUNTRIES}label='Country'placeholder='Select a country'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select a country' : undefined),},listeners: {onChange: ({ value }) => {if (value && STATES_BY_COUNTRY[value]) {setStateOptions(STATES_BY_COUNTRY[value]);} else {setStateOptions({});}// Reset state field when country changesform.setFieldValue('state', '');},},}}/><SelectFieldform={form}name='state'options={stateOptions}label='State / Province'placeholder={Object.keys(stateOptions).length === 0 ? 'Select a country first' : 'Select a state'}formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select a state' : undefined),},}}/><SubmitButton form={form}>Submit</SubmitButton></form>);}
Dynamic
Loading categories...
'use client';import { useForm } from '@tanstack/react-form';import React from 'react';import { SelectField } from '@/components/ui/shuip/tanstack-form/select-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';// Simulate API callasync function fetchCategories(): Promise<Record<string, string>> {await new Promise((resolve) => setTimeout(resolve, 1000));return {Technology: 'tech',Design: 'design',Marketing: 'marketing',Finance: 'finance',Health: 'health',};}export default function TsfSelectFieldDynamicExample() {const [categories, setCategories] = React.useState<Record<string, string>>({});const [isLoading, setIsLoading] = React.useState(true);const form = useForm({defaultValues: {category: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});React.useEffect(() => {fetchCategories().then((data) => {setCategories(data);setIsLoading(false);}).catch(() => {setIsLoading(false);});}, []);if (isLoading) {return <div className='text-muted-foreground'>Loading categories...</div>;}return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><SelectFieldform={form}name='category'options={categories}label='Category'placeholder='Select a category'description='Categories loaded from API'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select a category' : undefined),},}}/><SubmitButton form={form}>Submit</SubmitButton></form>);}
Default
Loading...
'use client';import { useForm } from '@tanstack/react-form';import { SelectField } from '@/components/ui/shuip/tanstack-form/select-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfSelectFieldExample() {const form = useForm({defaultValues: {country: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><SelectFieldform={form}name='country'options={{'United States': 'us','United Kingdom': 'uk',France: 'fr',Germany: 'de',}}label='Country'placeholder='Select a country'description='Choose your country'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select a country' : undefined),},}}/><SubmitButton form={form} /></form>);}
Props
Prop
Type