The menu surface a Select opens — a rounded card wrapping a slot of Select Items, with optional dividers between them.
BorderType and its values and the ⤷ SelectionSlot follow the naming guidelines, and the Scrollbar frame is intentional.How the select group appears in real product screens — the list of options that opens under an expanded Select (the GCredit limit on Account Details), next to a closed Select on Scheduled Transfer.

BorderType and its values are joined after this pass, the MIddle Inset typo is gone, and the slot carries the family's ⤷ prefix as ⤷ SelectionSlot.SLOT filled with Select Item instances, and consumed as a whole by Select. What it cannot do is apply its own properties to substituted content — see the resolved list.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| BorderType=MiddleInset | N/A | N/A | inset dividers | Dividers sit between rows, inset from both edges. The default the component ships with. |
| BorderType=FullWidth | N/A | N/A | edge-to-edge dividers | Same dividers, running the full width of the card. |
| BorderType=None | N/A | N/A | no dividers | The divider nodes are absent entirely, which is why these variants are 24 shorter than their bordered siblings. |
| Density | N/A | N/A | 312 / 368 / 424 | Sets the row height of the Select Items in the default content — 40, 48 and 56 respectively — which is what drives the card's height. |
| ⤷ SelectionSlot | N/A | N/A | 7 rows by default | Ships with seven Select Items and a hidden Label. Substituted content replaces all of it, dividers included. |
- The MIddle Inset typo is gone. The value carried a capital I in the middle of the word. It is now
MiddleInset— and joined at the same time, so the property readsBorderType = MiddleInset | FullWidth | None. C2 · Variant & Property Naming - The vestigial Dropdown Item - Last name is gone. The final row in the slot was still called
Dropdown Item - Last, left over from before the Dropdown to Select rename — and confusingly its instance had been renamed while the master had not, so the two disagreed depending on where you looked. It is now simplySelect Item: seven rows with six dividers between them, and no trailing divider to suppress. C1 · Layer Structure & Naming - The hidden 366px leftovers are gone. Five stale
Select Iteminstances sat in the slot at 366 × 52 with no children, all stacked at a single coordinate. They were invisible but shipped inside every variant. Removed. C1 · Layer Structure & Naming - The slot is named for what belongs in it. It was
Slot— the arrow prefix arrived first, then the name.⤷ SelectionSlotnow says what the container is for, rather than just marking it as a slot. C2 · Variant & Property Naming - Both properties shape the default content rather than being applied to the slot. Each of the nine variants contains its own hand-placed rows:
MiddleInsetandFullWidthinterleave sixHorizontal / Dividersiblings between seven Select Items, andNonesimply has no divider nodes at all. So a consumer who fills the slot with their own rows loses both the dividers and the density, because those lived in the default content they replaced. This is the same Figma limit the Slider hit — a component property cannot reach into a slot — and it is understood and already instructed to designers. Recorded here because natively both are list-level properties applied to whatever rows the list holds, which is a genuine divergence a developer needs told rather than left to infer. C4 · Native Mappability - The hidden Label is a subsection header. A 320 × 28 instance at the top of the slot, hidden in all nine variants. Confirmed as intentional: it acts as a divider or subsection heading for grouped option lists, and stays hidden by default because most lists are flat. C1 · Layer Structure & Naming
- The Scrollbar frame stays. A hand-drawn 12 × full-height rectangle inside the slot, hidden. Native platforms render their own scroll indicator, so this has no counterpart in code — kept deliberately as a design-time affordance, consistent with how the same call was made elsewhere in the system. C4 · Native Mappability
- Pass the Divider component's name upstream. The dividers are instances of a component published as
Horizontal / Divider. Two things are off, neither of them this component's to fix. The spaces around the slash are inconsistent with how the rest of the file namespaces —Primary/Label/Large,Flags Library - 16px— and the order is inverted: it produces a folder named Horizontal containing Divider, and presumably a Vertical folder containing another Divider, splitting one family across two folders named for the wrong thing.Divider/Horizontalwould group them. Docs - Write down what a slot swap costs. Because the dividers and the density live in the default content, replacing the slot's contents silently drops both. Designers have been briefed, but the constraint is invisible from the component — it belongs in the usage documentation where someone reaches for it, and in the native contract where it does not apply at all. Docs
- Give the Label a way to be switched on. It is a real subsection header sitting hidden in every variant. Revealing it currently means finding a hidden layer inside a slot. A boolean on the group — or shipping it as an option inside the slot's default content — would make it discoverable rather than folklore. Property
- See siblings: Select Item fills the slot, and Select is the control that opens this surface. Select Field is the trigger, deliberately outside this family's scope. Family
- Publish the token names once Dev Mode is read. v2.0.1: Applied — all four colours the group paints are named.
bg/color-bg-mainon the card,border/color-border-weakon both its hairline and the dividers, andborder/color-borderon the hidden scrollbar. Read against the rest of the system rather than from Dev Mode, which still returns variable IDs instead of names. Everything inside a row belongs to Select Item, which is why those are named rather than valued. Token
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.*
Three properties. Two native parameters have no row because neither is one: header would drive the Label layer at the top of the slot, and the scrollbar is a hidden frame — both ship hidden as options held in reserve, and on both platforms the scrollbar is drawn by the platform rather than by the component. The one thing that does not map cleanly is how the two properties reach the rows. In Figma BorderType and Density shape the slot’s default content; a component property cannot reach inside a slot, so replacing the rows leaves both properties with nothing to act on. Natively they apply to whatever fills the slot, which is the behaviour the page documents and the better one — but it means the Figma component and the native component differ in mechanism, not just in syntax.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| BorderType — None, MiddleInset, FullWidth | borderType: EBBorderType | borderType: EBBorderType |
| Density — Compact, Default, Comfortable | density: EBDensity = .compact | density: EBDensity = Compact |
| ⤷ SelectionSlot (slot) | @ViewBuilder content: () -> Content | content: @Composable ColumnScope.() -> Unit |
EBSelectGroup(borderType: .none) { EBSelectItem(label: "Savings") EBSelectItem(label: "Checking") }
EBSelectGroup(borderType = EBBorderType.None) {
EBSelectItem(label = "Savings")
EBSelectItem(label = "Checking")
} EBSelectGroup(borderType: .middleInset) { EBSelectItem(label: "Savings") EBSelectItem(label: "Checking") }
EBSelectGroup(borderType = EBBorderType.MiddleInset) {
EBSelectItem(label = "Savings")
EBSelectItem(label = "Checking")
} EBSelectGroup(borderType: .fullWidth, density: .comfortable) { EBSelectItem(label: "Philippines") EBSelectItem(label: "Singapore") }
EBSelectGroup(borderType = EBBorderType.FullWidth, density = EBDensity.Comfortable) {
EBSelectItem(label = "Philippines")
EBSelectItem(label = "Singapore")
} | Requirement | iOS | Android |
|---|---|---|
| Exposed as a list of options | .accessibilityElement(children: .contain) with a radio-group trait | Modifier.selectableGroup() on the column |
| Announced when it opens | Focus moves into the list; the Select's label carries the context | paneTitle on the popup |
| Dividers are decorative | .accessibilityHidden(true) | contentDescription = null |
| Subsection header reads as a heading | .accessibilityAddTraits(.isHeader) when the Label is shown | Modifier.semantics { heading() } |
| Long lists stay reachable | Scrolls to the selected row when opened | Same — the selected row is brought into view |
Do
Let the group own the dividers between rows.
Don't
Don't add dividers inside a Select Item — BorderType already places them.
Do
Re-apply density when you replace the slot's contents.
Don't
Don't assume swapped-in rows inherit the group's density — they don't, in Figma.
Do
Use None when the rows already read as separate cards.
Don't
Don't stack dividers against a row that draws its own border.
Do
Show the Label when a list has genuine sections.
Don't
Don't use it as a title for the whole menu — that belongs on the Select.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | The vestigial Dropdown Item - Last and the five hidden 366px leftovers are both gone. The hidden Label is confirmed as an intentional subsection header. |
| C2 | Variant & Property Naming | Ready | BorderType = MiddleInset | FullWidth | None after the typo fix and the joining, and the slot is ⤷ SelectionSlot. |
| C3 | Token Coverage | Ready | Every colour is bound and named: bg/color-bg-main on the card, border/color-border-weak on both its hairline and the dividers, and border/color-border on the hidden scrollbar. Everything inside a row belongs to Select Item, which is why the rows are named rather than valued here. |
| C4 | Native Mappability | Ready | A card wrapping a list maps directly. The one divergence is mechanical rather than cosmetic: in Figma BorderType and Density shape the slot’s default content, so replacing the rows leaves both with nothing to act on; natively they apply to whatever fills the slot. The native behaviour is the better one, and it is what the mapping documents — but the two components differ in mechanism, so the native binding will not be a straight pass-through. |
| C5 | Interaction State Coverage | Not Applicable | The surface has no states of its own. Pressed and selected belong to the rows inside it. |
| C6 | Asset & Icon Quality | Ready | No artwork of its own. The Label header and the Scrollbar are both hidden by default and confirmed intentional — options for when a group needs a heading or a list overflows, which most do not. |
3 BorderType × 3 Density = 9. Row count is not an axis — the slot ships seven and takes whatever replaces them.
| BorderType | Density | Row height | Card height | Node |
|---|---|---|---|---|
| MiddleInset | Compact | 40 | 312 | 7947:111631 |
| FullWidth | Compact | 40 | 312 | 7947:111659 |
| None | Compact | 40 | 288 | 7947:111687 |
| MiddleInset | Default | 48 | 368 | 7947:111709 |
| FullWidth | Default | 48 | 368 | 7947:111737 |
| None | Default | 48 | 344 | 7947:111765 |
| MiddleInset | Comfortable | 56 | 424 | 7947:111787 |
| FullWidth | Comfortable | 56 | 424 | 7947:111815 |
| None | Comfortable | 56 | 400 | 7947:111843 |
BorderType is the driving property, so None, MiddleInset and FullWidth each get a card. Density stays a control, and ⤷ SelectionSlot is a slot, which leaves the panel with one control for a component of nine variants.MiddleInset is a 320 × 4 frame holding a hairline from x=12 to x=308; FullWidth runs 0 to 320; None has no divider layers at all. That last one is the whole of the 24px height difference — six gaps × the 4px frame each divider sits in — which had read as an unexplained number.Width also read a bare 320 where the panel says Fill — 320 is only what it resolves to here.currentColor, so the selected row’s blue reaches it through one declaration instead of three brittle selectors. Select Item now draws the same block, so both pages render this nested instance identically.text/color-text-primary while the row keeps its surface.Primary/Multi-line Label/Light/Base and the hidden group header is Primary/Label/Light/Small, replacing font specs and a "names pending Dev Mode read" IOU.bg/color-bg-main on the card, border/color-border-weak on both its hairline and the dividers, border/color-border on the scrollbar. Everything inside a row belongs to Select Item, so the rows are named rather than valued — and a Colors by BorderType table now carries the lot.Label header and the Scrollbar both ship hidden — options for when a group needs a heading or a list overflows, which most do not. Written into the Properties rows, the colours table and C6, because a documented scrollbar colour that never appears reads as a defect otherwise..eb-preview-sgroup declared font-family: inherit, which resolves to BarkAda; every visible layer is a Select Item row and the hidden header is Primary/*, all Proxima Soft. Caught by the typeface check rather than by eye.MiddleInset and edge to edge at FullWidth. This is the same 24px that separates None from the other two, and the preview now reconstructs it rather than asserting it.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.*.Label (hidden) and Scrollbar are not in the panel. Three rows now, one per property, with header and the scrollbar explained in the description — the scrollbar because on both platforms it is drawn by the platform rather than by the component.EBSelectItem a way its own page does not. One used an unlabelled leading string, one used leading: — the parameter renamed to type: during Select Item’s own pass — and one passed header: to the group. A parent quoting a stale child API is invisible until the child is fixed, and nothing checks for it: each page validates fine alone.BorderType and Density shape the slot’s default content — a component property cannot reach inside a slot, so replacing the rows leaves both with nothing to act on. Natively they apply to whatever fills the slot, which is the better behaviour and the one the page documents. It is not fixable in Figma, so a Code Connect binding will not be a straight pass-through.getSnippet, so both language tabs were frozen on a static string; they now show the slot as a trailing closure, which is the honest shape given the rows arrive through it rather than as a parameter.MIddle Inset typo is gone — the property and its values are now joined as BorderType = MiddleInset | FullWidth | None.Dropdown Item - Last is gone. Its master and instance had disagreed since the Dropdown to Select rename; the last row is now simply Select Item, with six dividers between seven rows and none trailing.Slot renamed to ⤷ SelectionSlot, so the name says what belongs in it rather than only marking it as a slot.Label confirmed as a subsection header for grouped lists.25783:1255 (Sticker Sheets v2) to 7947:111630 (2026 Working File), and navGroup changed from Dropdown to Select.6383:3442 is a hand-built Dropdown - Item frame instead of a DropdownItem component instance. Breaks consistency.
OpenMenu (iOS) and DropdownMenu (Compose) draw the shadowed card automatically. This component has no 1:1 native mapping.
Open