Select Field
Dropdown select component integrated with TanStack Form via React context. 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
'use client';import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';import { useFieldContext } from '@/components/ui/shuip/tanstack-form/form-context';export type SelectFieldOption = Record<string, string>;export interface SelectFieldProps {options: SelectFieldOption;label?: string;placeholder?: string;description?: string;fieldProps?: React.ComponentProps<typeof Field>;props?: React.ComponentProps<typeof Select>;}export function SelectField({ options, label, description, placeholder, fieldProps, props }: SelectFieldProps) {const field = useFieldContext<string>();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}onValueChange={(value) => field.handleChange(value)}{...props}><SelectTrigger aria-invalid={!isValid}><SelectValue placeholder={placeholder} /></SelectTrigger><SelectContent>{Object.entries(options).map(([optionLabel, value]) => (<SelectItem key={value} value={value}>{optionLabel}</SelectItem>))}</SelectContent></Select>{!isValid && (<FieldErrorclassName='text-xs text-left'errors={errors.map((error) => ({ message: typeof error === 'string' ? error : error?.message }))}/>)}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}
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.
The component reads the surrounding field via useFieldContext, so you compose it inside a <form.AppField>. 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
- Context-bound field state via
useFieldContext— no prop drilling - 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
Setup
Field components are bound via React context. In your project, create lib/form.ts once:
// lib/form.ts
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { SelectField } from '@/components/ui/shuip/tanstack-form/select-field';
import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { SelectField },
formComponents: { SubmitButton },
});See the form-context item for details.
Options format
import { useAppForm } from '@/lib/form';
const options = {
'Display Label 1': 'value1',
'Display Label 2': 'value2',
};
const form = useAppForm({
defaultValues: { choice: '' },
onSubmit: async ({ value }) => {
await saveData(value);
},
});
<form.AppField
name='choice'
children={(field) => (
<field.SelectField
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
},
});
<form.AppField
name='country'
children={(field) => (
<field.SelectField options={{ 'United States': 'us' }} label='Country' />
)}
/>
<form.AppField
name='state'
children={(field) => (
<field.SelectField
options={states}
label='State'
placeholder='Select country first'
/>
)}
/>Examples
Conditional
'use client';import { createFormHook, useStore } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';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';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { InputField, SelectField },formComponents: { SubmitButton },});export default function TsfSelectFieldConditionalExample() {const form = useAppForm({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'><form.AppFieldname='accountType'validators={{onChange: ({ value }) => (!value ? 'Please select an account type' : undefined),}}children={(field) => (<field.SelectFieldoptions={{Personal: 'personal',Business: 'business',}}label='Account Type'placeholder='Select account type'/>)}/>{/* Show business fields only when Business is selected */}{accountType === 'business' && (<><form.AppFieldname='businessName'validators={{onChange: ({ value }) => (!value ? 'Business name is required' : undefined),}}children={(field) => <field.InputField label='Business Name' />}/><form.AppFieldname='companySize'validators={{onChange: ({ value }) => (!value ? 'Please select company size' : undefined),}}children={(field) => (<field.SelectFieldoptions={{'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'/>)}/></>)}<form.AppForm><form.SubmitButton>Create Account</form.SubmitButton></form.AppForm></form>);}
Dependent
'use client';import { createFormHook } from '@tanstack/react-form';import React from 'react';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';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',},};const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { SelectField },formComponents: { SubmitButton },});export default function TsfSelectFieldDependentExample() {const [stateOptions, setStateOptions] = React.useState<Record<string, string>>({});const form = useAppForm({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'><form.AppFieldname='country'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', '');},}}children={(field) => <field.SelectField options={COUNTRIES} label='Country' placeholder='Select a country' />}/><form.AppFieldname='state'validators={{onChange: ({ value }) => (!value ? 'Please select a state' : undefined),}}children={(field) => (<field.SelectFieldoptions={stateOptions}label='State / Province'placeholder={Object.keys(stateOptions).length === 0 ? 'Select a country first' : 'Select a state'}/>)}/><form.AppForm><form.SubmitButton>Submit</form.SubmitButton></form.AppForm></form>);}
Dynamic
'use client';import { createFormHook } from '@tanstack/react-form';import React from 'react';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';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',};}const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { SelectField },formComponents: { SubmitButton },});export default function TsfSelectFieldDynamicExample() {const [categories, setCategories] = React.useState<Record<string, string>>({});const [isLoading, setIsLoading] = React.useState(true);const form = useAppForm({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'><form.AppFieldname='category'validators={{onChange: ({ value }) => (!value ? 'Please select a category' : undefined),}}children={(field) => (<field.SelectFieldoptions={categories}label='Category'placeholder='Select a category'description='Categories loaded from API'/>)}/><form.AppForm><form.SubmitButton>Submit</form.SubmitButton></form.AppForm></form>);}
Default
'use client';import { createFormHook } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { SelectField } from '@/components/ui/shuip/tanstack-form/select-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { SelectField },formComponents: { SubmitButton },});export default function TsfSelectFieldExample() {const form = useAppForm({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'><form.AppFieldname='country'validators={{onChange: ({ value }) => (!value ? 'Please select a country' : undefined),}}children={(field) => (<field.SelectFieldoptions={{'United States': 'us','United Kingdom': 'uk',France: 'fr',Germany: 'de',}}label='Country'placeholder='Select a country'description='Choose your country'/>)}/><form.AppForm><form.SubmitButton /></form.AppForm></form>);}
Props
Prop
Type