One selectable row inside a Select Group — a leading element, a label with optional supporting text, and an optional trailing badge.
State axis, the flag is a vector from the Flags Library, and Type reads Icon, PesoSignVector, Flag and PesoSignText.How the select item appears in real product screens — country rows with flags in the Express Send country code list, and peso amount rows in a GLoan loan amount list with the chosen amount highlighted.

State is orthogonal to Type after the rebuild, the disabeld spelling is gone, and the multi-word values are joined — PesoSignVector and PesoSignText, matching the same values on Select.⤷ SelectionSlot. Leading, content and trailing are separate components, so the row composes rather than redrawing their internals.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| State=Default | N/A | N/A | row #FFFFFF | Label #0A2757. The row the group ships six of. |
| State=Pressed | N/A | N/A | row #F6F9FD | The row tints; the label colour does not change. |
| State=Disabled | N/A | N/A | row #FFFFFF | Label drops to #C2CFE5. The row keeps its white background rather than tinting. |
| isSelected=true | N/A | N/A | label #005CE5 | Label and leading icon turn brand blue. The row background stays white — selection is carried by colour, not by a fill. |
| Density | N/A | N/A | 40 / 48 / 56 | Compact, Default and Comfortable change the row height and its vertical padding. The content block itself is the same in all three. |
| Supporting Text | N/A | N/A | hidden by default | A second line under the label. The row hugs, so enabling it grows Comfortable from 56 to 60. |
- The disabeld typo is gone. The old build shipped a misspelled enum value, and the documentation carried both
type:disabeldandtype:disabledkeys defensively so the spec table would render either way. Disabled is now a value on theStateaxis, spelled correctly, and the defensive duplicate is no longer needed. C2 · Variant & Property Naming - Disabled is a state, not a type. It used to be a value on
type, which meant a disabled row could not also be an icon row or a flag row — the two ideas were competing for one axis.State = Default | Pressed | Disabledis now orthogonal toType, so every leading type has all three. C4 · Native Mappability - The flag is a real vector. The Country variant drew its flag from a raster PNG. It is now a
Flags Library - 16pxinstance — the exported SVG is paths, masks and a boolean operation with no<image>element anywhere in it, so it scales and recolours like every other asset in the system. C6 · Asset & Icon Quality - Pressed exists now. There was no pressed treatment at all, on a row whose entire purpose is being tapped. Every Type and Density now has
State=Pressed, tinting the row to#F6F9FD. C5 · Interaction State Coverage - The multi-word values are joined.
Peso SignandText - Peso Signcarried spaces and a hyphen separator. They are nowPesoSignVectorandPesoSignText— renamed for what they actually are rather than how they were first described, and matching the same two values on Select. C2 · Variant & Property Naming - The sparse State × isSelected grid is deliberate. Four of the six combinations exist per Type and Density: Default with either
isSelected, plus Pressed and Disabled unselected. Selected-and-pressed and selected-and-disabled are intentionally left out, which is why the set is 48 variants rather than 72. C5 · Interaction State Coverage - Type stays a fixed enum rather than a slot. All four values differ only in what sits in the 24 × 24 leading element, and that element is already an instance — so promoting it to a slot would collapse the set from 48 variants to 12 and let any future leading content in without a fifth value. Kept as an enum on purpose: fixed choices are easier for less experienced designers to use correctly than an open slot. Carried as a recommendation rather than a fault, for whenever flexibility matters more than guardrails. C2 · Variant & Property Naming
- Promote the leading element to a slot when guardrails matter less.
Typeexists only to enumerate what goes in the 24 × 24 leading box, and that box is already an instance. Turning it into⤷ LeadingSlotwould take the set from 48 variants to 12 and absorb any future leading content — a flag for a new market, a merchant logo, an avatar — without a fifth enum value. The trade is discoverability: a slot gives a designer no menu to pick from. Worth revisiting once the team is comfortable with the slot pattern, which is the same move that worked on List Item - Asset. Slot - Mute Supporting Text in the disabled state. Primary Text drops to
#C2CFE5when the row is disabled, but Supporting Text stays at#6780A9in every state. In a disabled row that leaves the supporting line noticeably darker than the label it supports, which inverts their hierarchy. It only shows when Supporting Text is switched on, so this is latent rather than visible today. Token - Check Supporting Text's contrast before shipping it.
#6780A9on white is 4.01:1. At 12px SemiBold that counts as normal text under WCAG, which needs 4.5:1 — so the supporting line falls just short. A step darker would clear it. Worth settling before the line is used, rather than after it appears in a screen. A11y - Pass the icon-grid scaffolding upstream. The leading icon instances nest a
Gridtemplate carryingGuide lines,KeyshapesandTrim area. They are hidden and do not render, so this is not a fault here, but they ship inside every instance and belong to the icon library rather than to this row. Docs - Document when to use PesoSignVector versus PesoSignText. The two render almost identically at 16px — one is a custom SVG drawn to match the font, the other is Proxima's own ₱ glyph. The distinction is real and deliberate, but a designer picking from a dropdown cannot see it. One sentence on the component saves a coin-flip. Docs
- See siblings: Select Group is the surface these rows fill, and Select is the control that opens it. Select Field is the trigger, deliberately outside this family's scope. Family
Component Playground
Layout
VStack
Typography
Colors
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:select:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.select.*
Six properties, in the order the Figma property panel lists them. Three native parameters have no row: label and supporting are the two text layers inside Content Element, and the trailing content is a nested Badge instance — none is a component property, so Figma has nothing to bind them to. State is the one property that is not a parameter either: Pressed is what the platform already does while a finger is down, and Disabled is each platform’s own idiom rather than an enum case. Type chooses the leading mark, and each mark brings its own width — 24, 16, 15 or 10 — which is deliberate; the row hugs whatever it is given rather than reserving a fixed slot.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| Type — Icon, PesoSignVector, Flag, PesoSignText | type: EBSelectItemType | type: EBSelectItemType |
| Density — Compact, Default, Comfortable | density: EBDensity = .compact | density: EBDensity = Compact |
| State — Default, Pressed, Disabled | not a parameter — Pressed is the press itself; Disabled is .disabled(true) | not a parameter — Disabled is enabled = false |
| isSelected — true, false | isSelected: Bool = false | isSelected: Boolean = false |
| hasLeading — true, false | hasLeading: Bool = true | hasLeading: Boolean = true |
| hasTrailing — true, false | hasTrailing: Bool = true | hasTrailing: Boolean = true |
EBSelectItem( label: "Notifications", type: .icon, density: .compact )
EBSelectItem(
label = "Notifications",
type = EBSelectItemType.Icon,
density = EBDensity.Compact
) EBSelectItem( label: "Send money", type: .pesoSignVector, density: .default, hasTrailing: false )
EBSelectItem(
label = "Send money",
type = EBSelectItemType.PesoSignVector,
density = EBDensity.Default,
hasTrailing = false
) EBSelectItem( label: "Philippines", type: .flag, density: .default )
EBSelectItem(
label = "Philippines",
type = EBSelectItemType.Flag,
density = EBDensity.Default
) EBSelectItem( label: "Cash in", type: .pesoSignText, density: .comfortable, isSelected: true )
EBSelectItem(
label = "Cash in",
type = EBSelectItemType.PesoSignText,
density = EBDensity.Comfortable,
isSelected = true
) | Requirement | iOS | Android |
|---|---|---|
| Exposed as a selectable option | .accessibilityAddTraits(.isButton) plus .isSelected when chosen | Modifier.selectable(selected = …, role = Role.RadioButton) |
| Selection is not colour-only | Selected state announced, not just rendered in brand blue | selected = true carries into the node's state description |
| Supporting text reads with the label | .accessibilityElement(children: .combine) | Modifier.semantics(mergeDescendants = true) |
| Leading element is decorative | .accessibilityHidden(true) — except Flag, whose country belongs in the label | contentDescription = null — same exception |
| Row meets the touch minimum | Compact is 40 — needs 44 of hit area even where the row draws shorter | Compact is 40 — needs 48dp |
Do
Pick one Density for a whole list.
Don't
Don't mix densities inside one Select Group — the rows stop scanning as a column.
Do
Use Flag when the option is a country and the flag identifies it.
Don't
Don't rely on the flag alone — the country name belongs in the label for screen readers.
Do
Keep the label short enough to sit on one line.
Don't
Don't push detail into the label when Supporting Text is the place for it.
Do
Let the group own the dividers between rows.
Don't
Don't add a divider inside the row — Select Group's BorderType already places them.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Leading, Content and Trailing are each their own component. The row draws none of their internals. |
| C2 | Variant & Property Naming | Ready | The disabeld typo is gone, the multi-word values are joined, and Type is a deliberate enum rather than a slot. |
| C3 | Token Coverage | Ready | Every colour is bound and named: bg/color-bg-main and bg/color-bg on the row, text/color-text stepping to -disabled and -primary on the label, bg/color-bg-inverse on the leading mark — which tracks the label rather than staying fixed — and the badge’s bg/color-bg-secondary with text/color-text-primary stepping to -primary-disabled. Supporting Text is bound to text/color-text-weaker but ships hidden, so its 4.01:1 on white is not a live contrast concern; it becomes one the day the layer is switched on. |
| C4 | Native Mappability | Ready | Four axes map onto a selectable list row on both platforms. State is the one that is not a parameter: Pressed is what the platform already does under a finger, and Disabled is each platform’s own idiom. The leading mark’s width varies by Type — 24, 16, 15 or 10 — which is deliberate, so the native row should hug what it is given rather than reserve a fixed slot. hasTrailing defaulting to true is deliberate too: the badge shows that the slot exists, and both consumers turn it off, so the native default should follow the primitive rather than the usage. The gap is that neither text layer nor the trailing instance is a Figma property. |
| C5 | Interaction State Coverage | Ready | Pressed now exists across every Type and Density. The two missing selected combinations are confirmed intentional. |
| C6 | Asset & Icon Quality | Ready | The raster flag is now a vector Flags Library instance. The icons nest hidden grid scaffolding, which belongs to the icon library. |
4 Type × 3 Density × 4 of the 6 State × isSelected combinations = 48. Selected-and-pressed and selected-and-disabled are intentionally omitted, which is what keeps it from being 72. hasLeading and hasTrailing are boolean properties, so they switch the two nested instances without multiplying the set.
| Type | Leading element | Width | State × isSelected | Heights | Count |
|---|---|---|---|---|---|
| Icon | A 24 × 24 icon instance | 24 | Default ×2, Pressed, Disabled | 40 / 48 / 56 | 12 |
| PesoSignVector | Drawn ₱ artwork — one bar through the P | 15 | Default ×2, Pressed, Disabled | 40 / 48 / 56 | 12 |
| Flag | Flags Library instance — a vector | 16 | Default ×2, Pressed, Disabled | 40 / 48 / 56 | 12 |
| PesoSignText | Proxima Soft’s own ₱, set as type | 10 | Default ×2, Pressed, Disabled | 40 / 48 / 56 | 12 |
View full Type × Density × State × isSelected breakdown (48 rows)
| Type | Density | State | isSelected | Node |
|---|---|---|---|---|
| Icon | Compact | Default | false | 7947:111970 |
| Icon | Compact | Default | true | 7947:111974 |
| Icon | Compact | Pressed | false | 7947:111978 |
| Icon | Compact | Disabled | false | 7947:111982 |
| Icon | Default | Default | false | 7947:111986 |
| Icon | Default | Default | true | 7947:111990 |
| Icon | Default | Pressed | false | 7947:111994 |
| Icon | Default | Disabled | false | 7947:111998 |
| Icon | Comfortable | Default | false | 7947:112002 |
| Icon | Comfortable | Default | true | 7947:112006 |
| Icon | Comfortable | Pressed | false | 7947:112010 |
| Icon | Comfortable | Disabled | false | 7947:112014 |
| PesoSignVector | Compact | Default | false | 7947:112018 |
| Flag | Compact | Default | false | 7947:112022 |
| PesoSignText | Compact | Default | false | 7947:120575 |
| PesoSignVector | Compact | Pressed | false | 7947:112026 |
| Flag | Compact | Pressed | false | 7947:112030 |
| PesoSignText | Compact | Pressed | false | 7947:121075 |
| PesoSignVector | Compact | Disabled | false | 7947:112034 |
| Flag | Compact | Disabled | false | 7947:112038 |
| PesoSignText | Compact | Disabled | false | 7947:120607 |
| PesoSignVector | Default | Default | false | 7947:112042 |
| Flag | Default | Default | false | 7947:112046 |
| PesoSignText | Default | Default | false | 7947:120579 |
| PesoSignVector | Default | Pressed | false | 7947:112050 |
| Flag | Default | Pressed | false | 7947:112054 |
| PesoSignText | Default | Pressed | false | 7947:121071 |
| PesoSignVector | Default | Disabled | false | 7947:112058 |
| Flag | Default | Disabled | false | 7947:112062 |
| PesoSignText | Default | Disabled | false | 7947:120611 |
| PesoSignVector | Comfortable | Default | false | 7947:112066 |
| Flag | Comfortable | Default | false | 7947:112070 |
| PesoSignText | Comfortable | Default | false | 7947:120583 |
| PesoSignVector | Comfortable | Pressed | false | 7947:112074 |
| Flag | Comfortable | Pressed | false | 7947:112078 |
| PesoSignText | Comfortable | Pressed | false | 7947:121067 |
| PesoSignVector | Comfortable | Disabled | false | 7947:112082 |
| Flag | Comfortable | Disabled | false | 7947:112086 |
| PesoSignText | Comfortable | Disabled | false | 7947:120615 |
| PesoSignVector | Compact | Default | true | 7947:112090 |
| Flag | Compact | Default | true | 7947:112094 |
| PesoSignText | Compact | Default | true | 7947:120587 |
| PesoSignVector | Default | Default | true | 7947:112098 |
| Flag | Default | Default | true | 7947:112102 |
| PesoSignText | Default | Default | true | 7947:120591 |
| PesoSignVector | Comfortable | Default | true | 7947:112106 |
| Flag | Comfortable | Default | true | 7947:112110 |
| PesoSignText | Comfortable | Default | true | 7947:120595 |
.pesosignvector and .pesosigntext where Swift lowerCamels a multi-word case. The page was self-consistent, so nothing on it failed, but it left this component the odd one out in a family where Select and Select Group both lowerCamel — the kind of drift each page validates clean and only a family read catches. Now .pesoSignVector and .pesoSignText on both tabs; Compose still takes the Figma value verbatim. The Style cards are generated from getSnippet now rather than hand-written, so the two tabs cannot drift apart again.Density to EBSelectItemDensity while Select Group mapped the identical three values to EBDensity. Neither page was wrong on its own, which is why no check caught it — it only shows when the two are read together. It matters because the group’s density has to reach its rows: in Figma that is what makes the group 288 tall at Compact and 400 at Comfortable, so natively the group passes density down. Two enums with identical cases would need a conversion at that hand-off, and would drift the day a case is added to one and not the other. Unified to EBDensity, which Select will need as a third consumer. EBSelectItemType keeps its prefix: Icon, PesoSignVector, Flag and PesoSignText mean nothing outside this component.hasTrailing defaults to true on purpose, and the page now says why. Both consumers — Select Group and Select — set it false, so no row in a real list shows a badge. That reads like a wrong default until you know the intent: the primitive shows the badge so the trailing slot is visible at a glance, and consumers turn it off. Recorded in the Style tab’s description, on every card’s Properties row and in C4, because the reasoning is not recoverable from the file and the next reviewer would otherwise file it as a finding.supporting and badge from before the restructure, while the reader had moved to hasLeading and hasTrailing — so both were inert and neither reported an error. The panel now mirrors Figma’s six properties in order. The generator that writes it fails the build if the panel declares a control the script does not read, or the script reads one the panel does not declare.Type is the driving property, so Icon, PesoSignVector, Flag and PesoSignText each get a card. The other three axes — Density, State, isSelected — are controls on every card rather than cards of their own, which is what keeps four cards describing 48 variants.hasLeading and hasTrailing, both defaulting to True; neither was on the page. Nothing changed in Figma — get_node_info cannot read property definitions.₱ character for both peso versions, with a note arguing the difference was invisible at that size. It is not: the vector is 15 wide against the text glyph’s 10, with a single bar through the P where Proxima draws two. The real path is now in the preview, exported from node 7947:112019 and drawn in currentColor. The two versions exist precisely because they are not interchangeable, and the preview had been erasing the distinction it was there to document.#0A2757, #C2CFE5 disabled and #005CE5 selected. The row now carries the same variants Primary Text does. The previous page said "follows the label colour", which was right — this pass had replaced a true qualitative note with a precise wrong one before catching it.padding: 0 12px, with the heights faked by min-height, so it looked right and described the component wrongly.Primary Text is Primary/Multi-line Label/Light/Base and Supporting Text is .../Fine, replacing three font specs and a "names pending Dev Mode read" IOU.bg/color-bg when pressed; Primary Text and the leading mark step through text/color-text, -disabled and -primary; the badge label steps to -primary-disabled. The badge fill is bg/color-bg-secondary at #E5F1FF — comparing that against Countdown, which had the same token recorded at #EEF2F9, is what surfaced a wrong token name on that page.supporting toggle Figma does not have. The layer is documented — bound to text/color-text-weaker, with its own text style — and marked as shipping hidden, so nothing on the page currently draws it..eb-preview-sitem declared font-family: inherit, which resolves to BarkAda; both text layers are Primary/* and are Proxima Soft in Figma. The root now names Proxima Soft. This is the first component caught by the typeface check rather than by eye.gcash/east-blue-ios and com.gcash.eastblue:components:1.0.0, with no Import line. It now cites the Select family artifact com.eastblue.ds:select:1.0.0 and imports com.eastblue.ds.select.*.hasLeading and hasTrailing had no rows; Primary Text, Supporting Text and Trailing Element are two text layers and a nested instance. Seven rows became six, one per panel property.Type mapped to leading: EBSelectLeading against the Style tab’s type: EBSelectItemType; Density had a SwiftUI modifier against a Compose parameter; and State was "not a parameter" in the mapping while the Style tab emitted state: .disabled. More disagreements than any other component in this run of reviews.State is not a parameter, and the Style tab moved to match. The mapping had it right: Pressed is what the platform already does under a finger, and Disabled is each platform’s own idiom — .disabled(true) in SwiftUI, enabled = false in Compose. Third component in the system where a Figma State axis exists only because a designer has to pick one to see it, after Countdown and Select Item’s own sibling.Type value now, four in total, with density, selection and hasTrailing demonstrated across them.Type, so the row hugs what it is given rather than reserving a fixed slot; and the two peso versions coexist while a transition finishes, with one to be deprecated later. Both are confirmed intentional and noted in C4 and the mapping description so a later reviewer does not file them as findings.Primary Text, Supporting Text and the trailing Badge are layers and a nested instance rather than component properties, so Code Connect will have no anchor for the row’s two strings or its trailing content. The six properties that do exist all trace 1:1. Recorded against C7, which was already open on registration.getSnippet, so both language tabs were frozen on a static string; they now produce four distinct calls and respect the sparse grid — isSelected is only emitted alongside State=Default.disabeld typo is gone. Disabled is now a correctly spelled value on the State axis, so the documentation no longer needs to carry both spellings defensively.type value to an orthogonal State. Every leading type now has Default, Pressed and Disabled instead of the two ideas competing for one axis.Flags Library - 16px instance — the exported SVG contains no image element.#F6F9FD.Peso Sign and Text - Peso Sign renamed to PesoSignVector and PesoSignText — spaces and the hyphen separator removed, and named for what they are rather than how they were first described.18577:13033 (Sticker Sheets v2) to 7947:111969 (2026 Working File), and navGroup changed from Dropdown to Select to match the component's name.isSelected collapsed to two values — the ambiguous third Default value was removed; the prop now applies uniformly across Icon, Peso Sign, and Flag. Coverage is consistent at 9 × unselected + 3 × selected per Type, and selection maps 1:1 to a native Bool.
ResolvedisSelected values renamed Yes/No → true/false across all 36 variants. Select Item now matches the documented C2 rule and the rebuilt Radio Button; the system is on a single boolean vocabulary.
Resolveddisabeld resolved — disabled is now a correctly-spelled orthogonal State value, not a misspelled content type. Composes with any Type (e.g. Peso Sign + Disabled), resolving the earlier C2 + C4 issues.
ResolvedFlags Library - 16px in the Leading slot, locale-swappable rather than a baked-in Philippines PNG.
ResolvedState=Pressed covers touch feedback (focused is N/A on mobile), and isSelected=Yes flips the label to brand color plus exposes a checkmark via the Trailing icon slot.
ResolvedPeso Sign - Proxima) with the value in Content; the trailing slot stays free for a badge or value.
ResolvedisSelected exposes three values (No / Yes / Default) — ambiguous for a boolean and inconsistent across Types. Collapse to Yes/No.
Opendisabeld misspelled — ships into the generated TS type. Rename to disabled.
Openselected on/off and a pseudo-disabled content type. Touch/keyboard feedback unmodeled.
Opentype value — collides with content types (text / amount / country). Should be an orthogonal disabled axis.
Open