Skip to main content

Overview

The radioGroup widget creates a group of mutually exclusive radio buttons. Only one option can be selected at a time. Radio buttons are displayed as ( ) when unselected and (*) when selected.

Basic Usage

import { ui } from "@rezi-ui/core";

ui.radioGroup({
  id: "payment-method",
  value: state.paymentMethod,
  options: [
    { value: "card", label: "Credit Card" },
    { value: "paypal", label: "PayPal" },
    { value: "bank", label: "Bank Transfer" }
  ],
  onChange: (value) => app.update({ paymentMethod: value })
})

Props

id
string
required
Unique identifier for focus routing. Required for all radio groups.
value
string
required
Currently selected option value. Must be controlled by your application state.
options
readonly SelectOption[]
required
Available options. Each option has:
  • value: string - Option value used in form state
  • label: string - Display label for the option
  • disabled?: boolean - Whether this option is disabled
onChange
(value: string) => void
Callback invoked when selection changes.
direction
'horizontal' | 'vertical'
default:"vertical"
Layout direction for radio options.
disabled
boolean
default:"false"
When true, entire radio group cannot be focused or changed.
focusable
boolean
default:"true"
When false, opt out of Tab focus order while keeping id-based routing available.
accessibleLabel
string
Optional semantic label for accessibility and debug announcements.

Styling Props

dsTone
WidgetTone
default:"default"
Design system color tone for selected/focus rendering: "default", "primary", "success", "warning", "danger".
dsSize
WidgetSize
default:"md"
Design system size preset: "xs", "sm", "md", "lg", "xl".
focusConfig
FocusConfig
Optional focus appearance configuration for custom focus indicators.

Event Handling

Radio groups use a controlled pattern. You must manage the selected value in state:
ui.radioGroup({
  id: "plan",
  value: state.selectedPlan,
  options: [
    { value: "free", label: "Free Plan" },
    { value: "pro", label: "Pro Plan" },
    { value: "enterprise", label: "Enterprise Plan" }
  ],
  onChange: (value) => {
    app.update({ selectedPlan: value });
    
    // Optionally trigger side effects
    fetchPlanDetails(value);
  }
})

Layout Direction

Vertical (Default)

ui.radioGroup({
  id: "size",
  value: state.size,
  direction: "vertical",
  options: [
    { value: "small", label: "Small" },
    { value: "medium", label: "Medium" },
    { value: "large", label: "Large" }
  ]
})

Horizontal

ui.radioGroup({
  id: "size",
  value: state.size,
  direction: "horizontal",
  options: [
    { value: "s", label: "S" },
    { value: "m", label: "M" },
    { value: "l", label: "L" },
    { value: "xl", label: "XL" }
  ]
})

Design System Integration

Radio groups automatically use design system recipes when the active theme provides semantic color tokens:
// Primary tone
ui.radioGroup({
  id: "priority",
  value: state.priority,
  dsTone: "primary",
  options: [
    { value: "low", label: "Low" },
    { value: "medium", label: "Medium" },
    { value: "high", label: "High" }
  ]
})

// Success tone
ui.radioGroup({
  id: "status",
  value: state.status,
  dsTone: "success",
  options: [
    { value: "active", label: "Active" },
    { value: "inactive", label: "Inactive" }
  ]
})

Form Integration

Radio groups are typically wrapped with ui.field() for labels:
ui.form([
  ui.field({
    label: "Subscription Plan",
    required: true,
    children: ui.radioGroup({
      id: "plan",
      value: state.plan,
      options: [
        { value: "free", label: "Free - $0/month" },
        { value: "pro", label: "Pro - $10/month" },
        { value: "enterprise", label: "Enterprise - $50/month" }
      ],
      onChange: (value) => app.update({ plan: value })
    })
  }),
  ui.actions([
    ui.button("continue", "Continue", { intent: "primary" })
  ])
])

Disabled Options

Specific Options Disabled

ui.radioGroup({
  id: "tier",
  value: state.tier,
  options: [
    { value: "basic", label: "Basic" },
    { value: "standard", label: "Standard" },
    { value: "premium", label: "Premium (Coming Soon)", disabled: true }
  ]
})

Entire Group Disabled

ui.radioGroup({
  id: "readonly-option",
  value: state.value,
  disabled: true,
  options: [
    { value: "a", label: "Option A" },
    { value: "b", label: "Option B" }
  ]
})

Examples

Payment Method Selector

ui.panel("Payment Method", [
  ui.radioGroup({
    id: "payment",
    value: state.paymentMethod,
    options: [
      { value: "card", label: "💳 Credit Card" },
      { value: "paypal", label: "💰 PayPal" },
      { value: "bank", label: "🏦 Bank Transfer" },
      { value: "crypto", label: "₿ Cryptocurrency", disabled: true }
    ],
    onChange: (value) => app.update({ paymentMethod: value })
  }),
  state.paymentMethod === "card" && ui.column({ gap: 1 }, [
    ui.field({
      label: "Card Number",
      children: ui.input({
        id: "card-number",
        value: state.cardNumber,
        placeholder: "1234 5678 9012 3456"
      })
    })
  ])
])

Survey Question

ui.column({ gap: 1 }, [
  ui.text("How satisfied are you with our product?", { variant: "heading" }),
  ui.radioGroup({
    id: "satisfaction",
    value: state.satisfaction,
    options: [
      { value: "very-satisfied", label: "😄 Very Satisfied" },
      { value: "satisfied", label: "🙂 Satisfied" },
      { value: "neutral", label: "😐 Neutral" },
      { value: "dissatisfied", label: "😞 Dissatisfied" },
      { value: "very-dissatisfied", label: "😡 Very Dissatisfied" }
    ],
    onChange: (value) => app.update({ satisfaction: value })
  })
])

Settings Toggle

ui.field({
  label: "Theme",
  children: ui.radioGroup({
    id: "theme",
    value: state.theme,
    direction: "horizontal",
    options: [
      { value: "light", label: "Light" },
      { value: "dark", label: "Dark" },
      { value: "auto", label: "Auto" }
    ],
    onChange: (value) => {
      app.update({ theme: value });
      applyTheme(value);
    }
  })
})

Shipping Options

ui.form([
  ui.field({
    label: "Shipping Method",
    children: ui.radioGroup({
      id: "shipping",
      value: state.shippingMethod,
      options: [
        { value: "standard", label: "Standard (5-7 days) - Free" },
        { value: "express", label: "Express (2-3 days) - $10" },
        { value: "overnight", label: "Overnight - $25" }
      ],
      onChange: (value) => {
        const costs = { standard: 0, express: 10, overnight: 25 };
        app.update({
          shippingMethod: value,
          shippingCost: costs[value as keyof typeof costs]
        });
      }
    })
  }),
  ui.divider(),
  ui.row({ justify: "between" }, [
    ui.text("Shipping:"),
    ui.text(`$${state.shippingCost.toFixed(2)}`)
  ]),
  ui.row({ justify: "between" }, [
    ui.text("Total:", { variant: "heading" }),
    ui.text(`$${(state.subtotal + state.shippingCost).toFixed(2)}`, { variant: "heading" })
  ])
])

Filter Controls

ui.row({ gap: 2, items: "start" }, [
  ui.column({ gap: 1 }, [
    ui.text("Sort By", { variant: "caption" }),
    ui.radioGroup({
      id: "sort",
      value: state.sortBy,
      options: [
        { value: "newest", label: "Newest First" },
        { value: "oldest", label: "Oldest First" },
        { value: "popular", label: "Most Popular" }
      ],
      onChange: (value) => {
        app.update({ sortBy: value });
        refreshResults();
      }
    })
  ]),
  ui.column({ gap: 1 }, [
    ui.text("Status", { variant: "caption" }),
    ui.radioGroup({
      id: "status-filter",
      value: state.statusFilter,
      options: [
        { value: "all", label: "All" },
        { value: "active", label: "Active" },
        { value: "completed", label: "Completed" }
      ],
      onChange: (value) => {
        app.update({ statusFilter: value });
        refreshResults();
      }
    })
  ])
])

Keyboard Navigation

When focused, radio groups support:
  • Arrow Up/Down - Navigate between options (vertical layout)
  • Arrow Left/Right - Navigate between options (horizontal layout)
  • Space - Select focused option
  • Enter - Select focused option
  • Tab - Move to next focusable element
  • Shift+Tab - Move to previous focusable element
Navigation automatically skips disabled options.

Visual States

StateIndicatorDescription
Unselected( )Not selected
Selected(*)Currently selected option
Disabled unselected(-)Cannot be selected
Disabled selected(*)Selected but cannot be changed

Accessibility

  • Radio groups require a unique id for focus routing
  • Use accessibleLabel for descriptive announcements
  • Always wrap with ui.field() to provide a group label
  • Provide clear option labels
  • Disabled options are skipped during keyboard navigation
  • Focus indicators are shown when navigating with keyboard
  • Checkbox - For independent boolean options
  • Select - For dropdown selection
  • Field - For wrapping inputs with labels
  • Button - For action buttons

Build docs developers (and LLMs) love