FSAI Design System
Components

Radio

Labeled radio control for mutually exclusive choices, built on RadioPrimitive.

Overview

Radio is the shared labeled radio control used for mutually exclusive choices.

It wraps RadioPrimitive with:

  • optional label text
  • left or right label placement
  • clickable label behavior

Unlike a native HTML radio group, each Radio is a controlled boolean item. The parent is responsible for group state and ensuring only the intended option is checked.

Basic Usage

<div className="space-y-2">
  <Radio
    label="Save over current version"
    checked={!saveAsNewVersion}
    onChange={() => setSaveAsNewVersion(false)}
  />
  <Radio
    label="Save as new version"
    checked={saveAsNewVersion}
    onChange={() => setSaveAsNewVersion(true)}
  />
</div>

This is the pattern used in PortalEditorLayout.tsx.

Label Placement

Use labelSide="right" when the option layout reads better with the control before the text.

<Radio
  label="Send notifications"
  labelSide="right"
  checked={sendNotifications}
  onChange={setSendNotifications}
/>

Props

PropTypeDefaultDescription
checkedbooleanWhether this option is selected.
onChange(checked: boolean) => voidCalled when the radio is toggled.
labelstringOptional label text.
labelSide'left' | 'right''left'Which side the label appears on.
labelClickablebooleantrueAllows clicking the label to toggle the radio.
size'sm' | 'md''md'Control size.
disabledbooleanfalseDisables interaction.

Radio Primitive

RadioPrimitive is the low-level circle control behind Radio.

Use it directly only when you need a custom radio-group layout that should not use the standard Radio label wrapper.

<div className="flex items-center gap-2">
  <RadioPrimitive
    checked={value === 'draft'}
    onChange={() => setValue('draft')}
    aria-label="Draft"
  />
  <span>Draft</span>
</div>

Radio composes:

  • Label for the visible text
  • RadioPrimitive for the actual circular control

It passes label through as aria-label to the primitive, so unlabeled custom usage should still provide an accessible label another way.

Primitive props:

PropTypeDefaultDescription
checkedbooleanWhether the radio is selected.
onChange(checked: boolean) => voidCalled when the radio is toggled.
aria-labelstringAccessible label for the control.
size'sm' | 'md''md'Control size.
disabledbooleanfalseDisables interaction.

Brand Dashboard Usage

Representative usages:

PatternPathNotes
Explicit version choicefsai/apps/brand-dashboard/src/pages/PortalEditor/ApplicantPortalEditor/PortalEditorLayout/PortalEditorLayout.tsxTwo radios model a mutually exclusive save choice.
Deal or fee optionsfsai/apps/brand-dashboard/src/modules/deals/components/DealOverviewFees.tsxRadios used for exclusive fee configuration choices.
Settings choice in a modalfsai/apps/brand-dashboard/src/modules/library/components/DeleteCollectionConfirmModal/DeleteCollectionConfirmModal.tsxRadios used for modal-level choice between behaviors.
Quiz configurationfsai/apps/brand-dashboard/src/pages/CourseBuilder/QuizBuilder/QuizSettings.tsxMutually exclusive settings selection.

Important Cautions

  • Radio is not a native <input type="radio">.
  • There is no built-in name grouping or native form-post behavior.
  • The parent component must own the selected value and keep the group mutually exclusive.
  • RadioPrimitive renders a custom element with role="radio" and aria-checked, but it still does not provide native group semantics by itself.
  • For explicit yes/no controls, check whether BooleanRadio already matches the desired UI before composing a pair of radios yourself.

Guidelines

  • Do use Radio when the user must pick exactly one option from a small set
  • Do keep group state in the parent component
  • Do use RadioPrimitive only for custom option layouts
  • Do use Toggle or Checkbox instead when the control is really a boolean on/off state
  • Don't assume Radio behaves like a native radio input group
  • Don't use a single Radio by itself for a normal boolean field

On this page