FixNeeds Refinement
Toggle - Segmented Control Component link

A 2-option segmented control where the selected segment is filled brand-blue and the unselected segment carries a brand-blue outline.

Promote to a data-driven segmented control + add coverage states
Today's component locks in two segments via two manual variants (selected=first / selected=second). That doesn't scale to 3+ options and forces consumers to detach for any non-binary case. Recommendation: ship one EBSegmentedControl with a segments: [Segment] array and a selectedIndex prop. The visual treatment (filled-on-selected / outlined-on-unselected) stays. Also add Pressed, Focused, and Disabled state coverage — currently only Default exists.
In Context

Used in filters, list-view toggles ("List / Grid"), and binary-mode pickers ("Send / Receive", "Daily / Monthly"). Sits inline above a content area and switches what's rendered below.

Option 1 Option 2
Live Preview
Properties
Selected
DS Health
Reusable
Warn
Hard-locked to 2 segments. Any consumer needing 3+ segments must detach. A segments: [Segment] data prop would let one component cover every count.
Self-contained
Pass
Owns its own bg / border / label tokens. No external instance dependencies.
Consistent
Partial
Treatment is clear (filled selected, outlined unselected) but axis naming selected=first|second is positional. Should be selectedIndex: Int or selected: SegmentID in code.
Composable
Warn
Each segment's label is baked text. No slot for an icon-prefixed segment or a segment with a trailing count badge.
Behavior
State iOS Android Figma Property Notes
Tap unselected segment Yes Yes Switches selectedIndex Pressed state on the tapped segment isn't modeled — should preview the destination on touch-down, commit on touch-up.
Tap selected segment N/A N/A No-op Re-tapping the selected segment should be a no-op (or, optionally, reset to a default). Not spec'd.
Pressed N/A N/A Not modeled Need a transient pressed treatment (e.g. 8% darken on bg, segment scale 0.97) for tap feedback.
Focused (keyboard / a11y) N/A N/A Not modeled No visible focus ring. Important for accessible web embeds and Android TV.
Disabled N/A N/A Not modeled No disabled state spec'd — a control inside a form needs one.
Open Issues
  • Component locks 2 segments via positional variants. selected=first|second means consumers stuck with exactly two segments. List-view toggles, period pickers, and filter chips frequently want 3+ segments. Replace with segments: [Segment] + selectedIndex: Int so one component covers every count. C1 · Layer Structure & Naming
  • Positional naming in the selection axis. first / second are not semantic — they refer to position, not value. If the segments are reordered, the prop value's meaning shifts. Use selectedIndex: Int (or selected: Segment.ID) so the prop is stable. C2 · Variant & Property Naming
  • No Pressed / Focused / Disabled state. Only Default state is modeled. A toggle control inside a form needs disabled coverage; an accessible web embed needs a keyboard focus ring; tap targets need a transient pressed treatment. C5 · Interaction State Coverage
  • Segment label is baked text — no icon or count slot. Real-world segmented controls often need a leading icon ("⊞ Grid" / "≡ List") or a trailing count ("Unread (12)"). The component doesn't expose slots for either; consumers detach. C4 · Native Mappability
  • Equal-width 168 segments are hardcoded. Total 336 × 40. For 3-segment use cases the math breaks (112 each? 168/n?). A data-driven control would distribute available width across the segment count automatically. C1 · Layer Structure & Naming
  • Code Connect mappings not registered. Blocked on the data-driven restructure — once segments is an array, Code Connect can register a single EBSegmentedControl mapping instead of per-variant entries. C7 · Code Connect Linkability
Design Recommendations
  • Promote to a data-driven EBSegmentedControl. Target API: EBSegmentedControl(segments: [Segment], selectedIndex: Int, onChange: (Int) -> Void) where Segment = { id, label, icon?, badge? }. Covers 2-, 3-, 4-segment cases with no per-count variants in Figma. Property
  • Rename the selected axis to be data-stable. Use selectedIndex: Int (or a selected: SegmentID string) so the value's meaning doesn't shift if segments are reordered. Rename
  • Add Pressed, Focused, and Disabled state coverage. Pressed: subtle darken on bg + 0.97 scale on touch-down. Focused: 2px outline (offset 2) for keyboard nav. Disabled: 40% opacity on the whole row, pointer-events: none. State
  • Expose leading-icon + trailing-badge slots per segment. Common segmented-control patterns prefix segments with an icon ("⊞ Grid") or append a count badge ("Unread (12)"). Both need named slots so consumers don't detach. Slot
  • Distribute segment width evenly via flex. Don't hardcode 168 per segment. Use flex:1 on each so a 3-segment control divides 336 / 3 = 112 each automatically, with no Figma rework needed. Property
  • Document the A11y model. Role: tablist with each segment as tab, currently selected gets aria-selected="true". Keyboard: ←/→ to move, Space/Enter to commit. VoiceOver: "Tab, 1 of 2, selected". A11y
Types
Default
DES DEV

336 × 40 control with two 168-wide segments. Selected segment fills with brand-blue (#005CE5) and shows white label; the other segment carries a 1.5px brand-blue outline and a brand-blue label.

Properties
Selected
Properties
Selected First
Colors
Selected bg #005CE5
Selected label #FFFFFF
Unselected bg #FFFFFF
Unselected border #005CE5
Unselected label #005CE5
Layout
Control 336 × 40
Segment 168 × 40
Padding 12 vert · 16 horiz
Border radius 6
Border width 1.5 (unselected only)
Typography
Label Proxima Soft Bold · 16 / 16 · +0.25
Colors by Selection

Selected segment fills with brand-blue; unselected segment carries an outline of the same brand-blue.

Role Token SelectedUnselected
Background toggle-segmented-control/color/{state}/bg #005CE5 #FFFFFF
Border toggle-segmented-control/color/{state}/border none #005CE5
Label toggle-segmented-control/color/{state}/label #FFFFFF #005CE5
Property Mapping

Today's 2-variant axis maps to a single data-driven control once promoted. The table reflects the proposed shape.

Figma PropertySwiftUICompose
selected=first / second selectedIndex: Int selectedIndex: Int
(implicit 2 segments) segments: [Segment] segments: List
segment label Segment.label: String Segment.label: String
(no leading icon today) Segment.icon: Image? Segment.icon: ImageVector?
(no count badge today) Segment.badge: Int? Segment.badge: Int?
(no callback today) onSelectionChange: (Int) -> Void onSelectionChange: (Int) -> Unit
SwiftUI
ios/Components/SegmentedControl/EBSegmentedControl.swift
Jetpack Compose
android/components/segmentedcontrol/EBSegmentedControl.kt
Accessibility
RequirementiOSAndroid
Role Use a custom UISegmentedControl-style trait. Each segment is a .button with .isSelected trait set on the active one. Apply Role.RadioButton on each segment + Modifier.selectableGroup on the row.
Selection announce Use accessibilityValue = "Option 1, 1 of 2, selected". Avoid relying on visual color alone. Use contentDescription = "Option 1, 1 of 2, selected".
Keyboard / focus ←/→ arrows move selection; Space/Enter commits. Visible focus ring is mandatory in non-touch contexts. DPAD ←/→ moves selection; Enter commits. Use Modifier.focusable() with a visible indicator.
Disabled Set .disabled(true) on the row; segments are not focusable. Set enabled = false; segments not focusable; opacity 0.4.
Tap target ≥ 44 × 44 hit area per segment. With segments at 168 × 40, vertical hit area needs 4 px extension via .contentShape. ≥ 48 dp touch target. Use Modifier.minimumInteractiveComponentSize() when segments fall below.
Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Requires Rework Two manual variants for what should be a data-driven segments array.
C2 Variant & Property Naming Needs Refinement selected=first|second is positional. Use selectedIndex: Int.
C3 Token Coverage Needs Refinement Hex values consistent but no toggle-segmented-control/* token namespace registered.
C4 Native Mappability Needs Refinement No leading-icon or count-badge slots per segment. Common segmented-control patterns can't be expressed.
C5 Interaction State Coverage Requires Rework Only Default. No Pressed / Focused / Disabled.
C6 Asset & Icon Quality Not Applicable No assets in v1 (text only). When icon slot lands, must accept vector instances.
C7 Code Connect Linkability Not Mapped Blocked until data-driven restructure lands.
Variants Inventory (2 total)

Two variants on a single selected axis (first | second). Same visual treatment; only which segment is filled differs.

#selectedSizeNode
1first336 × 4027:30930
2second336 × 4027:30935
1.0.0 — 2026-05-19Major
Initial Assessment · node 27:30929
Component assessed — 2 variants on selected axis. Used in list-view toggles, period pickers, filter chips. Documented
Initial
Verdict: Fix — Promote to data-driven EBSegmentedControl with segments array + selectedIndex prop. Add Pressed / Focused / Disabled state coverage. Open
Family
C1 — Hardcoded segment count — Two manual variants lock the count at 2. Replace with segments: [Segment] array. Open
C1
C2 — Positional namingselected=first|second not stable under reorder. Use selectedIndex: Int. Open
C2
C5 — Missing states — No Pressed, Focused, or Disabled state spec'd. Open
C5
C7 — Code Connect — Not registered. Blocked on restructure. Open
C7