KeepNeeds Refinement
Radio Button with Label Component link

A form row pairing a Radio Button with a text label. 39 variants across Style (Default/Check) × State (Default/Pressed/Disabled) × Size (Large/Medium/Small) × isSelected × isError — mirroring the Radio Button atom exactly.

Rebuilt — in sync with the atom
The v2.0 rebuild split the compound size values into orthogonal Size × State × isError props, paired the radio size to the label size, added full state coverage including Disabled + isSelected=false, removed the _space_12 spacer in favour of a real auto-layout gap, and fixed the icon-ofsset typo. Schema, property order, and all 39 variants now mirror the Radio Button atom. Only Code Connect registration remains.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. Radio Button with Label appears in form questions and preference settings, stacked vertically as a group.

Continue
Live Preview
Label
Proposed API
size
isError
selected
DS Health
Reusable
Pass
Used for mutually exclusive form choices: payment method, shipping option, preference questions.
Self-contained
Pass
Carries its own gap, vertical padding, and typography, all token-bound. The 12px gap is real auto-layout spacing — the _space_12 annotation instance that used to shim it has been removed, so the row maps to native padding rather than a spacer view.
Consistent
Pass
The compound "default - error" / "large - error" values are gone. Five orthogonal props — Style × State × Size × isSelected × isError — in the same order as the Radio Button atom, so native enum mapping is clean.
Composable
Pass
Wraps the Radio Button atom and mirrors its schema exactly — same props, same order, same 39 variants — so every atom state is reachable through the row. Radio size now pairs to label size (Size=Large nests the 24 × 24 atom) instead of always instancing the small radio.
Behavior
State iOS Android Figma Property Notes
Default (unselected) Yes Yes State=Default, isSelected=false Empty ring, label #445C85. Resting state for every unpicked row.
Selected Yes Yes State=Default, isSelected=true Brand ring + dot #005CE5. Exactly one row per group carries this.
Pressed Yes Yes State=Pressed Radio darkens to #2340A9 and the label to #0A2757. Derived at runtime from the touch interaction, not passed as a parameter.
Disabled Yes Yes State=Disabled Radio and label both mute. Ships at both isSelected values, so a disabled group renders its unselected rows.
Error Yes Yes isError=true Radio switches to #D61B2C; the label is unchanged. Orthogonal to State — combines with Default and Pressed.
Selected — check style Yes Yes Style=Check, isSelected=true Filled circle + vector checkmark. For single-select list rows; only meaningful when selected.
Issues
  • Code Connect mappings not registered. Previously blocked by the size prop split and missing state variants — both resolved in v2.0. Registration is now unblocked, but the SwiftUI / Compose mappings are not yet wired. C7 · Code Connect Linkability
  • v2.0: size no longer encodes state — the compound "default - error" / "large - error" strings are gone, replaced by orthogonal Size (Large/Medium/Small) × State (Default/Pressed/Disabled) × isError props. Native enum mapping is now clean. (C2)
  • v2.0: Radio size now pairs to label size — Size=Large nests the 24 × 24 atom instead of hardcoding the 16 × 16 small radio into every variant. (C6)
  • v2.0: Full state coverage added — isSelected, State=Pressed, State=Disabled, and isError, including Disabled + isSelected=false so a disabled radio group renders its unselected rows. 39 variants total. (C5)
  • v2.0: _space_12 spacer instance removed — the 12px gap is now real auto-layout spacing. The annotation instance (a #0500FF fill wrapping a Roboto "12" text node) no longer ships inside the component, and the layout maps to native padding rather than a spacer view. (C1/C4)
  • v2.0: Layer typo fixed — icon-ofsset renamed to icon-offset across all 39 variants. (C1)
  • v2.0: Schema aligned with the Radio Button atom — same five props, same property order, same 39-variant coverage. The wrapper can now express everything the atom can. (C2)
  • v2.0: Check + Pressed mislabel corrected — three Check variants (26184:2839 / 2845 / 2851) were labelled isError=true while painted pressed-navy, which also left Check + Pressed + isError=false non-existent. Renamed to isError=false; label and paint now agree and the wrapper matches the atom. (C5)
  • v2.0: Disabled deliberately excludes isError — confirmed intentional, matching the atom. A locked row offers no path to resolve a validation error, so the combination ships no variants by design. (C5)
  • v2.0: Nesting the radio as a plain INSTANCE rather than a Figma Slot reviewed and accepted — the mirrored 39-variant matrix already exposes every atom state through Style / State / Size / isSelected / isError, so a Slot would largely duplicate it. (C4)
Recommendations
  • Make the whole row tappable. The label should be part of the tap target, not just the radio — standard for form rows and a meaningful hit-area win on mobile. Runtime behaviour, so it can't be expressed in Figma; document it in Accessibility instead. A11y
  • Register Code Connect mapping to EBRadioButtonRow. With the prop split, size pairing, and state coverage all shipped, wire the Figma properties (Style, State, Size, isSelected, isError) 1:1 to the SwiftUI / Compose API — forwarding the same values down to the nested EBRadioButton. Docs
  • Split the size prop. v2.0: Applied — and taken further than proposed. Size × State × isError × isSelected × Style, with pressed folded into State. The compound string values are gone. Property
  • Pair radio size to label size. v2.0: Applied — Size=Large nests the 24 × 24 atom; each size pairs correctly instead of always instancing the small radio. Composition
  • Add disabled + selected variants. v2.0: Applied — full coverage across isSelected, Pressed, Disabled, and isError, including the disabled-unselected rows. State
  • Adopt a Figma Slot for the nested radio. v2.0: Reviewed and closed as not needed — the mirrored 39-variant matrix already exposes every atom state, so a Slot would largely duplicate the variant surface. Revisit only if consumers need to nest a radio the matrix can't express. Slot
Styles
Default
DES DEV

Small radio + 14 / 16 label. Default unselected state.

Label
Properties
size
isError
selected
Properties
Variant Default
State Default
Colors
Border #D7E0EF
Fill #005CE5
Label #445C85
Error border #D61B2C
Layout
Outer ring 24 × 24
Inner dot 12 × 12
Padding 12 vertical · 0 horizontal
Gap (radio ↔ label) 12px
Typography
Label style Primary/Multi-line Label/Light/Base
Label font Proxima Soft Semibold · 16 / 20 · +0.25
Typography
Role Token Spec
default Primary/Multi-line Label/Light/Small Proxima Soft Semibold · 14 / 16 · +0.25
large Primary/Multi-line Label/Light/Base Proxima Soft Semibold · 16 / 20 · +0.25
Large
DES DEV

16 / 20 label (Proxima Soft Semibold). Still uses the 16 × 16 radio — should be 20 × 20.

Label
Properties
size
isError
selected
Properties
Variant Large
State Default
Colors
Border #D7E0EF
Fill #005CE5
Label #445C85
Error border #D61B2C
Layout
Outer ring 24 × 24
Inner dot 12 × 12
Padding 12 vertical · 0 horizontal
Gap (radio ↔ label) 12px
Typography
Label style Primary/Multi-line Label/Light/Base
Label font Proxima Soft Semibold · 16 / 20 · +0.25
Layout
Role Token Value
Radio → label gap space/space-12 12px
Radio icon offset padding (top) space/space-4 4px
Text container padding (default) space/space-4 4px vertical
Text container padding (large) 3t / 5b
Default width (demo instance) 63px
Large width (demo instance) 69px
Default — error
DES DEV

Default size with red radio border. Label text color unchanged.

Label
Properties
size
isError
selected
Properties
Variant Default — error
State Error
Colors
Border #D7E0EF
Fill #005CE5
Label #445C85
Error border #D61B2C
Layout
Outer ring 24 × 24
Inner dot 12 × 12
Padding 12 vertical · 0 horizontal
Gap (radio ↔ label) 12px
Typography
Label style Primary/Multi-line Label/Light/Base
Label font Proxima Soft Semibold · 16 / 20 · +0.25
Large — error
DES DEV

Large size with red radio border.

Label
Properties
size
isError
selected
Properties
Variant Large — error
State Error
Colors
Border #D7E0EF
Fill #005CE5
Label #445C85
Error border #D61B2C
Layout
Outer ring 24 × 24
Inner dot 12 × 12
Padding 12 vertical · 0 horizontal
Gap (radio ↔ label) 12px
Typography
Label style Primary/Multi-line Label/Light/Base
Label font Proxima Soft Semibold · 16 / 20 · +0.25
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
size=default / large size: EBRadioSize .controlSize(.small/.large)
"- error" suffix isError: Bool .ebError(true)
selected: Bool selected: Bool
label: String title: String
disabled: Bool .disabled(true)
onToggle onChange: (Bool) -> Void
SwiftUI
ios/Components/Radio/EBRadioButtonWithLabel.swift
Jetpack Compose
android/components/radio/EBRadioButtonWithLabel.kt
Usage Snippets Planned API
Usage
// Default row
EBRadioButtonWithLabel("Pay with GCash", selected: $selection == .gcash) {
    selection = .gcash
}

// Large row with error
EBRadioButtonWithLabel("Option A", selected: isSelected)
    .controlSize(.large)
    .ebError(true)

// Disabled
EBRadioButtonWithLabel("Not available", selected: false)
    .disabled(true)
// Default row
EBRadioButtonWithLabel(
    label = "Pay with GCash",
    selected = selection == Option.Gcash,
    onCheckedChange = { selection = Option.Gcash }
)

// Large row with error
EBRadioButtonWithLabel(
    label = "Option A",
    selected = isSelected,
    onCheckedChange = { isSelected = it },
    size = EBRadioSize.Large,
    isError = true
)

// Disabled
EBRadioButtonWithLabel(
    label = "Not available",
    selected = false,
    onCheckedChange = { },
    enabled = false
)
Accessibility
RequirementiOSAndroid
Whole-row tap target Wrap the row in a Button or tap gesture — label must be tappable, not just the radio Apply Modifier.selectable(...) to the row
Group semantics Wrap options in .accessibilityElement(children: .contain) Use Modifier.selectableGroup() on parent
Role announcement Radio Button role inherited from the atom Role.RadioButton via selectable
Error announcement Include error state in the option's accessibility label Use semantics { error(...) }
Touch target size Row should be at least 44pt tall 48dp minimum
Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Ready Semantic names throughout: icon-offset (typo fixed in v2.0), text-container, #label. The _space_12 annotation shim was removed — the 12px gap is real auto-layout spacing.
C2 Variant & Property Naming Ready Five orthogonal props — Style × State × Size × isSelected × isError — in the same order as the atom. The compound "default - error" values are gone; booleans are lowercase true/false.
C3 Token Coverage Ready Label colors, gap, and padding all token-bound. The radio itself inherits the atom's token-bound vector fills and strokes.
C4 Native Mappability Ready Maps to an HStack / Row of radio + label. Auto-layout spacing translates to native padding — no spacer view to reproduce since _space_12 was removed.
C5 Interaction State Coverage Ready Full coverage: isSelected × State (Default / Pressed / Disabled) × isError, including Disabled + isSelected=false. Focused is N/A on mobile; Disabled + isError is a deliberate omission.
C6 Asset & Icon Quality Ready Radio size now pairs to label size — Size=Large nests the 24 × 24 atom instead of always instancing the 16 × 16 small radio.
C7 Code Connect Linkability Needs Refinement Unblocked — the C2 prop split and state coverage both landed. No CLI mappings registered yet; waiting on the native component to exist.
Variants Inventory (39 total)

Style (2) × State (3) × Size (3) × isSelected (2) × isError (2) = 72 theoretical. 39 ship — mirroring the Radio Button atom exactly. Check is only meaningful when selected, and Disabled deliberately excludes isError. Grouped below by Style × State; each row covers all three sizes (Large / Medium / Small).

StyleStateisSelectedisErrorCountNotes
DefaultDefaultfalsefalse3Empty ring + label
DefaultDefaulttruefalse3Brand ring + dot, brand label
DefaultDefaultfalsetrue3Error ring #D61B2C
DefaultDefaulttruetrue3Error ring + dot
DefaultPressedfalsefalse3Pressed ring #2340A9, navy label
DefaultPressedtruefalse3Pressed ring + dot
DefaultPressedfalsetrue3Pressed error ring
DefaultPressedtruetrue3Pressed error ring + dot
DefaultDisabledfalsefalse3Muted empty ring + muted label
DefaultDisabledtruefalse3Muted ring + dot
CheckDefaulttruefalse3Brand fill + vector checkmark
CheckPressedtruefalse3Pressed fill + checkmark
CheckDisabledtruefalse3Muted fill + checkmark
2.0.0 — July 2026Major
Rebuilt · node 26184:2712
Component rebuilt on a new node — 39 variants across Style × State × Size × isSelected × isError, mirroring the Radio Button atom exactly (same props, same order, same coverage). Restructured
Rebuild
size no longer encodes state — the compound "default - error" / "large - error" values are replaced by orthogonal Size × State × isError props. Native enum mapping is clean. Resolved
C2 Resolved
Radio size now pairs to label sizeSize=Large nests the 24 × 24 atom instead of hardcoding the 16 × 16 small radio into every variant. Resolved
C6 Resolved
Full state coverage addedisSelected, Pressed, Disabled, and isError, including Disabled + isSelected=false so disabled groups render their unselected rows. Resolved
C5 Resolved
_space_12 spacer removed — the 12px gap is now real auto-layout spacing. The annotation instance (#0500FF fill wrapping a Roboto "12" text node) no longer ships inside the component, and layout maps to native padding rather than a spacer view. Resolved
C1 · C4 Resolved
Layer typo fixedicon-ofsseticon-offset across all 39 variants. Resolved
C1 Resolved
Check + Pressed mislabel corrected26184:2839 / 2845 / 2851 were labelled isError=true while painted pressed-navy, leaving Check + Pressed + isError=false non-existent. Renamed to isError=false; the wrapper now matches the atom. Resolved
C5 Resolved
Disabled + isError=true not covered — same gap as the atom. 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:35673
Component assessed — 4 variants (default / large × error). Documented
Initial
size prop encodes state — "default - error" and "large - error" mix size + state. Should split. Open
C2 Open
Missing state variants — No disabled or selected. Open
C5 Open
Radio hardcoded to small — large label still uses 16 × 16 radio. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open