Radio Group
A set of checkable buttons where only one can be checked at a time.
The Radio Group component provides a set of radio buttons where only one option can be selected at a time. It supports controlled and uncontrolled modes, keyboard navigation, and accessibility features. Built from scratch using React and native HTML elements. No dependencies on any UI library.
Code
TypeScript: Copy this code into components/ui/radio-group.tsx:
tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface RadioGroupContextValue {
value?: string
onValueChange?: (value: string) => void
name?: string
}
const RadioGroupContext = React.createContext<RadioGroupContextValue | undefined>(undefined)
const useRadioGroup = () => {
const context = React.useContext(RadioGroupContext)
return context
}
interface RadioGroupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
disabled?: boolean
name?: string
}
const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
({ className, value: controlledValue, defaultValue, onValueChange, disabled, name, ...props }, ref) => {
const [uncontrolledValue, setUncontrolledValue] = React.useState<string | undefined>(defaultValue)
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : uncontrolledValue
const handleValueChange = React.useCallback((newValue: string) => {
if (disabled) return
if (!isControlled) {
setUncontrolledValue(newValue)
}
onValueChange?.(newValue)
}, [disabled, isControlled, onValueChange])
const groupName = name || React.useId()
return (
<RadioGroupContext.Provider value={{ value, onValueChange: handleValueChange, name: groupName }}>
<div
ref={ref}
className={cn("space-y-2", className)}
role="radiogroup"
{...props}
/>
</RadioGroupContext.Provider>
)
}
)
RadioGroup.displayName = "RadioGroup"
interface RadioGroupItemProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
value: string
}
const RadioGroupItem = React.forwardRef<HTMLInputElement, RadioGroupItemProps>(
({ className, value, disabled, ...props }, ref) => {
const context = useRadioGroup()
const isChecked = context?.value === value
const isDisabled = disabled || false
const handleChange = () => {
if (!isDisabled) {
context?.onValueChange?.(value)
}
}
return (
<div className="flex items-center space-x-2">
<input
ref={ref}
type="radio"
value={value}
checked={isChecked}
onChange={handleChange}
disabled={isDisabled}
name={context?.name}
className="sr-only"
{...props}
/>
<button
type="button"
role="radio"
aria-checked={isChecked}
disabled={isDisabled}
onClick={handleChange}
className={cn(
"relative flex h-5 w-5 items-center justify-center rounded-full border-2 border-foreground transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isDisabled && "opacity-50 cursor-not-allowed",
!isDisabled && "cursor-pointer hover:bg-muted",
className
)}
>
{isChecked && (
<div className="h-2.5 w-2.5 rounded-full bg-foreground" />
)}
</button>
</div>
)
}
)
RadioGroupItem.displayName = "RadioGroupItem"
export {
RadioGroup,
RadioGroupItem,
}JavaScript: Copy this code into components/ui/radio-group.jsx:
jsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const RadioGroupContext = React.createContext(undefined)
const useRadioGroup = () => {
const context = React.useContext(RadioGroupContext)
return context
}
const RadioGroup = React.forwardRef(
({ className, value: controlledValue, defaultValue, onValueChange, disabled, name, ...props }, ref) => {
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : uncontrolledValue
const handleValueChange = React.useCallback((newValue) => {
if (disabled) return
if (!isControlled) {
setUncontrolledValue(newValue)
}
onValueChange?.(newValue)
}, [disabled, isControlled, onValueChange])
const groupName = name || React.useId()
return (
<RadioGroupContext.Provider value={{ value, onValueChange: handleValueChange, name: groupName }}>
<div
ref={ref}
className={cn("space-y-2", className)}
role="radiogroup"
{...props}
/>
</RadioGroupContext.Provider>
)
}
)
RadioGroup.displayName = "RadioGroup"
const RadioGroupItem = React.forwardRef(
({ className, value, disabled, ...props }, ref) => {
const context = useRadioGroup()
const isChecked = context?.value === value
const isDisabled = disabled || false
const handleChange = () => {
if (!isDisabled) {
context?.onValueChange?.(value)
}
}
return (
<div className="flex items-center space-x-2">
<input
ref={ref}
type="radio"
value={value}
checked={isChecked}
onChange={handleChange}
disabled={isDisabled}
name={context?.name}
className="sr-only"
{...props}
/>
<button
type="button"
role="radio"
aria-checked={isChecked}
disabled={isDisabled}
onClick={handleChange}
className={cn(
"relative flex h-5 w-5 items-center justify-center rounded-full border-2 border-foreground transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isDisabled && "opacity-50 cursor-not-allowed",
!isDisabled && "cursor-pointer hover:bg-muted",
className
)}
>
{isChecked && (
<div className="h-2.5 w-2.5 rounded-full bg-foreground" />
)}
</button>
</div>
)
}
)
RadioGroupItem.displayName = "RadioGroupItem"
export {
RadioGroup,
RadioGroupItem,
}Usage
TypeScript:
tsx
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import * as React from "react"
function MyComponent() {
const [value, setValue] = React.useState("default")
return (
<RadioGroup value={value} onValueChange={setValue}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="default" id="default" />
<Label htmlFor="default">Default</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="comfortable" id="comfortable" />
<Label htmlFor="comfortable">Comfortable</Label>
</div>
</RadioGroup>
)
}JavaScript:
jsx
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import * as React from "react"
function MyComponent() {
const [value, setValue] = React.useState("default")
return (
<RadioGroup value={value} onValueChange={setValue}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="default" id="default" />
<Label htmlFor="default">Default</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="comfortable" id="comfortable" />
<Label htmlFor="comfortable">Comfortable</Label>
</div>
</RadioGroup>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.