KeepNeeds Refinement
Radio Button Component link

A circular radio control for single-select groups. 39 variants across Style (Default/Check) × State (Default/Pressed/Disabled) × Size (Large/Medium/Small) × isSelected × isError.

Rebuilt — structurally clean
The v2.0 rebuild resolved every structural issue: orthogonal axes (Style × State × Size × isSelected × isError), vector ring + dot at all three sizes, semantic layer names, a real Pressed state, and Disabled + unselected coverage. Pressed now folds into State and error is an orthogonal isError boolean — matching Button v4.0 and Select Item. Only Code Connect registration remains.
In Context

Radio Buttons appear in Radio Button with Label groups — see the Radio Button with Label preview for the composed form row.

Live Preview
Properties
selected
size
style
DS Health
Reusable
Pass
Works across forms, surveys, preference pickers, and single-select list rows. Three sizes (24 / 20 / 16) cover the Large, Medium, and Small text scales, and the Check style extends it to iOS-style picker rows without a second component.
Self-contained
Pass
Every size is built from layered vectors — containercircle (fill) + ring (stroke) — so tokens propagate to all three. Carries its own selection, error, and interaction styling; the raster imgContainer export is gone.
Consistent
Pass
Five orthogonal props — Style × State × Size × isSelected × isError. No conditional axes and no invalid combinations: pressed lives in State alongside Disabled, error is its own boolean, and booleans are lowercase true/false. Matches Button v4.0 and Select Item.
Composable
Pass
Nests inside Radio Button – With Label, which mirrors this schema exactly — same props, same order, same 39 variants. Layers are semantic (container / circle / ring) and the Check style is a vector icon instance rather than a drawn shape.
Behavior
State iOS Android Figma Property Notes
Default (unselected) Yes Yes State=Default, isSelected=false Empty ring. Resting state for every unpicked option in a group.
Selected Yes Yes State=Default, isSelected=true Brand ring + inner dot, both #005CE5. Exactly one option per group carries this.
Pressed Yes Yes State=Pressed Ring and dot darken to #2340A9. Combines with either isSelected value. Derived at runtime from the touch interaction — not a parameter you pass.
Disabled Yes Yes State=Disabled Muted ring and dot. Ships at both isSelected values, so a disabled group can render its unselected options.
Error Yes Yes isError=true Ring and dot switch to #D61B2C. Orthogonal to State — combines with Default and Pressed.
Selected — check style Yes Yes Style=Check, isSelected=true Filled circle + vector checkmark instead of a dot. For single-select list rows; only meaningful when selected.
Issues
  • Code Connect mappings not registered. Previously blocked by the sparse matrix and the raster large radio — both resolved in v2.0. Registration is now unblocked, but the SwiftUI / Compose mappings are not yet wired. C7 · Code Connect Linkability
  • v2.0: Variant matrix restructured into orthogonal axes — Style (Default/Check) × State (Default/Pressed/Disabled) × Size (Large/Medium/Small) × isSelected × isError. selected no longer conflates selection with modifier states, and no property is conditional on another. (C2)
  • v2.0: Pressed folded into State, error split into an orthogonal isError boolean — removes the illegal Disabled + Pressed combination the old two-axis model allowed, and matches the Button v4.0 / Select Item pattern. (C2/C4)
  • v2.0: Large radio rebuilt as layered vectors — containercircle (ELLIPSE fill) + ring (ELLIPSE stroke), both on #005CE5. The pre-rendered imgContainer raster is gone, so token changes now propagate to every size. (C3)
  • v2.0: Misleading .base/checkbox frame renamed — layers are now container / circle / ring. (C6)
  • v2.0: Pressed state added — State=Pressed covers touch feedback across Default, Check, and error variants (#2340A9). Focused is N/A on mobile: touch has no focus ring. (C5)
  • v2.0: Disabled + isSelected=false added at all three sizes — a disabled radio group can now render its unselected options, which was previously impossible. (C5)
  • v2.0: Boolean variant values lowercased to true/false, matching the C2 convention. (C2)
  • v2.0: Checkmark promoted to a vector icon instance (CheckmarkGrid) rather than a drawn shape. (C6)
  • v2.0: Disabled deliberately excludes isError — confirmed as an intentional omission, not a gap. A locked control offers the user no path to resolve a validation error, so the combination has no affordance and ships no variants. (C5)
  • v2.0: Style=Check confirmed as intentional — retained rather than retired, now shipping as a vector icon instance with full Default / Pressed / Disabled coverage. Usage guidance added to disambiguate it from Checkbox. (C6)
Recommendations
  • Register Code Connect mapping to EBRadioButton. With the orthogonal axes, vector rebuild, and state coverage all shipped, wire the Figma properties (Style, State, Size, isSelected, isError) 1:1 to the SwiftUI / Compose API. Note isSelected/isError are variant enums, so Code Connect maps them via figma.enum()Bool. Docs
  • Split properties into orthogonal axes. v2.0: Applied — and taken further than proposed. Style × State × Size × isSelected × isError, with pressed folded into State and error split out as its own boolean. No invalid combinations remain. Property
  • Rebuild the large radio as vector layers. v2.0: Applied — every size is now containercircle + ring vector ellipses. Tokens flow to all three sizes. Asset
  • Rename the internal frame. v2.0: Applied — .base/checkbox is gone; layers are container / circle / ring. Rename
  • Add pressed states. v2.0: Applied — State=Pressed maps touch feedback to #2340A9. Focused was dropped as N/A on mobile. State
  • Document when to use Check vs Default. v2.0: Applied — the Check style was reviewed and deliberately retained rather than retired. Usage guidance now disambiguates it from Checkbox: Check for single-select list rows, Default (filled dot) for forms where real Checkboxes appear alongside. Docs
  • Record Disabled + isError as an intentional omission. v2.0: Applied — reviewed and confirmed deliberate. A locked control offers no path to resolve a validation error, so the combination ships no variants by design. Docs
Styles
All states — large + small
DES DEV

Pick a state, size, and style to preview. The same matrix would otherwise be 12 static cells; here it's controlled.

Properties
selected
size
style
Properties
Variant All states — large + small
Selected true/false (boolean)
Colors
Border #D7E0EF
Fill #005CE5
Dot #FFFFFF
Error border #D61B2C
Layout
Outer ring 20 × 20
Inner dot 10 × 10 (when selected)
Border radius 50% (circle)
Border 1.5px solid
Typography
Inline label Primary/Multi-line Label/Light/Small
Label font Proxima Soft Semibold · 14 / 16
Colors by State
Role Token TokenValue
Unselected border main/radio-button/color/default/unselected/border #D7E0EF
Selected bg (fill + ring) main/radio-button/color/default/selected/bg #005CE5
border main/radio-button/color/default/selected/border #005CE5
inner dot / checkmark main/radio-button/color/default/selected/icon #FFFFFF
Disabled bg main/radio-button/color/disabled/selected/bg #C2CFE5
border main/radio-button/color/disabled/selected/border #C2CFE5
inner icon main/radio-button/color/disabled/selected/icon #FFFFFF
Error border (unselected) main/radio-button/color/error/unselected/border #D61B2C
bg (selected) main/radio-button/color/error/selected/bg #D61B2C
border (selected) main/radio-button/color/error/selected/border #D61B2C
Installation Planned API

iOS — Swift Package Manager

// In Xcode: File → Add Package Dependencies
"https://github.com/AY-Org/eb-ds-ios"

Android — Gradle (Kotlin DSL)

dependencies {
    implementation("com.eastblue.ds:radio:1.0.0")
}
Property Mapping
Figma PropertySwiftUICompose
selected=unselected/selected selected: Bool selected: Bool
selected=disabled state=disabled (combine w/ selected) .disabled(true)
selected=error state=error state: .error
size=small/large size: EBRadioSize .controlSize(.small)
style=filled/checkmark (retire — pick filled only)
style=default (unselected/error) implicit from selected=false
onToggle onChange: (Bool) -> Void
SwiftUI
ios/Components/Radio/EBRadioButton.swift
Jetpack Compose
android/components/radio/EBRadioButton.kt
Usage Snippets Planned API
Usage
// Basic selection
EBRadioButton(selected: $isSelected)

// Large size with error state
EBRadioButton(selected: $isSelected, state: .error)
    .controlSize(.large)

// Disabled
EBRadioButton(selected: true)
    .disabled(true)
// Basic selection
EBRadioButton(selected = checked, onCheckedChange = { checked = it })

// Large size with error state
EBRadioButton(
    selected = checked,
    onCheckedChange = { checked = it },
    size = EBRadioSize.Large,
    state = EBRadioState.Error
)

// Disabled
EBRadioButton(
    selected = true,
    onCheckedChange = { },
    enabled = false
)
Accessibility
RequirementiOSAndroid
Role Inherit radio semantics via Toggle(isOn:) with radio style Use Modifier.selectable(role = Role.RadioButton)
Selected state .accessibilityAddTraits(.isSelected) selected = true in semantics
Group label Wrap options in a .accessibilityElement(children: .contain) with group label Use Modifier.selectableGroup() on parent
Tap target Radio is 20px; wrap in 44pt hit area Wrap in 48dp hit area
Error announcement Pair with a label and announce the error message after the label Use semantics { error(...) }
Usage Guidelines

Do

Use <code>Style=Check</code> for single-select list rows — settings pickers, option lists, and full-width choice rows where the checkmark reads as "this one is chosen" (the iOS picker convention).

Don't

Don't use <code>Style=Check</code> in a form where real Checkboxes appear alongside it — the two become visually indistinguishable. Use <code>Style=Default</code> (filled dot) there; the dot is the universal single-select affordance.

Do

Use <code>isError=true</code> together with <code>State=Default</code> or <code>Pressed</code> to flag a failed validation the user can still act on.

Don't

Don't expect <code>Disabled + isError</code> — it ships no variants by design. A locked control gives the user no way to resolve the error, so surface the message elsewhere (e.g. a Callout or Alert) rather than on the radio.

Do

Pair <code>Size</code> to the surrounding text scale — Large (24) for 18px labels, Medium (20) for 16px, Small (16) for dense lists.

Don't

Don't mix sizes within a single radio group — every option in one group should share the same <code>Size</code> and <code>Style</code>.

Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Needs Refinement Internal frame named .base/checkbox instead of .base/radio.
C2 Variant & Property Naming Requires Rework selected mixes selection with modifiers; style is conditional.
C3 Token Coverage Requires Rework Large variants are raster — tokens don't flow to the large size.
C4 Native Mappability Ready Maps to Toggle / RadioButton with custom style.
C5 Interaction State Coverage Requires Rework No pressed or focused states.
C6 Asset & Icon Quality Requires Rework Checkmark style conflicts with Checkbox visually; large radio is a pre-rendered image.
C7 Code Connect Linkability Needs Refinement Blocked by C2. Clean mapping lands after prop split.
Variants Inventory (39 total)

Style (2) × State (3) × Size (3) × isSelected (2) × isError (2) = 72 theoretical. 39 ship. Check is only meaningful when selected, and Disabled deliberately excludes isError — a locked control offers no path to resolve the error. Grouped below by Style × State; each row covers all three sizes.

StyleStateisSelectedisErrorCountNotes
DefaultDefaultfalsefalse3Empty ring · all sizes
DefaultDefaulttruefalse3Brand ring + dot #005CE5
DefaultDefaultfalsetrue3Error ring #D61B2C
DefaultDefaulttruetrue3Error ring + dot
DefaultPressedfalsefalse3Pressed ring #2340A9
DefaultPressedtruefalse3Pressed ring + dot
DefaultPressedfalsetrue3Pressed error ring
DefaultPressedtruetrue3Pressed error ring + dot
DefaultDisabledfalsefalse3Muted empty ring
DefaultDisabledtruefalse3Muted ring + dot
CheckDefaulttruefalse3Brand fill + vector checkmark
CheckPressedtruefalse3Pressed fill + checkmark
CheckDisabledtruefalse3Muted fill + checkmark
2.0.0 — July 2026Major
Rebuilt · node 26184:2588
Component rebuilt on a new node — 39 variants across Style (Default/Check) × State (Default/Pressed/Disabled) × Size (Large/Medium/Small) × isSelected × isError, replacing the old sparse selected × size × style matrix. Restructured
Rebuild
Properties split into orthogonal axes — pressed folded into State, error split into an orthogonal isError boolean. Removes the illegal Disabled + Pressed combination and aligns with Button v4.0 / Select Item. Resolved
C2 · C4 Resolved
Large radio rebuilt as vector layerscontainercircle + ring ellipses on #005CE5. The pre-rendered imgContainer raster is gone; tokens now propagate to all three sizes. Resolved
C3 Resolved
Layer naming fixed + checkmark promoted to an icon.base/checkbox replaced by container / circle / ring; the Check style now nests a vector Checkmark icon instance. Resolved
C6 Resolved
Pressed state + disabled-unselected coverage addedState=Pressed (#2340A9) covers touch feedback, and Disabled + isSelected=false now exists at all sizes so disabled groups can render unselected options. Focused dropped as N/A on mobile. Resolved
C5 Resolved
Boolean variant values lowercasedTrue/Falsetrue/false, matching the C2 convention. Radio Button is now the reference for boolean vocabulary in the system. Resolved
C2 Resolved
Check + Pressed mislabel corrected — three Check variants were relabelled isError=true while still painted pressed-navy, which also removed Check's pressed state. Renamed back to State=Pressed, isError=false; label and paint now agree. Resolved
C5 Resolved
Disabled + isError=true not covered — 9 slots absent. Likely intentional, but undocumented. Confirm and record. Open
C5 Open
Code Connect mappings — now unblocked by the rebuild; SwiftUI / Compose mappings not yet registered. Open
C7 Open
1.0.0 — April 2026Major
Initial Assessment · node 18482:35698
Component assessed — 11 variants across sparse selected × size × style matrix. Documented
Initial
Sparse matrix, mixed paradigmsselected mixes selection with modifier states; style is only meaningful when selected. Open
C2 Open
Large radio is raster-baked — tokens don't flow to the large size. Open
C3 Open
Internal frame named .base/checkbox + checkmark style overlaps with Checkbox. Open
C6 Open
No pressed/focused states. Open
C5 Open
Code Connect mappings — Not registered. Open
C7 Open