Radio

A control element that allows the user to choose only one of a predefined set of mutually exclusive options. The singular property of a radio button makes it distinct from checkboxes, where the user can select and unselect any number of items.

import {Radio, RadioGroup} from "@qualcomm-ui/react/radio"

Examples

Simple

Basic radio with label using the simple API.

Language
<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.

Language
<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.

Language
<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.

Disabled
<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 default 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.

Language
<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>

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.

Programming Language
Framework
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 default 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.

Programming Language
Framework
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 default function RadioTanstackFormDemo(): ReactElement {
  const form = useForm({
    defaultValues: defaultFormData,
    onSubmit: async ({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>

API

<RadioGroup.Root>

PropTypeDefault
React children prop.
The initial value of the checked radio when rendered. Use when you don't need to control the value of the radio group.
string
The document's text/writing direction.
'ltr' | 'rtl'
'ltr'
Whether the input is disabled. When true, prevents user interaction and applies visual styling to indicate the disabled state.
boolean
The id of the form that the input belongs to.
string
A root node to correctly resolve document in custom environments. i.e., Iframes, Electron.
    () =>
    | Node
    | ShadowRoot
    | Document
    Controls the visual error state of the input. When true, applies semantic error styling to indicate validation failure.
    boolean
    The name of the input field. Useful for form submission.
    string
    Function called once a radio is checked
      (
      value: string,
      ) => void
      Orientation of the radio group
      | 'horizontal'
      | 'vertical'
      'vertical'
      
      Whether the input is read-only. When true, prevents user interaction while keeping the input focusable and visible.
      boolean
      Allows you to replace the component's HTML element with a different tag or component. Learn more
      | ReactElement
      | ((
      props: object,
      ) => ReactElement)
      Whether the input is required. When true, the input must have a value for form validation to pass.
      boolean
      The size of the radio and its elements. Governs properties like label font size, control size, and indicator size.
      | 'sm'
      | 'md'
      | 'lg'
      'md'
      
      The controlled value of the radio group
      string
      Description
      React children prop.
      Type
      string
      Description
      The initial value of the checked radio when rendered. Use when you don't need to control the value of the radio group.
      Type
      'ltr' | 'rtl'
      Description
      The document's text/writing direction.
      Type
      boolean
      Description
      Whether the input is disabled. When true, prevents user interaction and applies visual styling to indicate the disabled state.
      Type
      string
      Description
      The id of the form that the input belongs to.
      Type
      () =>
      | Node
      | ShadowRoot
      | Document
      Description
      A root node to correctly resolve document in custom environments. i.e., Iframes, Electron.
        Type
        boolean
        Description
        Controls the visual error state of the input. When true, applies semantic error styling to indicate validation failure.
        Type
        string
        Description
        The name of the input field. Useful for form submission.
        Type
        (
        value: string,
        ) => void
        Description
        Function called once a radio is checked
          Type
          | 'horizontal'
          | 'vertical'
          Description
          Orientation of the radio group
          Type
          boolean
          Description
          Whether the input is read-only. When true, prevents user interaction while keeping the input focusable and visible.
          Type
          | ReactElement
          | ((
          props: object,
          ) => ReactElement)
          Description
          Allows you to replace the component's HTML element with a different tag or component. Learn more
          Type
          boolean
          Description
          Whether the input is required. When true, the input must have a value for form validation to pass.
          Type
          | 'sm'
          | 'md'
          | 'lg'
          Description
          The size of the radio and its elements. Governs properties like label font size, control size, and indicator size.
          Type
          string
          Description
          The controlled value of the radio group

          <Radio.Root>

          PropTypeDefault
          string
          React children prop.
          boolean
          id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
          string
          Allows you to replace the component's HTML element with a different tag or component. Learn more
          | ReactElement
          | ((
          props: object,
          ) => ReactElement)
          The size of the radio and its elements. Governs properties like label font size, control size, and indicator size.
          | 'sm'
          | 'md'
          | 'lg'
          'md'
          
          Type
          string
          Description
          React children prop.
          Type
          boolean
          Type
          string
          Description
          id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
          Type
          | ReactElement
          | ((
          props: object,
          ) => ReactElement)
          Description
          Allows you to replace the component's HTML element with a different tag or component. Learn more
          Type
          | 'sm'
          | 'md'
          | 'lg'
          Description
          The size of the radio and its elements. Governs properties like label font size, control size, and indicator size.

          <Radio>

          The Radio component supports all props from Radio.Root, plus the additional props listed below.

          PropType
          Props applied to the control element.
          RadioControlProps
          Props applied to the hidden input element.
          RadioHiddenInputProps
          Optional label describing the element. Recommended. This element is automatically associated with the component's input element for accessibility.
          Props applied to the label element.
          RadioLabelProps
          Type
          RadioControlProps
          Description
          Props applied to the control element.
          Type
          RadioHiddenInputProps
          Description
          Props applied to the hidden input element.
          Description
          Optional label describing the element. Recommended. This element is automatically associated with the component's input element for accessibility.
          Type
          RadioLabelProps
          Description
          Props applied to the label element.