RadioSelect

PreviousNext

A dropdown trigger styled like a text field that opens a panel of radio buttons, enabling single-value selection.

Please select a priority
'use client';

import { useState } from 'react';
import { RadioSelect } from '@/ui/forms/RadioSelect';

const statusOptions = [
  { value: 'open', label: 'Open' },
  { value: 'fixed', label: 'Fixed' },
  { value: 'in-progress', label: 'In Progress' },
  { value: 'closed', label: 'Closed' },
];

export function RadioSelectDemo() {
  const [status, setStatus] = useState<string>('open');
  const [category, setCategory] = useState<string>('');

  return (
    <div className="space-y-24" style={{ minWidth: 300 }}>
      <RadioSelect
        label="Status"
        placeholder="Select a status..."
        options={statusOptions}
        value={status}
        onChange={(_, v) => setStatus(v)}
      />

      <RadioSelect
        label="Category"
        placeholder="Select a category..."
        groups={[
          {
            label: 'Travel',
            options: [
              { value: 'flight', label: 'Flight' },
              { value: 'hotel', label: 'Hotel' },
            ],
          },
          {
            label: 'Activities',
            options: [
              { value: 'ski', label: 'Ski' },
              { value: 'golf', label: 'Golf' },
              { value: 'spa', label: 'Spa' },
            ],
          },
        ]}
        value={category}
        onChange={(_, v) => setCategory(v)}
      />

      <RadioSelect
        label="Priority"
        options={[
          { value: 'high', label: 'High' },
          { value: 'medium', label: 'Medium' },
          { value: 'low', label: 'Low' },
        ]}
        validationStatus="error"
        errorMessage="Please select a priority"
      />

      <RadioSelect label="Tags" options={statusOptions} value="open" disabled />
    </div>
  );
}

Installation

npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/radio-select.json

Usage

import { RadioSelect } from '@/ui/forms/RadioSelect';
 
export function Component() {
  return (
    <RadioSelect
      label="Status"
      options={[
        { value: 'open', label: 'Open' },
        { value: 'fixed', label: 'Fixed' },
      ]}
    />
  );
}

Examples

Flat options

The simplest usage — pass an options array and optionally a value to pre-select.

<RadioSelect
  label="Status"
  placeholder="Select a status..."
  options={[
    { value: 'open', label: 'Open' },
    { value: 'fixed', label: 'Fixed' },
    { value: 'in-progress', label: 'In Progress' },
    { value: 'closed', label: 'Closed' },
  ]}
  value="open"
/>

Controlled

Manage the selected value in external state with onChange.

function Controlled() {
  const [value, setValue] = useState('open');
 
  return (
    <RadioSelect
      label="Status"
      options={[
        { value: 'open', label: 'Open' },
        { value: 'fixed', label: 'Fixed' },
        { value: 'in-progress', label: 'In Progress' },
        { value: 'closed', label: 'Closed' },
      ]}
      value={value}
      onChange={(_, v) => setValue(v)}
    />
  );
}

Grouped options

Use groups instead of options to render options under labeled sections.

<RadioSelect
  label="Category"
  placeholder="Select a category..."
  groups={[
    {
      label: 'Travel',
      options: [
        { value: 'flight', label: 'Flight' },
        { value: 'hotel', label: 'Hotel' },
      ],
    },
    {
      label: 'Activities',
      options: [
        { value: 'ski', label: 'Ski' },
        { value: 'golf', label: 'Golf' },
        { value: 'spa', label: 'Spa' },
      ],
    },
  ]}
/>

Custom trigger label

Use renderValue to display a custom string in the trigger instead of the option label.

<RadioSelect
  label="Priority"
  options={[
    { value: 'high', label: 'High priority' },
    { value: 'medium', label: 'Medium priority' },
    { value: 'low', label: 'Low priority' },
  ]}
  value="high"
  renderValue={(val) => `Priority: ${val}`}
/>

Validation states

Pass validationStatus and errorMessage to indicate an error or success.

<RadioSelect
  label="Priority"
  options={priorityOptions}
  validationStatus="error"
  errorMessage="Please select a priority"
/>
 
<RadioSelect
  label="Priority"
  options={priorityOptions}
  value="high"
  validationStatus="success"
/>

Disabled

<RadioSelect
  label="Status"
  options={statusOptions}
  value="open"
  disabled
/>

Behaviour

  • Opening the dropdown shows all options as radio buttons.
  • Selecting an option updates the trigger label and closes the dropdown immediately — unlike CheckboxSelect, only one value can be active at a time.
  • Clicking outside the open panel closes it without changing the selection.
  • The trigger inherits the same pill-shaped border styling as TextField and Select.
  • When no value is selected the placeholder text is shown in muted grey.

Accessibility

  • The trigger has aria-haspopup="listbox" and aria-expanded to communicate its state to screen readers.
  • The panel uses role="listbox" and is hidden via aria-hidden when closed.
  • Each option is a native <input type="radio"> with an associated <label>, giving full keyboard and screen-reader support.
  • The component is disabled at the native level (disabled on the <button> and each <input>), so it is excluded from the tab order automatically.

API Reference

RadioSelect

PropTypeDefaultDescription
labelReactNodeundefinedLabel displayed above the trigger
descriptionstringundefinedSecondary description shown next to the label
placeholderstring"Select..."Text shown in the trigger when no value is selected
optionsRadioSelectOption[]undefinedFlat list of options. Use groups instead for grouped options
groupsRadioSelectGroup[]undefinedGrouped options with optional group labels
valueValueundefinedControlled selected value
onChange(event: Event, value: Value) => voidundefinedCalled with the event and new value when an option is selected
renderValue(selected: Value) => ReactNodeundefinedCustom renderer for the trigger label when a value is selected
disabledbooleanfalseDisables the trigger and all radio buttons
requiredbooleanfalseMarks the field as required
hideRequiredStarbooleanundefinedHides the required star even when required is true
validationStatus"default" | "error" | "success""default"Visual validation state of the trigger
errorMessagestringundefinedError text shown below when validationStatus is "error"
classNamestringundefinedAdditional CSS classes on the outer wrapper

RadioSelectOption

PropTypeDescription
valueValueThe option's value
labelReactNodeLabel shown in the panel

RadioSelectGroup

PropTypeDescription
labelReactNodeOptional group heading
optionsRadioSelectOption[]Options within this group

Notes

  • Pass groups instead of options to render options under labeled sections.
  • Use renderValue to customize the summary shown in the trigger (e.g. "Priority: high").
  • The dropdown closes automatically after a selection — this distinguishes RadioSelect from CheckboxSelect, where the panel stays open to allow multiple picks.
  • options and groups are mutually exclusive; if both are provided, options takes precedence.