Radio Field
Radio button group component integrated with TanStack Form. Options are defined as Array<{label, value}>.
npx shadcn@latest add https://shuip.plvo.dev/r/tsf-radio-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-radio-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-radio-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 { Label } from '@/components/ui/label';import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';export interface RadioFieldOption {label: string;value: string;}export interface RadioFieldProps<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: RadioFieldOption[];label?: 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 RadioGroup>;}export function RadioField<TFormData,TName extends DeepKeys<TFormData>,TData extends DeepValue<TFormData, TName> = DeepValue<TFormData, TName>,>({ form, name, options, label, description, formProps, fieldProps, props }: RadioFieldProps<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>}<RadioGroupname={field.name}value={field.state.value as string}onValueChange={(value) => field.handleChange(value as TData)}onBlur={field.handleBlur}{...props}>{options.map(({ label, value }) => (<div key={value} className='flex items-center space-x-3 space-y-0'><RadioGroupItem id={`${field.name}-${value}`} value={value} aria-invalid={!isValid} /><Label htmlFor={`${field.name}-${value}`}>{label}</Label></div>))}</RadioGroup>{!isValid && (<FieldError className='text-xs text-left' errors={errors.map((error) => ({ message: error }))} />)}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}}</form.Field>);}
Loading...
Radio groups let users select a single option from a list. RadioField uses Radix UI's RadioGroup with automatic label associations and proper ARIA attributes. Unlike SelectField, radio buttons show all options upfront—ideal for 2-5 choices where comparison is important.
The options prop takes an array of {label, value} objects, making it easy to map from data structures. Each radio button gets a unique ID combining the field name and value, ensuring proper accessibility.
Built-in features
- Visible all-at-once for easy comparison
- Array-based options with label/value structure
- Automatic label linking with unique IDs per option
- Single-selection enforcement via RadioGroup
Options structure
const options = [
{ label: 'Display Text 1', value: 'val1' },
{ label: 'Display Text 2', value: 'val2' },
]
<RadioField
form={form}
name='choice'
options={options}
label='Select one'
/>Examples
Conditional Pricing
Loading...
'use client';import { useForm, useStore } from '@tanstack/react-form';import { Card } from '@/components/ui/card';import { RadioField } from '@/components/ui/shuip/tanstack-form/radio-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';const PLANS = [{ label: 'Free - $0/month', value: 'free', price: 0 },{ label: 'Pro - $29/month', value: 'pro', price: 29 },{ label: 'Enterprise - $99/month', value: 'enterprise', price: 99 },];export default function TsfRadioFieldConditionalPricingExample() {const form = useForm({defaultValues: {plan: '',billingCycle: 'monthly',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});const plan = useStore(form.store, (state) => state.values.plan);const billingCycle = useStore(form.store, (state) => state.values.billingCycle);const selectedPlan = PLANS.find((p) => p.value === plan);const monthlyPrice = selectedPlan?.price || 0;const annualPrice = monthlyPrice * 12 * 0.8; // 20% discountconst displayPrice = billingCycle === 'annual' ? annualPrice / 12 : monthlyPrice;return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><RadioFieldform={form}name='plan'options={PLANS.map((p) => ({ label: p.label, value: p.value }))}label='Select Plan'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select a plan' : undefined),},}}/>{plan && plan !== 'free' && (<RadioFieldform={form}name='billingCycle'options={[{ label: 'Monthly', value: 'monthly' },{ label: 'Annual (Save 20%)', value: 'annual' },]}label='Billing Cycle'/>)}{plan && (<Card className='p-4'><div className='space-y-2'><div className='flex justify-between'><span className='font-semibold'>Plan:</span><span className='capitalize'>{plan}</span></div>{plan !== 'free' && (<><div className='flex justify-between'><span className='font-semibold'>Billing:</span><span className='capitalize'>{billingCycle}</span></div><div className='flex justify-between text-lg font-bold'><span>Total:</span><span>${displayPrice.toFixed(2)}/month</span></div>{billingCycle === 'annual' && (<p className='text-sm text-muted-foreground'>Billed annually at ${annualPrice.toFixed(2)}</p>)}</>)}</div></Card>)}<SubmitButton form={form}>Continue</SubmitButton></form>);}
Default
Loading...
'use client';import { useForm } from '@tanstack/react-form';import { RadioField } from '@/components/ui/shuip/tanstack-form/radio-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfRadioFieldExample() {const form = useForm({defaultValues: {plan: '',},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'><RadioFieldform={form}name='plan'options={[{ label: 'Free', value: 'free' },{ label: 'Pro', value: 'pro' },{ label: 'Enterprise', value: 'enterprise' },]}label='Subscription Plan'description='Choose your subscription plan'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select a plan' : undefined),},}}/><SubmitButton form={form} /></form>);}
Payment Method
Loading...
'use client';import { useForm, useStore } from '@tanstack/react-form';import { InputField } from '@/components/ui/shuip/tanstack-form/input-field';import { RadioField } from '@/components/ui/shuip/tanstack-form/radio-field';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';export default function TsfRadioFieldPaymentMethodExample() {const form = useForm({defaultValues: {paymentMethod: '',cardNumber: '',paypalEmail: '',accountNumber: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 1000));alert(JSON.stringify(value, null, 2));},});const paymentMethod = useStore(form.store, (state) => state.values.paymentMethod);return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><RadioFieldform={form}name='paymentMethod'options={[{ label: 'Credit Card', value: 'card' },{ label: 'PayPal', value: 'paypal' },{ label: 'Bank Transfer', value: 'bank' },]}label='Payment Method'description='Select how you want to pay'formProps={{validators: {onChange: ({ value }) => (!value ? 'Please select a payment method' : undefined),},}}/>{paymentMethod === 'card' && (<InputFieldform={form}name='cardNumber'label='Card Number'props={{ placeholder: '1234 5678 9012 3456' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'Card number is required';if (!/^\d{4}\s?\d{4}\s?\d{4}\s?\d{4}$/.test(value)) return 'Invalid card number format';return undefined;},},}}/>)}{paymentMethod === 'paypal' && (<InputFieldform={form}name='paypalEmail'label='PayPal Email'props={{ type: 'email', placeholder: 'your@email.com' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'Email is required';if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';return undefined;},},}}/>)}{paymentMethod === 'bank' && (<InputFieldform={form}name='accountNumber'label='Account Number'props={{ placeholder: 'Enter your account number' }}formProps={{validators: {onChange: ({ value }) => {if (!value) return 'Account number is required';if (value.length < 8) return 'Account number must be at least 8 digits';return undefined;},},}}/>)}<SubmitButton form={form}>Process Payment</SubmitButton></form>);}
Props
Prop
Type