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.
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.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.
_space_12 annotation instance that used to shim it has been removed, so the row maps to native padding rather than a spacer view."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.Size=Large nests the 24 × 24 atom) instead of always instancing the small radio.| 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. |
- Code Connect mappings not registered. Previously blocked by the
sizeprop 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:
sizeno longer encodes state — the compound"default - error"/"large - error"strings are gone, replaced by orthogonalSize(Large/Medium/Small) ×State(Default/Pressed/Disabled) ×isErrorprops. Native enum mapping is now clean. (C2) - v2.0: Radio size now pairs to label size —
Size=Largenests 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, andisError, includingDisabled + isSelected=falseso a disabled radio group renders its unselected rows. 39 variants total. (C5) - v2.0:
_space_12spacer instance removed — the 12px gap is now real auto-layout spacing. The annotation instance (a#0500FFfill 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-ofssetrenamed toicon-offsetacross 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 labelledisError=truewhile painted pressed-navy, which also leftCheck + Pressed + isError=falsenon-existent. Renamed toisError=false; label and paint now agree and the wrapper matches the atom. (C5) - v2.0:
Disableddeliberately excludesisError— 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)
- 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 nestedEBRadioButton. Docs
- Split the
sizeprop. v2.0: Applied — and taken further than proposed.Size×State×isError×isSelected×Style, with pressed folded intoState. The compound string values are gone. Property - Pair radio size to label size. v2.0: Applied —
Size=Largenests 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, andisError, 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
Small radio + 14 / 16 label. Default unselected state.
| 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 |
16 / 20 label (Proxima Soft Semibold). Still uses the 16 × 16 radio — should be 20 × 20.
| 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 size with red radio border. Label text color unchanged.
Large size with red radio border.
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") }
| Figma Property | SwiftUI | Compose |
|---|---|---|
| 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 |
// 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 )
| Requirement | iOS | Android |
|---|---|---|
| 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 |
| 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. |
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).
| Style | State | isSelected | isError | Count | Notes |
|---|---|---|---|---|---|
| Default | Default | false | false | 3 | Empty ring + label |
| Default | Default | true | false | 3 | Brand ring + dot, brand label |
| Default | Default | false | true | 3 | Error ring #D61B2C |
| Default | Default | true | true | 3 | Error ring + dot |
| Default | Pressed | false | false | 3 | Pressed ring #2340A9, navy label |
| Default | Pressed | true | false | 3 | Pressed ring + dot |
| Default | Pressed | false | true | 3 | Pressed error ring |
| Default | Pressed | true | true | 3 | Pressed error ring + dot |
| Default | Disabled | false | false | 3 | Muted empty ring + muted label |
| Default | Disabled | true | false | 3 | Muted ring + dot |
| Check | Default | true | false | 3 | Brand fill + vector checkmark |
| Check | Pressed | true | false | 3 | Pressed fill + checkmark |
| Check | Disabled | true | false | 3 | Muted fill + checkmark |
Style × State × Size × isSelected × isError, mirroring the Radio Button atom exactly (same props, same order, same coverage).
Restructuredsize no longer encodes state — the compound "default - error" / "large - error" values are replaced by orthogonal Size × State × isError props. Native enum mapping is clean.
ResolvedSize=Large nests the 24 × 24 atom instead of hardcoding the 16 × 16 small radio into every variant.
ResolvedisSelected, Pressed, Disabled, and isError, including Disabled + isSelected=false so disabled groups render their unselected rows.
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.
Resolvedicon-ofsset → icon-offset across all 39 variants.
Resolved26184: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.
ResolvedDisabled + isError=true not covered — same gap as the atom. Likely intentional, but undocumented. Confirm and record.
Opensize prop encodes state — "default - error" and "large - error" mix size + state. Should split. Open