Radio
Radio buttons are essential form controls that present users with a clear set of options where only one selection is possible at a time.
import {Radio, RadioGroup} from "@qualcomm-ui/react/radio"Examples
Simple
Basic radio with label using the simple API.
<RadioGroup.Root defaultValue="html">
<RadioGroup.Label>Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="HTML" value="html" />
<Radio label="CSS" value="css" />
<Radio label="TypeScript" value="ts" />
</RadioGroup.Items>
</RadioGroup.Root>
Composite
Build with the composite API for granular control. This API requires you to provide each subcomponent, but gives you full control over the structure and layout.
<RadioGroup.Root defaultValue="html" name="language">
<RadioGroup.Label>Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio.Root value="html">
<Radio.Control />
<Radio.HiddenInput />
<Radio.Label>HTML</Radio.Label>
</Radio.Root>
<Radio.Root value="css">
<Radio.Control />
<Radio.HiddenInput />
<Radio.Label>CSS</Radio.Label>
</Radio.Root>
<Radio.Root value="ts">
<Radio.Control />
<Radio.HiddenInput />
<Radio.Label>TypeScript</Radio.Label>
</Radio.Root>
</RadioGroup.Items>
</RadioGroup.Root>
Layout
Use the composite API when you need to modify the layout of the radio buttons.
<RadioGroup.Root defaultValue="html" name="language">
<RadioGroup.Label>Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio.Root value="html">
<Radio.HiddenInput />
<Radio.Label>HTML</Radio.Label>
<Radio.Control />
</Radio.Root>
<Radio.Root value="css">
<Radio.HiddenInput />
<Radio.Label>CSS</Radio.Label>
<Radio.Control />
</Radio.Root>
<Radio.Root value="ts">
<Radio.HiddenInput />
<Radio.Label>TypeScript</Radio.Label>
<Radio.Control />
</Radio.Root>
</RadioGroup.Items>
</RadioGroup.Root>
Disabled
When disabled is true, the radio group becomes non-interactive and is typically rendered with reduced opacity to indicate its unavailable state.
<RadioGroup.Root defaultValue="html" disabled name="language">
<RadioGroup.Items>
<Radio label="HTML" value="html" />
<Radio label="CSS" value="css" />
<Radio label="TypeScript" value="ts" />
</RadioGroup.Items>
</RadioGroup.Root>
Controlled State
Set the initial value using the defaultValue prop, or use value and onValueChange to control the value manually. These props follow our controlled state pattern.
import {type ReactElement, useState} from "react"
import {Radio, RadioGroup} from "@qualcomm-ui/react/radio"
export function RadioControlledDemo(): ReactElement {
const [value, setValue] = useState<string | null>("html")
return (
<form>
<RadioGroup.Root
name="language"
onValueChange={(nextValue) => {
console.log("radio value change:", nextValue)
setValue(nextValue)
}}
value={value}
>
<RadioGroup.Items>
<Radio label="HTML" value="html" />
<Radio label="CSS" value="css" />
<Radio label="TypeScript" value="ts" />
</RadioGroup.Items>
</RadioGroup.Root>
</form>
)
}Orientation
Controls the layout direction of radio buttons within the group.
<RadioGroup.Root
defaultValue="html"
name="language"
orientation="horizontal"
>
<RadioGroup.Label>Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="HTML" value="html" />
<Radio label="CSS" value="css" />
<Radio label="TypeScript" value="ts" />
</RadioGroup.Items>
</RadioGroup.Root>
Sizes
The radio supports three sizes: sm, md (default), and lg.
<RadioGroup.Root defaultValue="html">
<RadioGroup.Items>
<Radio label="small (sm)" size="sm" value="sm" />
<Radio label="medium (md)" size="md" value="md" />
<Radio label="large (lg)" size="lg" value="lg" />
</RadioGroup.Items>
</RadioGroup.Root>
Indented
Use the indented prop to indent the radio items.
<RadioGroup.Root defaultValue="html" indented name="language">
<RadioGroup.Label>Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="HTML" value="html" />
<Radio label="CSS" value="css" />
<Radio label="TypeScript" value="ts" />
</RadioGroup.Items>
</RadioGroup.Root>
Hint
Add a hint to provide additional context for the radio group.
<RadioGroup.Root defaultValue="html">
<RadioGroup.Label>Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="HTML" value="html" />
<Radio label="CSS" value="css" />
<Radio label="TypeScript" value="ts" />
</RadioGroup.Items>
<RadioGroup.Hint>You can change this later</RadioGroup.Hint>
</RadioGroup.Root>
Forms
Choose the form library that fits your needs—we've built examples with React Hook Form and Tanstack Form to get you started.
React Hook Form
Use React Hook Form to handle the radio state and validation. ArkType works great for schema validation if you need it.
import type {ReactElement} from "react"
import {arktypeResolver} from "@hookform/resolvers/arktype"
import {type} from "arktype"
import {Controller, useForm} from "react-hook-form"
import {Button} from "@qualcomm-ui/react/button"
import {Radio, RadioGroup} from "@qualcomm-ui/react/radio"
interface FormData {
framework: string
language: string
}
const FormSchema = type({
framework: "string>0",
language: "string>0",
})
export function RadioReactHookFormDemo(): ReactElement {
const {control, handleSubmit} = useForm<FormData>({
defaultValues: {
framework: "",
language: "",
},
resolver: arktypeResolver(FormSchema),
})
return (
<form
className="flex w-56 flex-col gap-4"
onSubmit={(e) => {
void handleSubmit((data) => console.log(data))(e)
}}
>
<Controller
control={control}
name="language"
render={({
field: {name, onChange, value, ...fieldProps},
fieldState: {error},
}) => {
return (
<RadioGroup.Root
invalid={!!error}
name={name}
onValueChange={onChange}
value={value}
{...fieldProps}
>
<RadioGroup.Label>Programming Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="JavaScript" value="javascript" />
<Radio label="TypeScript" value="typescript" />
<Radio label="Python" value="python" />
</RadioGroup.Items>
<RadioGroup.ErrorText>{error?.message}</RadioGroup.ErrorText>
</RadioGroup.Root>
)
}}
/>
<Controller
control={control}
name="framework"
render={({
field: {name, onChange, value, ...fieldProps},
fieldState: {error},
}) => {
return (
<RadioGroup.Root
invalid={!!error}
name={name}
onValueChange={onChange}
value={value}
{...fieldProps}
>
<RadioGroup.Label>Framework</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="React" value="react" />
<Radio label="Vue" value="vue" />
<Radio label="Angular" value="angular" />
</RadioGroup.Items>
<RadioGroup.ErrorText>{error?.message}</RadioGroup.ErrorText>
</RadioGroup.Root>
)
}}
/>
<Button
className="mt-1"
emphasis="primary"
size="sm"
type="submit"
variant="fill"
>
Submit
</Button>
</form>
)
}Tanstack Form
Tanstack Form handles radio validation with its built-in validators.
import type {ReactElement} from "react"
import {useForm} from "@tanstack/react-form"
import {Button} from "@qualcomm-ui/react/button"
import {Radio, RadioGroup} from "@qualcomm-ui/react/radio"
interface FormData {
framework: string | null
language: string | null
}
const defaultFormData: FormData = {
framework: "",
language: "",
}
const requiredMessage = "Please select an option"
export function RadioTanstackFormDemo(): ReactElement {
const form = useForm({
defaultValues: defaultFormData,
onSubmit: ({value}) => {
console.log(value)
},
})
return (
<form
className="flex w-56 flex-col gap-4"
onSubmit={(event) => {
event.preventDefault()
event.stopPropagation()
void form.handleSubmit()
}}
>
<form.Field
name="language"
validators={{
onChange: (field) => (field.value ? undefined : requiredMessage),
onSubmit: (field) => (field.value ? undefined : requiredMessage),
}}
>
{({handleChange, name, state}) => {
const fieldError =
state.meta.errorMap["onChange"] || state.meta.errorMap["onSubmit"]
return (
<RadioGroup.Root
invalid={!!fieldError}
name={name}
onValueChange={handleChange}
value={state.value}
>
<RadioGroup.Label>Programming Language</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="JavaScript" value="javascript" />
<Radio label="TypeScript" value="typescript" />
<Radio label="Python" value="python" />
</RadioGroup.Items>
<RadioGroup.ErrorText>{fieldError}</RadioGroup.ErrorText>
</RadioGroup.Root>
)
}}
</form.Field>
<form.Field
name="framework"
validators={{
onChange: (field) => (field.value ? undefined : requiredMessage),
onSubmit: (field) => (field.value ? undefined : requiredMessage),
}}
>
{({handleChange, name, state}) => {
const fieldError =
state.meta.errorMap["onChange"] || state.meta.errorMap["onSubmit"]
return (
<RadioGroup.Root
invalid={!!fieldError}
name={name}
onValueChange={handleChange}
value={state.value}
>
<RadioGroup.Label>Framework</RadioGroup.Label>
<RadioGroup.Items>
<Radio label="React" value="react" />
<Radio label="Vue" value="vue" />
<Radio label="Angular" value="angular" />
</RadioGroup.Items>
<RadioGroup.ErrorText>{fieldError}</RadioGroup.ErrorText>
</RadioGroup.Root>
)
}}
</form.Field>
<Button
className="mt-1"
emphasis="primary"
size="sm"
type="submit"
variant="fill"
>
Submit
</Button>
</form>
)
}Tanstack form also supports ArkType.
Guidelines
RadioGroup
The Radio is only intended to be used as direct descendants of the <RadioGroup.Root> component.
/* Won't work alone ❌ */
<Radio label="Label" value="value" />
/* Works as expected ✅ */
<RadioGroup.Root>
<Radio label="Label" value="value" />
</RadioGroup.Root>Composite Components
The Radio's composite components are only intended to be used as direct descendants of the <Radio.Root> component.
/* Won't work alone ❌ */
<Radio.HiddenInput />
<Radio.Control />
/* Works as expected ✅ */
<Radio.Root>
<Radio.HiddenInput />
<Radio.Control />
</Radio.Root>Explorer
Component Anatomy
Hover to highlight, click to view API
API
<RadioGroup.Root>
string'ltr' | 'rtl'
booleanstring() =>
| Node
| ShadowRoot
| Document
booleanbooleanstring(
value: string,
) => void
| 'horizontal'
| 'vertical'
boolean| ReactElement
| ((
props: object,
) => ReactElement)
boolean| 'sm'
| 'md'
| 'lg'
stringclassName'qui-radio-group__root'data-disableddata-orientation| 'horizontal'
| 'vertical'
data-radio-part'group'<RadioGroup.Label>
string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-radio-group__label'data-disableddata-radio-part'label'data-size| 'sm'
| 'md'
| 'lg'
<RadioGroup.Items>
| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-radio-group__items'data-indenteddata-orientation| 'horizontal'
| 'vertical'
data-radio-part'items'data-size| 'sm'
| 'md'
| 'lg'
<RadioGroup.Hint>
| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-input__hint'data-disableddata-radio-part'hint'hiddenboolean<RadioGroup.ErrorText>
| LucideIcon
| ReactNode
| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-input__error-text'data-radio-part'error-text'hiddenboolean<Radio>
The Radio component supports all props from Radio.Root, plus the additional props listed below.
stringstring<Radio.Root>
stringbooleanstringboolean| ReactElement
| ((
props: object,
) => ReactElement)
| 'sm'
| 'md'
| 'lg'
className'qui-radio__root'data-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-radio-part'item'data-readonlydata-state| 'checked'
| 'unchecked'
<Radio.Label>
string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-radio__label'data-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-radio-part'item-label'data-readonlydata-size| 'sm'
| 'md'
| 'lg'
data-state| 'checked'
| 'unchecked'
<Radio.Control>
| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-radio__control'data-activedata-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-radio-part'item-control'data-readonlydata-size| 'sm'
| 'md'
| 'lg'
data-state| 'checked'
| 'unchecked'
<Radio.HiddenInput>
className'qui-radio__hidden-input'data-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-ownedbystringdata-radio-part'item-hidden-input'data-readonlydata-state| 'checked'
| 'unchecked'
style<Radio.Hint>
string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-input__hint'data-disableddata-focusdata-focus-visibledata-hoverdata-invaliddata-radio-part'item-hint'data-readonlydata-state| 'checked'
| 'unchecked'
hiddenboolean
<div>element by default.