Skip to content
v2.5.0
English
Русский
Documentation

Radio

Radio is the single-choice selection control for mutually exclusive options that share the same name.
radio.tsx · 6 lines
import { Radio, Text } from '@vega-ui/react';

<div role='radiogroup' aria-label='Billing period'>
  <label style={{ display: 'flex', alignItems: 'center', gap: 10 }}><Radio name='preview-billing' value='monthly' defaultChecked /><Text size={3}>Monthly</Text></label>
  <label style={{ display: 'flex', alignItems: 'center', gap: 10 }}><Radio name='preview-billing' value='yearly' /><Text size={3}>Yearly</Text></label>
</div>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
13 tests

Summary

Radio is the single-choice selection control for mutually exclusive options that share the same name. It is the most form-like exclusive-choice primitive in the selection family.

Imports

import {
  Radio,
  type RadioProps,
  type RadioSize,
  type RadioVariant,
} from '@vega-ui/react';

Exported Types

  • RadioProps
  • RadioSize
  • RadioVariant

Minimal Composition

minimal-composition.tsx · 16 lines
import { Radio } from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <div role='radiogroup' aria-label='Theme' style={{ display: 'flex', gap: 'var(--spacing-8)' }}>
      <label style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <Radio name='theme' value='light' defaultChecked />
        Light
      </label>
      <label style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <Radio name='theme' value='dark' />
        Dark

Required Parts

  • Radio: native radio input surface

Optional Parts

  • No extra public parts beyond the input itself

Composition Order

  1. visible label wrapper
  2. Radio
  3. label text

Variants

  • Sizes: sm, md, lg
  • Variants: primary, secondary
  • States: unchecked, checked, disabled, checked-disabled

Edge Cases

  • Radios only behave as one group when the consumer gives related inputs the same name.
  • Labels should describe the option, not the action of clicking it.
  • Radios are the wrong choice for independent boolean settings.
  • If options need richer descriptions or tile-sized targets, SegmentedControl or CheckboxCard may be clearer depending on exclusivity.

Common Mistakes

  • Using radios without a shared name.
  • Using radios for multi-select choices.
  • Building a radio row where only the tiny control is clickable.

Examples

Basic

Basic: choice group in a fieldset

Radio is a bare native input[type='radio'], so each one is wrapped in a <label> with its text. Radios with the same name form one group: only one can be checked, and arrow keys move between them. Fieldset with a legend names the group.

labeled-radio-option.tsx · 23 lines
import { Fieldset, FieldsetLegend, Radio } from '@vega-ui/react';

const methods = [
  { value: 'standard', label: 'Standard · 5–7 business days · Free' },
  { value: 'express', label: 'Express · 2–3 business days · $9.90' },
  { value: 'overnight', label: 'Overnight · next business day · $24.00' },
];

export default function LabeledRadioOption() {
  return (
    <Fieldset>
      <FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-5)' }}>Shipping method</FieldsetLegend>

Controlled/Stateful

Controlled/Stateful: selected value in the parent

The parent stores the selected value and passes checked={cycle === item.value} to every radio; onChange reads event.currentTarget.value. The price below is computed from the same state.

parent-owned-selected-plan.tsx · 39 lines
import { useState } from 'react';
import { Fieldset, FieldsetLegend, Radio, Text } from '@vega-ui/react';

const cycles = [
  { value: 'monthly', label: 'Monthly', perMonth: 15 },
  { value: 'yearly', label: 'Yearly (2 months free)', perMonth: 12.5 },
];

export default function ParentOwnedSelectedPlan() {
  const [cycle, setCycle] = useState('yearly');

  const selected = cycles.find((item) => item.value === cycle) ?? cycles[0];

Form/Integration

Form/Integration: required group and FormData

required on the radios makes the whole group required, so the browser blocks submit until one is chosen. FormData.get('size') returns the checked value; reset clears the selection.

form-data-submission.tsx · 41 lines
import { FormEvent, useState } from 'react';
import { Button, Fieldset, FieldsetLegend, Radio, Text } from '@vega-ui/react';

const sizes = [
  { value: 's-2vcpu-4gb', label: '2 vCPU · 4 GB RAM · $24/mo' },
  { value: 's-4vcpu-8gb', label: '4 vCPU · 8 GB RAM · $48/mo' },
  { value: 's-8vcpu-16gb', label: '8 vCPU · 16 GB RAM · $96/mo' },
];

export default function FormDataSubmission() {
  const [result, setResult] = useState('');

Layout/Overlay

Layout/Overlay: choice inside a dialog

The radios live in a form method='dialog': saving reads FormData, updates the value outside and closes the dialog. Reopening starts from the saved value because the content remounts.

radio-choice-in-overlays.tsx · 71 lines
import { CSSProperties, FormEvent, useState } from 'react';
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Fieldset,
  FieldsetLegend,
  Heading,
  Icon,
  IconButton,

Error

Error: required group without a selection

The invalid event of the required radios replaces the browser bubble: the error is linked to the Fieldset through aria-describedby, the radios get aria-invalid, and focus moves to the first option. Choosing any option clears the error.

missing-required-radio-selection.tsx · 55 lines
import { FormEvent, useRef, useState } from 'react';
import { Button, Fieldset, FieldsetLegend, HelperText, Radio } from '@vega-ui/react';

const reasons = [
  { value: 'price', label: 'It is too expensive' },
  { value: 'features', label: 'It is missing features I need' },
  { value: 'switching', label: 'I am switching to another product' },
  { value: 'other', label: 'Other' },
];

export default function MissingRequiredRadioSelection() {
  const [invalid, setInvalid] = useState(false);

Disabled

Disabled: disabled option and disabled group

A single unavailable option is disabled and explains why through aria-describedby. A Fieldset disabled disables every radio inside natively. The label text is dimmed by the example, because Radio only styles the input. Disabled radios are not submitted.

disabled-option.tsx · 42 lines
import { Fieldset, FieldsetLegend, HelperText, Radio } from '@vega-ui/react';

export default function DisabledOption() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <Fieldset>
        <FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-5)' }}>Data region</FieldsetLegend>
        <div style={{ display: 'grid', gap: 'var(--spacing-6)' }}>
          <label style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
            <Radio name='region' value='eu' defaultChecked />
            European Union (Frankfurt)
          </label>

Edge

Edge: options with long descriptions

When each option has a description, the label is linked with htmlFor/id, the description through aria-describedby, and the radio is aligned to the first line with flex-start and flex-shrink: 0 so long text does not squeeze it.

options-with-descriptions.tsx · 47 lines
import { Fieldset, FieldsetLegend, Radio, Text } from '@vega-ui/react';

const policies = [
  {
    value: 'anyone',
    label: 'Anyone with the link',
    description: 'People outside your organization can open shared documents without signing in.',
  },
  {
    value: 'organization',
    label: 'People in Acme Corp',
    description: 'Only signed-in members of your organization can open shared documents.',

Edge: repeated groups need unique names

Every table row is its own group, so each row gets its own name built from the row id; a shared name would turn the whole table into one group. Radios without visible text get an aria-label that names both the row and the option.

repeated-groups-unique-names.tsx · 48 lines
import { useState } from 'react';
import { Radio, Table, TableBody, TableData, TableHead, TableHeading, TableRow } from '@vega-ui/react';

const members = [
  { id: 'u-201', name: 'Maya Chen' },
  { id: 'u-202', name: 'Lucas Ferreira' },
  { id: 'u-203', name: 'Aisha Okafor' },
];
const roles = ['viewer', 'editor', 'admin'];

export default function RepeatedGroupsUniqueNames() {
  const [assigned, setAssigned] = useState<Record<string, string>>({ 'u-201': 'admin', 'u-202': 'editor', 'u-203': 'viewer' });

Anatomy

Overview

Radio is a single native input rather than a multi-part compound component. The real composition contract lives in how multiple radios are grouped through shared name and visible labels.

Required Parts

Radio

Required. Renders the native input[type='radio'] and owns checked, disabled, size, and variant behavior.

Optional Parts

Radio does not expose additional public child parts.

Composition Order

  1. visible label wrapper
  2. Radio
  3. label text

Valid Composition Patterns

Two valid ways to name a radio: wrap it in a <label> together with its text, or give it an id and point a separate Label at it with htmlFor. Both radios share one name, so they form one group.

valid-composition-patterns.tsx · 16 lines
import { Label, Radio } from '@vega-ui/react';

export default function ValidCompositionPatterns() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)' }}>
      <label style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <Radio name='density' value='comfortable' defaultChecked />
        Comfortable
      </label>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <Radio id='density-compact' name='density' value='compact' />
        <Label htmlFor='density-compact'>Compact</Label>

Invalid Composition Patterns

Radios without a shared name

This breaks mutually exclusive group behavior.

Radio used as a standalone boolean toggle

If the choice is independent rather than exclusive, use Checkbox or Switch.

Label text detached from the control

This makes the radio harder to understand and harder to click.