East Blue
KeepReady
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.

Keep — in sync with the atom
All four DS Health traits pass and nothing is open. Style, State, Size, isSelected and isError mirror the Radio Button atom across all 39 variants, with the radio paired to the label size.
In Context

How the radio button with label appears in real product screens — each Yes / No answer on Fill Information, and each End option on a recurring payment, is a radio with its label.

Radio Button with Label rows on two GCash screens: Yes and No for the PEP and beneficial ownership questions on Fill Information, and On a specific date, After a number of payments and No end date on a recurring payment
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
  • 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
  • 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

Component Playground

Layout

Typography

Colors

Installation Planned API

iOS — Swift Package Manager

.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")

Android — Gradle (Kotlin DSL)

implementation("com.eastblue.ds:radio:1.0.0")

Import

import EastBlueDS
import com.eastblue.ds.radio.*
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
Usage Guidelines

Do

Use it when people pick one option from a short list they should see all at once.

Don't

Don’t use it when people can pick more than one — use Checkbox Item.

Do

Write the label as the option itself — “Pay with GCash”, “Yes”, “No”.

Don't

Don’t put helper text in the label; add a Subtext Message under the group instead.

Do

Keep one Size and one Style across a group.

Don't

Don’t mix Sizes or Styles within a single group of options.

Do

Make the whole row — radio and label — the tap target.

Don't

Don’t leave only the radio tappable; at 16–24px it is too small on its own.

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.
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
View full Style × State × Size × isSelected × isError breakdown (39 rows)
StyleStateSizeisSelectedisErrorNode
DefaultDisabledLargefalsefalse26184:2713
DefaultDisabledMediumfalsefalse26184:2719
DefaultDisabledSmallfalsefalse26224:4298
DefaultPressedSmallfalsefalse26184:2725
DefaultDefaultLargefalsefalse26184:2731
DefaultPressedLargefalsefalse26219:4049
DefaultDefaultMediumfalsefalse26184:2737
DefaultPressedMediumfalsefalse26219:4078
DefaultDefaultSmallfalsefalse26184:2743
DefaultDisabledLargetruefalse26184:2749
DefaultDisabledMediumtruefalse26184:2755
DefaultDisabledSmalltruefalse26184:2761
CheckDisabledLargetruefalse26184:2767
CheckDisabledMediumtruefalse26184:2773
CheckDisabledSmalltruefalse26184:2779
DefaultDefaultLargetruefalse26184:2785
DefaultDefaultMediumtruefalse26184:2791
DefaultDefaultSmalltruefalse26184:2797
DefaultPressedLargetruefalse26184:2803
DefaultPressedMediumtruefalse26184:2809
DefaultPressedSmalltruefalse26184:2815
CheckDefaultLargetruefalse26184:2821
CheckDefaultMediumtruefalse26184:2827
CheckDefaultSmalltruefalse26184:2833
CheckPressedLargetruefalse26184:2839
CheckPressedMediumtruefalse26184:2845
CheckPressedSmalltruefalse26184:2851
DefaultDefaultLargefalsetrue26184:2857
DefaultDefaultMediumfalsetrue26184:2863
DefaultDefaultSmallfalsetrue26184:2869
DefaultPressedLargefalsetrue26184:2875
DefaultPressedMediumfalsetrue26184:2881
DefaultPressedSmallfalsetrue26184:2887
DefaultDefaultLargetruetrue26184:2893
DefaultDefaultMediumtruetrue26184:2899
DefaultDefaultSmalltruetrue26184:2905
DefaultPressedLargetruetrue26184:2911
DefaultPressedMediumtruetrue26184:2917
DefaultPressedSmalltruetrue26184:2923
2.0.1 — September 2026Patch
Documentation backfill · node 26184:2712
Resolved issues recorded. 9 resolved issues on the Overview tab had no changelog row: 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; 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; 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; v2.0: _space_12 spacer instance removed — the 12px gap is now real auto-layout spacing; v2.0: Layer typo fixed — icon-ofsset renamed to icon-offset across all 39 variants; v2.0: Schema aligned with the Radio Button atom — same five props, same property order, same 39-variant coverage; 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; v2.0: Disabled deliberately excludes isError — confirmed intentional, matching the atom; 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.
Docs
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 size — Size=Large nests the 24 × 24 atom instead of hardcoding the 16 × 16 small radio into every variant. Resolved
C6 Resolved
Full state coverage added — isSelected, 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 fixed — icon-ofsset → icon-offset across all 39 variants. Resolved
C1 Resolved
Check + Pressed mislabel corrected — 26184: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