Radio
A composable radio control and group for selecting one option from a set.
API Reference
Original primitive API
Behavior, accessibility details, and low-level props are documented by Base UI.
Basic
RadioGroupItemControl renders the radio indicator automatically. Use indicator or classNames.indicator when the control needs custom visuals.
import { RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemLabel, RadioGroupLabel, RadioGroupList,} from "moduix";import { useId } from "react";export function RadioDemo() { const labelId = useId(); return ( <RadioGroup aria-labelledby={labelId} defaultValue="team"> <RadioGroupLabel id={labelId}>Account Type</RadioGroupLabel> <RadioGroupList> {options.map((option) => ( <RadioGroupItem key={option.value}> <RadioGroupItemControl value={option.value} /> <RadioGroupItemLabel>{option.label}</RadioGroupItemLabel> </RadioGroupItem> ))} </RadioGroupList> </RadioGroup> );}const options = [ { value: "personal", label: "Personal account" }, { value: "team", label: "Team account" }, { value: "enterprise", label: "Enterprise account" },];Full list of component variables available for project-level overrides.
| Property | Default | Description |
|---|---|---|
| --radio-bg | var(--color-background) | Controls unchecked background color. |
| --radio-bg-checked | var(--color-primary) | Controls checked background color. |
| --radio-bg-hover | var(--color-accent) | Controls unchecked hover background color. |
| --radio-border-color | var(--color-border) | Controls unchecked border color. |
| --radio-border-color-checked | var(--color-primary) | Controls checked border color. |
| --radio-border-width | var(--border-width-sm) | Controls radio border width. |
| --radio-disabled-opacity | var(--opacity-disabled) | Controls disabled opacity. |
| --radio-focus-ring-color | var(--color-ring) | Controls focus ring color. |
| --radio-focus-ring-offset | var(--border-width-sm) | Controls focus ring offset. |
| --radio-focus-ring-width | var(--border-width-sm) | Controls focus ring width. |
| --radio-gap | var(--spacing-2) | Controls spacing between control and label. |
| --radio-group-color | var(--color-foreground) | Controls inherited group text color. |
| --radio-group-gap | var(--spacing-2) | Controls spacing inside the group root. |
| --radio-group-list-gap | var(--spacing-2) | Controls spacing between group items. |
| --radio-group-item-gap | var(--radio-gap) | Controls spacing inside each group item. |
| --radio-group-item-label-color | var(--radio-label-color) | Controls group item label text color. |
| --radio-group-item-label-font-size | var(--radio-label-font-size) | Controls group item label font size. |
| --radio-group-item-label-font-weight | var(--radio-label-font-weight) | Controls group item label font weight. |
| --radio-group-item-label-line-height | var(--radio-label-line-height) | Controls group item label line height. |
| --radio-group-label-color | var(--radio-group-color) | Controls group label text color. |
| --radio-group-label-font-size | var(--text-sm) | Controls group label font size. |
| --radio-group-label-font-weight | var(--weight-semibold) | Controls group label font weight. |
| --radio-group-label-line-height | var(--line-height-text-sm) | Controls group label line height. |
| --radio-indicator-border-color | currentColor | Controls indicator border color. |
| --radio-indicator-border-width | 0 | Controls indicator border width. |
| --radio-indicator-color | var(--color-primary-foreground) | Controls indicator color. |
| --radio-indicator-radius | var(--radius-full) | Controls indicator border radius. |
| --radio-indicator-size-xs | 0.25rem | Controls `xs` indicator size. |
| --radio-indicator-size-sm | 0.375rem | Controls `sm` indicator size. |
| --radio-indicator-size-md | 0.5rem | Controls `md` indicator size. |
| --radio-indicator-size-lg | 0.625rem | Controls `lg` indicator size. |
| --radio-indicator-size-xl | 0.75rem | Controls `xl` indicator size. |
| --radio-label-color | var(--color-foreground) | Controls standalone label text color. |
| --radio-label-font-size | var(--text-sm) | Controls standalone label font size. |
| --radio-label-font-weight | var(--weight-medium) | Controls standalone label font weight. |
| --radio-label-line-height | var(--line-height-text-sm) | Controls standalone label line height. |
| --radio-size-xs | 0.875rem | Controls `xs` radio size. |
| --radio-size-sm | 1rem | Controls `sm` radio size. |
| --radio-size-md | 1.25rem | Controls `md` radio size. |
| --radio-size-lg | 1.5rem | Controls `lg` radio size. |
| --radio-size-xl | 1.75rem | Controls `xl` radio size. |
| --radio-transition | var(--transition-default) | Controls state transition timing. |
Interactive variables scoped for docs preview without changing size scale tokens.
| Property | Value | Default | Description |
|---|---|---|---|
| --radio-bg | var(--color-background) | Controls unchecked background color. | |
| --radio-bg-checked | var(--color-primary) | Controls checked background color. | |
| --radio-bg-hover | var(--color-accent) | Controls unchecked hover background color. | |
| --radio-border-color | var(--color-border) | Controls unchecked border color. | |
| --radio-border-color-checked | var(--color-primary) | Controls checked border color. | |
| --radio-border-width | var(--border-width-sm) | Controls radio border width. | |
| --radio-focus-ring-color | var(--color-ring) | Controls focus ring color. | |
| --radio-indicator-border-color | currentColor | Controls indicator border color. | |
| --radio-indicator-border-width | 0 | Controls indicator border width. | |
| --radio-indicator-color | var(--color-primary-foreground) | Controls indicator color. |
Anatomy
Radio is always used inside RadioGroup. The recommended group composition keeps the clickable
row, control, and label explicit while the indicator stays internal.
RadioGroup
├─ RadioGroupLabel
└─ RadioGroupList
└─ RadioGroupItem
├─ RadioGroupItemControl
│ └─ indicator (internal)
└─ RadioGroupItemLabel<RadioGroup defaultValue="team" aria-labelledby={labelId}>
<RadioGroupLabel id={labelId}>Account Type</RadioGroupLabel>
<RadioGroupList>
<RadioGroupItem>
<RadioGroupItemControl value="team" />
<RadioGroupItemLabel>Team account</RadioGroupItemLabel>
</RadioGroupItem>
</RadioGroupList>
</RadioGroup>| Part | Role |
|---|---|
RadioGroup | Root state machine. Controls selected value, disabled/read-only state, form ownership, and validation metadata. |
RadioGroupLabel | Visible group label. Pair it with aria-labelledby on RadioGroup. |
RadioGroupList | Layout wrapper for the available options. |
RadioGroupItem | Enclosing label row for one option. |
RadioGroupItemControl | The radio control. It renders the indicator automatically and accepts size, indicator, and classNames. |
RadioGroupItemLabel | Text label for the option. |
RadioField | Compact enclosing-label helper for direct Radio + RadioLabel composition. |
Radio | Standalone radio control for custom layouts, field composition, and sibling-label/native-button patterns. |
RadioLabel | Text label used with RadioField or form field labels. |
Radio does not expose portal-like service layers. Its indicator is also rendered internally by
default, so the public composition stays focused on the group, option rows, control, and labels.
Use indicator for custom graphics and classNames.indicator or classNames.indicatorIcon when
the internal indicator needs targeted styling.
Composition
Use RadioGroup for behavior props such as value, defaultValue, onValueChange, disabled,
readOnly, required, name, and form. Use RadioGroupItemControl for the common grouped
option pattern; it is a Radio with a clearer name for group rows.
For compact form rows, use RadioField, Radio, and RadioLabel. For sibling labels with
htmlFor, render Radio as a native button with nativeButton and render={<button />}.
Visible parts accept className. The radio indicator is internal, so customize it through the
indicator prop or the classNames object:
<RadioGroupItemControl
value="team"
indicator={<CustomRadioIcon />}
classNames={{
indicator: styles.indicator,
indicatorIcon: styles.indicatorIcon,
}}
/>Examples
Sizes
Use size to align the radio control with different form densities.
import { RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemLabel, RadioGroupLabel, RadioGroupList,} from "moduix";import { useId } from "react";export function RadioSizesDemo() { const labelId = useId(); return ( <RadioGroup aria-labelledby={labelId} defaultValue="md"> <RadioGroupLabel id={labelId}>Control Size</RadioGroupLabel> <RadioGroupList> {sizes.map((item) => ( <RadioGroupItem key={item.value}> <RadioGroupItemControl value={item.value} size={item.value} /> <RadioGroupItemLabel>{item.label}</RadioGroupItemLabel> </RadioGroupItem> ))} </RadioGroupList> </RadioGroup> );}const sizes = [ { value: "xs", label: "Extra-small" }, { value: "sm", label: "Small" }, { value: "md", label: "Medium" }, { value: "lg", label: "Large" }, { value: "xl", label: "Extra-large" },] as const;Controlled
Control value from React state when the selected option needs to coordinate with other UI.
import { RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemLabel, RadioGroupLabel, RadioGroupList,} from "moduix";import { useId, useState } from "react";export function ControlledRadioDemo() { const labelId = useId(); const [value, setValue] = useState("personal"); return ( <RadioGroup aria-labelledby={labelId} value={value} onValueChange={setValue}> <RadioGroupLabel id={labelId}>Workspace Visibility</RadioGroupLabel> <RadioGroupList> <RadioGroupItem> <RadioGroupItemControl value="personal" /> <RadioGroupItemLabel>Only me</RadioGroupItemLabel> </RadioGroupItem> <RadioGroupItem> <RadioGroupItemControl value="team" /> <RadioGroupItemLabel>Team</RadioGroupItemLabel> </RadioGroupItem> </RadioGroupList> </RadioGroup> );}Disabled
Use disabled on the group to prevent interaction with every radio in the group.
import { RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemLabel, RadioGroupLabel, RadioGroupList,} from "moduix";import { useId } from "react";export function DisabledRadioDemo() { const labelId = useId(); return ( <RadioGroup aria-labelledby={labelId} defaultValue="enterprise" disabled> <RadioGroupLabel id={labelId}>Plan</RadioGroupLabel> <RadioGroupList> {options.map((option) => ( <RadioGroupItem key={option.value}> <RadioGroupItemControl value={option.value} /> <RadioGroupItemLabel>{option.label}</RadioGroupItemLabel> </RadioGroupItem> ))} </RadioGroupList> </RadioGroup> );}const options = [ { value: "personal", label: "Personal account" }, { value: "team", label: "Team account" }, { value: "enterprise", label: "Enterprise account" },];Custom Indicator
Pass a custom indicator node to Radio or RadioGroupItemControl to use any icon from your application or icon library.
import { RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemLabel, RadioGroupLabel, RadioGroupList,} from "moduix";import type { ComponentProps } from "react";import { useId } from "react";function CustomRadioIcon(props: ComponentProps<"svg">) { return ( <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" focusable="false" {...props} > <path d="M6 1.5L10.5 6L6 10.5L1.5 6L6 1.5Z" fill="currentColor" /> </svg> );}export function CustomIndicatorRadioDemo() { const labelId = useId(); return ( <RadioGroup aria-labelledby={labelId} defaultValue="team"> <RadioGroupLabel id={labelId}>Account Type</RadioGroupLabel> <RadioGroupList> {options.map((option) => ( <RadioGroupItem key={option.value}> <RadioGroupItemControl value={option.value} indicator={<CustomRadioIcon />} /> <RadioGroupItemLabel>{option.label}</RadioGroupItemLabel> </RadioGroupItem> ))} </RadioGroupList> </RadioGroup> );}const options = [ { value: "personal", label: "Personal account" }, { value: "team", label: "Team account" }, { value: "enterprise", label: "Enterprise account" },];Class Names
Pass className to visible slots and use classNames for internal indicator styling.
import { RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemLabel, RadioGroupLabel, RadioGroupList,} from "moduix";import { useId } from "react";import styles from "./styled-radio-demo.module.css";export function StyledRadioDemo() { const labelId = useId(); return ( <RadioGroup aria-labelledby={labelId} defaultValue="team" className={styles.group}> <RadioGroupLabel id={labelId} className={styles.label}> Styled Account Type </RadioGroupLabel> <RadioGroupList className={styles.list}> {options.map((option) => ( <RadioGroupItem key={option.value} className={styles.item}> <RadioGroupItemControl value={option.value} className={styles.control} classNames={{ indicator: styles.indicator }} /> <RadioGroupItemLabel className={styles.label}> {option.label} </RadioGroupItemLabel> </RadioGroupItem> ))} </RadioGroupList> </RadioGroup> );}.group { gap: var(--spacing-3);}.label { color: var(--color-primary);}.list { gap: var(--spacing-3);}.item { gap: var(--spacing-3);}.control { border-color: var(--color-primary);}.control[data-checked] { background-color: var(--color-primary);}.indicator { color: var(--color-primary-foreground);}const options = [ { value: "personal", label: "Personal account" }, { value: "team", label: "Team account" }, { value: "enterprise", label: "Enterprise account" },];Field Composition
Use RadioField, Radio, and RadioLabel directly when you want a compact enclosing-label composition.
import { Radio, RadioField, RadioGroup, RadioGroupLabel, RadioLabel,} from "moduix";import { useId } from "react";export function RadioFieldDemo() { const labelId = useId(); return ( <RadioGroup aria-labelledby={labelId} defaultValue="personal"> <RadioGroupLabel id={labelId}>Members</RadioGroupLabel> <div> {options.map((option) => ( <RadioField key={option.value}> <Radio value={option.value} /> <RadioLabel>{option.label}</RadioLabel> </RadioField> ))} </div> </RadioGroup> );}const options = [ { value: "personal", label: "Personal account" }, { value: "team", label: "Team account" }, { value: "enterprise", label: "Enterprise account" },];Sibling Label
Use nativeButton with render={<button />} for the sibling label pattern with htmlFor and id.
import { Radio, RadioGroup } from "moduix";import { useId } from "react";export function SiblingLabelRadioDemo() { const id = useId(); const labelId = useId(); return ( <div> <div id={labelId}>Delivery method</div> <RadioGroup defaultValue="email" aria-labelledby={labelId}> <Radio nativeButton render={<button />} id={id} value="email" /> </RadioGroup> <label htmlFor={id}>Email</label> </div> );}Form Integration
Use Field and Fieldset when the group needs form naming, validation state, or shared labeling.
import { Field, FieldItem, FieldLabel, Fieldset, FieldsetLegend, Radio, RadioGroup, RadioLabel,} from "moduix";export function RadioFormDemo() { return ( <Field name="storageType"> <Fieldset render={<RadioGroup defaultValue="ssd" />}> <FieldsetLegend>Storage type</FieldsetLegend> <FieldItem> <FieldLabel> <Radio value="ssd" /> <RadioLabel>SSD</RadioLabel> </FieldLabel> </FieldItem> <FieldItem> <FieldLabel> <Radio value="hdd" /> <RadioLabel>HDD</RadioLabel> </FieldLabel> </FieldItem> </Fieldset> </Field> );}