Radio Group
React formSingle-selection group of radio inputs. Vertical or horizontal layout, optional descriptions per item, and full keyboard navigation. Pair with Form for validation messages.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/radio-group.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/radio-group.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/radio-group.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/radio-group.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/radio-group
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
value The controlled value of the radio items to check. | string | — | optional |
defaultValue The value of the radio items that should be checked when initially rendered. | string | — | optional |
disabled When `true`, prevents the user from interacting with the radio group | boolean | false | optional |
orientation The orientation of the radio items | 'horizontal' | 'vertical' | vertical | optional |
loop When `true`, keyboard navigation will loop from last item to first, and vice versa | boolean | true | optional |
label Label for the radio group | string | — | optional |
hint Hint text for the radio group | string | — | optional |
errorMessages Error messages to display | string | string[] | — | optional |
error Whether to show error state | boolean | — | optional |
density Density of the radio items | 'compact' | 'default' | 'comfortable' | default | optional |
flat Whether the radio group appears flat | boolean | false | optional |
bordered Whether to show a border around the group | boolean | false | optional |
dir The reading direction | 'ltr' | 'rtl' | — | optional |
options Array of options to render automatically | RadioOption[] | — | optional |
size Size of button-style radios | 'small' | 'middle' | 'large' | middle | optional |
optionType Type of options to render | 'default' | 'button' | default | optional |
buttonVariant Visual variant for button-style radios | 'outline' | 'solid' | outline | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
RadioGroupContextValue type RadioGroupContextValue {
disabled?: boolean
size?: 'small' | 'middle' | 'large'
optionType?: 'default' | 'button'
buttonVariant?: 'outline' | 'solid'
orientation?: 'horizontal' | 'vertical'
} Dependencies
Files (2)
-
components/ui/radio-group/radio-group.tsx 13.9 kB
'use client' import * as React from 'react' import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' import { Circle } from 'lucide-react' import { cn } from '@/lib/utils' export type RadioOption = string | { label: string; value: string; disabled?: boolean } // Make group-level config (disabled / size / optionType / buttonVariant / // orientation) reachable from items without manual prop drilling — the React // equivalent of the Vue provide('radioGroup'). type RadioGroupContextValue = { disabled?: boolean size?: 'small' | 'middle' | 'large' optionType?: 'default' | 'button' buttonVariant?: 'outline' | 'solid' orientation?: 'horizontal' | 'vertical' } const RadioGroupContext = React.createContext<RadioGroupContextValue>({}) function normalizeOption(option: RadioOption): { label: string; value: string; disabled?: boolean } { if (typeof option === 'string') { return { label: option, value: option } } return option } export interface RadioGroupProps extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>, 'defaultValue' | 'dir'> { /** The controlled value of the radio items to check. */ value?: string /** The value of the radio items that should be checked when initially rendered. */ defaultValue?: string /** When `true`, prevents the user from interacting with the radio group */ disabled?: boolean /** The orientation of the radio items */ orientation?: 'horizontal' | 'vertical' /** When `true`, keyboard navigation will loop from last item to first, and vice versa */ loop?: boolean /** Label for the radio group */ label?: string /** Hint text for the radio group */ hint?: string /** Error messages to display */ errorMessages?: string | string[] /** Whether to show error state */ error?: boolean /** Density of the radio items */ density?: 'compact' | 'default' | 'comfortable' /** Whether the radio group appears flat */ flat?: boolean /** Whether to show a border around the group */ bordered?: boolean /** The reading direction */ dir?: 'ltr' | 'rtl' /** Array of options to render automatically */ options?: RadioOption[] /** Size of button-style radios */ size?: 'small' | 'middle' | 'large' /** Type of options to render */ optionType?: 'default' | 'button' /** Visual variant for button-style radios */ buttonVariant?: 'outline' | 'solid' } // Density classes const groupDensityClasses = { compact: 'gap-1', default: 'gap-3', comfortable: 'gap-4', } const RadioGroup = React.forwardRef<React.ElementRef<typeof RadioGroupPrimitive.Root>, RadioGroupProps>( ( { className, value, defaultValue, disabled = false, orientation = 'vertical', loop = true, label, hint, errorMessages, error, density = 'default', flat = false, bordered = false, dir, options, size = 'middle', optionType = 'default', buttonVariant = 'outline', children, ...props }, ref, ) => { const hasError = Boolean(error) || Boolean(errorMessages && (typeof errorMessages === 'string' ? errorMessages : errorMessages.length > 0)) return ( <RadioGroupContext.Provider value={{ disabled, size, optionType, buttonVariant, orientation }}> {/* `className` is forwarded ONLY to RadioGroupRoot below (per shadcn convention). Applying it on the outer wrapper as well caused grid-* utilities to fight the wrapper's `flex flex-col`, so consumers had to fall back to column-count hacks. Forwarding to one element keeps layout intent unambiguous. */} <div className="flex flex-col gap-2"> {label && <label className="text-sm font-medium">{label}</label>} {hint && !hasError && <p className="text-muted-foreground text-xs">{hint}</p>} <RadioGroupPrimitive.Root ref={ref} data-slot="radio-group" value={value} defaultValue={defaultValue} disabled={disabled} orientation={orientation} loop={loop} dir={dir} className={cn( 'grid gap-3', orientation === 'horizontal' && 'flex flex-row items-center gap-4', optionType === 'button' && orientation === 'horizontal' && 'flex flex-row items-stretch gap-0', optionType === 'button' && orientation === 'vertical' && 'flex flex-col items-stretch gap-0', optionType !== 'button' && groupDensityClasses[density], bordered && 'rounded-lg border p-4', className, )} {...props} > {options && options.length > 0 ? optionType === 'button' ? options.map((option) => { const opt = normalizeOption(option) return <RadioButton key={opt.value} value={opt.value} disabled={opt.disabled} label={opt.label} /> }) : options.map((option) => { const opt = normalizeOption(option) return ( <div key={opt.value} className="flex items-center gap-2"> <RadioGroupItem id={opt.value} value={opt.value} disabled={opt.disabled} /> <label htmlFor={opt.value} className={cn( 'cursor-pointer text-sm font-medium select-none', opt.disabled && 'cursor-not-allowed opacity-50', )} > {opt.label} </label> </div> ) }) : children} </RadioGroupPrimitive.Root> {hasError && ( <div className="flex flex-col gap-0.5"> {typeof errorMessages === 'string' ? ( <p className="text-destructive text-xs">{errorMessages}</p> ) : ( errorMessages?.map((msg, i) => ( <p key={i} className="text-destructive text-xs"> {msg} </p> )) )} </div> )} </div> </RadioGroupContext.Provider> ) }, ) RadioGroup.displayName = 'RadioGroup' export interface RadioGroupItemProps extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>, 'children'> { /** Size of the radio item */ size?: 'sm' | 'md' | 'lg' /** Custom color for the checked state */ color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | string /** Label text displayed next to the radio item */ label?: string /** Hint text shown below the radio item */ hint?: string /** Error message to display */ errorMessages?: string | string[] /** Whether to show error state */ error?: boolean /** Density of the radio item */ density?: 'compact' | 'default' | 'comfortable' /** Label position - before or after the radio */ labelPosition?: 'before' | 'after' /** Loading state */ loading?: boolean /** Hide the indicator icon */ hideIcon?: boolean children?: React.ReactNode } // Size classes const sizeClasses = { sm: 'size-3.5', md: 'size-4', lg: 'size-5', } const indicatorSizes = { sm: 'size-1.5', md: 'size-2', lg: 'size-2.5', } // Color classes const colorClasses: Record<string, string> = { primary: 'data-[state=checked]:border-primary', secondary: 'data-[state=checked]:border-secondary', success: 'data-[state=checked]:border-[var(--success)] data-[state=checked]:text-[var(--success)]', warning: 'data-[state=checked]:border-[var(--warning)] data-[state=checked]:text-[var(--warning)]', error: 'data-[state=checked]:border-destructive data-[state=checked]:text-destructive', info: 'data-[state=checked]:border-[var(--info)] data-[state=checked]:text-[var(--info)]', } // Density classes const itemDensityClasses = { compact: 'gap-1', default: 'gap-2', comfortable: 'gap-3', } const RadioGroupItem = React.forwardRef<React.ElementRef<typeof RadioGroupPrimitive.Item>, RadioGroupItemProps>( ( { className, id, value, disabled, size = 'md', color = 'primary', label, hint, errorMessages, error, density = 'default', labelPosition = 'after', loading, hideIcon = false, children, ...props }, ref, ) => { const groupContext = React.useContext(RadioGroupContext) const effectiveDisabled = disabled ?? groupContext.disabled ?? false const hasError = Boolean(error) || Boolean(errorMessages && (typeof errorMessages === 'string' ? errorMessages : errorMessages.length > 0)) const labelClasses = cn( 'cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', hasError ? 'text-destructive' : '', effectiveDisabled && 'cursor-not-allowed opacity-50', ) return ( <div className={cn('flex items-start', itemDensityClasses[density])}> {label && labelPosition === 'before' && ( <label htmlFor={id} className={cn('mr-2', labelClasses)}> {label} </label> )} <div className="flex items-center"> <RadioGroupPrimitive.Item ref={ref} id={id} data-uipkge="" data-slot="radio-group-item" value={value!} disabled={effectiveDisabled} className={cn( 'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square shrink-0 rounded-full border shadow-sm transition-colors duration-200 outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', sizeClasses[size], colorClasses[color] || colorClasses.primary, hasError && 'border-destructive', className, )} {...props} > <RadioGroupPrimitive.Indicator data-uipkge="" data-slot="radio-group-indicator" className="relative flex items-center justify-center" > {children ?? (!hideIcon ? ( <Circle className={cn( 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 fill-current text-current', indicatorSizes[size], )} /> ) : null)} </RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Item> {label && labelPosition === 'after' && ( <label htmlFor={id} className={cn('ml-2', labelClasses)}> {label} </label> )} </div> {hint && !hasError && <p className="text-muted-foreground mt-1 ml-6 text-xs">{hint}</p>} {hasError && ( <div className="mt-1 ml-6 flex flex-col gap-0.5"> {typeof errorMessages === 'string' ? ( <p className="text-destructive text-xs">{errorMessages}</p> ) : ( errorMessages?.map((msg, i) => ( <p key={i} className="text-destructive text-xs"> {msg} </p> )) )} </div> )} </div> ) }, ) RadioGroupItem.displayName = 'RadioGroupItem' export interface RadioButtonProps extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>, 'children'> { /** Size of the button radio */ size?: 'small' | 'middle' | 'large' /** Visual variant */ variant?: 'outline' | 'solid' /** Label text */ label?: string children?: React.ReactNode } const buttonSizeClasses = { small: 'h-7 px-2.5 text-xs', middle: 'h-8 px-4 text-sm', large: 'h-10 px-4.5 text-base', } const buttonVariantClasses = { outline: cn( 'border border-input bg-transparent text-foreground hover:text-foreground hover:bg-muted/50', 'data-[state=checked]:border-primary data-[state=checked]:text-primary', 'disabled:hover:bg-transparent', ), solid: cn( 'border border-input bg-transparent text-foreground hover:text-foreground hover:bg-muted/50', 'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', 'disabled:hover:bg-transparent', ), } const RadioButton = React.forwardRef<React.ElementRef<typeof RadioGroupPrimitive.Item>, RadioButtonProps>( ({ className, value, disabled, size, variant, label, children, ...props }, ref) => { const context = React.useContext(RadioGroupContext) const effectiveSize = size ?? context.size ?? 'middle' const effectiveVariant = variant ?? context.buttonVariant ?? 'outline' const effectiveDisabled = disabled ?? context.disabled ?? false const groupClasses = context.orientation === 'vertical' ? 'rounded-md w-full justify-start' : cn('rounded-none first:rounded-l-md last:rounded-r-md', 'border-l-0 first:border-l', '-ml-px first:ml-0') return ( <RadioGroupPrimitive.Item ref={ref} data-uipkge="" data-slot="radio-button" value={value!} disabled={effectiveDisabled} className={cn( 'inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-colors duration-200', 'focus-visible:border-ring focus-visible:ring-ring/50 outline-none focus-visible:ring-[3px]', 'disabled:cursor-not-allowed disabled:opacity-50', buttonSizeClasses[effectiveSize], buttonVariantClasses[effectiveVariant], groupClasses, className, )} {...props} > {children ?? (label ?? value)} </RadioGroupPrimitive.Item> ) }, ) RadioButton.displayName = 'RadioButton' export { RadioGroup, RadioGroupItem, RadioButton } -
components/ui/radio-group/index.ts 0.2 kB
export { RadioGroup, RadioGroupItem, RadioButton, type RadioGroupProps, type RadioGroupItemProps, type RadioButtonProps, type RadioOption, } from './radio-group'
Raw manifest: https://react.uipkge.dev/r/react/radio-group.json