Radio Field
Radio button group component integrated with React Hook Form for single-choice selections with automatic option rendering.
npx shadcn@latest add https://shuip.plvo.dev/r/rhf-radio-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-radio-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-radio-field.json
import type { FieldPath, FieldValues, UseFormRegisterReturn } from 'react-hook-form';import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';export interface RadioFieldProps<T extends FieldValues> extends React.ComponentProps<typeof RadioGroup> {register: UseFormRegisterReturn<FieldPath<T>>;options: string[];label?: string;description?: string;}export function RadioField<T extends FieldValues>({register,options,label,description,...props}: RadioFieldProps<T>) {return (<FormField{...register}render={({ field, fieldState }) => (<FormItem className='space-y-1.5' data-invalid={fieldState.invalid}>{label && <FormLabel>{label}</FormLabel>}<FormControl><RadioGrouponValueChange={field.onChange}defaultValue={field.value}className='flex flex-col space-y-1'aria-invalid={fieldState.invalid}{...props}>{options.map((value) => (<FormItem key={value} className='flex items-center space-x-3 space-y-0'><FormControl><RadioGroupItem value={value} id={`${field.name}-${value}`} aria-invalid={fieldState.invalid} /></FormControl><FormLabel htmlFor={`${field.name}-${value}`} className='font-normal'>{value}</FormLabel></FormItem>))}</RadioGroup></FormControl><FormMessage className='text-xs text-left' />{description && <FormDescription className='text-xs'>{description}</FormDescription>}</FormItem>)}/>);}
Loading...
Radio buttons are used for single-choice selections from a predefined set of options. RadioField automatically renders radio button options from an array, handling all the form integration and layout concerns.
The component eliminates the need to manually create individual radio items and their labels. It provides consistent spacing, proper accessibility attributes, and automatic form state management for radio groups.
Built-in features
- Automatic option rendering: Pass an array of strings and get fully functional radio buttons
- Consistent layout: Pre-configured spacing and alignment for radio groups
- Accessibility support: Proper ARIA attributes and keyboard navigation
- Zod validation: Native integration with react-hook-form and Zod schemas
- Error display: Automatic validation error messages
Less boilerplate
<FormField
control={form.control}
name="theme"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Select a theme</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="light" />
</FormControl>
<FormLabel className="font-normal">Light</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="dark" />
</FormControl>
<FormLabel className="font-normal">Dark</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="system" />
</FormControl>
<FormLabel className="font-normal">System</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>With RadioField, this reduces to a single declarative component:
<RadioField
register={form.register('theme')}
label="Select a theme"
options={['light', 'dark', 'system']}
/>Examples
Conditional Pricing
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Card } from '@/components/ui/card';import { Form } from '@/components/ui/form';import { RadioField } from '@/components/ui/shuip/react-hook-form/radio-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const PLANS = ['free', 'pro', 'enterprise'];const PLAN_PRICES = { free: 0, pro: 29, enterprise: 99 };const zodSchema = z.object({plan: z.enum(PLANS, {error: 'Please select a plan',}),billingCycle: z.enum(['monthly', 'annual']).default('monthly'),});export default function RhfRadioFieldConditionalPricingExample() {const form = useForm({defaultValues: {plan: '',billingCycle: 'monthly',},resolver: zodResolver(zodSchema),});const plan = form.watch('plan') as keyof typeof PLAN_PRICES;const billingCycle = form.watch('billingCycle');const monthlyPrice = PLAN_PRICES[plan] || 0;const annualPrice = monthlyPrice * 12 * 0.8; // 20% discountconst displayPrice = billingCycle === 'annual' ? annualPrice / 12 : monthlyPrice;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'><RadioFieldregister={form.register('plan')}options={PLANS}label='Select Plan'description='Choose the plan that fits your needs'/>{plan && plan !== 'free' && (<RadioFieldregister={form.register('billingCycle')}options={['monthly', 'annual']}label='Billing Cycle'description='Annual billing includes a 20% discount'/>)}{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>Continue</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 { RadioField } from '@/components/ui/shuip/react-hook-form/radio-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({selection: z.enum(['Yes', 'No', 'Maybe', 'Not sure']),});export default function RadioFieldExample() {const form = useForm({defaultValues: {selection: 'Yes' as const,},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'><RadioFieldregister={form.register('selection')}options={['Yes', 'No', 'Maybe', 'Not sure']}label='Are you sure?'description='This is a description'/><SubmitButton>Check</SubmitButton></form></Form>);}
Payment Method
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 { RadioField } from '@/components/ui/shuip/react-hook-form/radio-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({paymentMethod: z.enum(['card', 'paypal', 'bank'], {error: 'Please select a payment method',}),cardNumber: z.string().optional(),paypalEmail: z.string().optional(),accountNumber: z.string().optional(),});export default function RhfRadioFieldPaymentMethodExample() {const form = useForm({defaultValues: {paymentMethod: undefined,cardNumber: '',paypalEmail: '',accountNumber: '',},resolver: zodResolver(zodSchema),});const paymentMethod = form.watch('paymentMethod');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'><RadioFieldregister={form.register('paymentMethod')}options={['card', 'paypal', 'bank']}label='Payment Method'description='Select how you want to pay'/>{paymentMethod === 'card' && (<InputFieldregister={form.register('cardNumber')}label='Card Number'placeholder='1234 5678 9012 3456'description='Enter your credit card number'/>)}{paymentMethod === 'paypal' && (<InputFieldregister={form.register('paypalEmail')}type='email'label='PayPal Email'placeholder='your@email.com'description='Email associated with your PayPal account'/>)}{paymentMethod === 'bank' && (<InputFieldregister={form.register('accountNumber')}label='Account Number'placeholder='Enter your account number'description='Your bank account number for direct transfer'/>)}<SubmitButton>Process Payment</SubmitButton></form></Form>);}
Props
Prop
Type