Overview
Evaluating DS health and native mobile readiness for SwiftUI and Jetpack Compose.
Each component is evaluated against seven criteria. The report is self-contained and versions alongside the design system.
.html file in assessment-src/components/ with the nav, summary, and full assessment.node assessment-src/build.js to compile all component files into this page automatically.The overall status reflects the weakest criterion — one unresolved issue can block Code Connect linkability.
| ID | Criterion | What We Check |
|---|---|---|
| C1 | Layer Structure & Naming | Layers should use semantic names like leading-icon or content — not Figma defaults like Frame 42 or Group 7. The hierarchy should be logical and free of unnecessary nesting. |
| C2 | Variant & Property Naming | Variants and properties should follow clear, consistent conventions. Booleans expressed as true/false (not yes/no), enum values lowercase and hyphenated. |
| C3 | Token Coverage | All color, spacing, typography, and radius values should be bound to design tokens — not hardcoded. Token coverage determines how easily engineers can map decisions to a native token system. |
| C4 | Native Mappability | The component should map cleanly to a standard native primitive (e.g. DisclosureGroup, Button, List) with no web-only patterns that lack a native equivalent. |
| C5 | Interaction State Coverage | All expected interactive states should be defined as variants — default, pressed, focused, disabled, and error. Missing states force engineers to invent visual behavior. |
| C6 | Asset & Icon Quality | Icons should be vector components (not raster or PNG embeds) and colored using tokens so tinting works natively on both platforms. |
| C7 | Code Connect Linkability | The component should be a proper Figma component set with property names clean enough to map 1:1 to native parameters via Code Connect. |
| Status | Meaning |
|---|---|
| Ready | Linkable as-is. Clean structure, maps well to native. |
| Needs Refinement | Minor issues to resolve before linking. |
| Requires Rework | Needs redesign before native translation. |
| Not Applicable | No native equivalent. |
| Fix | Resolved via Figma MCP. Residual items may remain. |
Components
00 components in East Blue Design System
A disclosure row that expands to reveal content. Supports optional leading icon and description via boolean visibility properties. Reduced from 24 to 6 variants (Type × State) with color tokens fully connected.
How the accordion appears in a real product screen — expanding to reveal content.

Toggle properties to see the accordion update in real time.
content-body slot. Boolean visibility on leadingIcon and description lets designers configure the component without extra variants. content-body panel are both included. Engineers can implement it as a standalone unit with no external spec needed. Type) drives collapsed/expanded. leadingIcon and description are boolean show/hide properties — no duplicate variants needed. icon-leading, content, trailing-icon). Chevrons are vector instances. The icon slot accepts instances cleanly. | State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | Header row with chevron. Tap to expand/collapse. |
| Pressed | Yes | Yes | State=Pressed | Visual feedback on touch. Darker surface token. |
| Disabled | Yes | Yes | State=Disabled | Muted colors. Tap ignored. Chevron dimmed. |
| Focused (a11y) | N/A | N/A | — | Mobile OS handles focus rings natively. |
- Boolean properties converted from yes/no to true/false (C2)
- Layer names corrected to semantic naming:
container,icon-leading,content,trailing-icon(C1) - Pressed and disabled interaction states added across all 6 variants (C5)
- Expanded content panel with
content-bodySLOT added to all expanded variants (C4) - Variant set reduced from 24 to 6 —
Type×Statematrix (C2) leadingIconanddescriptionconverted to boolean visibility properties- Fixed 56px header height applied across all 6 variants
- 10 design tokens connected — all colors, spacing, and typography fully tokenized (C3)
- Annotation instance frame built with nested auto layout (Type × State grid)
- Code Connect CLI mappings not registered — no native component files linked (C7)
- Create an
AccordionGroupcompound component to manage exclusive expand (only one open at a time) — common FAQ and settings pattern New Component
Header row only — 56px fixed height. Trailing chevron points down. Tap anywhere in the row to expand.
All colors are bound to design tokens from the component variable collection.
| Role | Token | Default | Pressed | Disabled |
|---|---|---|---|---|
| Header bg | surface/default | #FFFFFF | — | — |
| Pressed bg | surface/pressed | — | #F4F7FB | — |
| Disabled bg | surface/disabled | — | — | #F8F9FB |
| Border | border/subtle | #E5EBF4 | #E5EBF4 | #E5EBF4 |
| Label | text/primary | #0A2757 | #0A2757 | — |
| Label (disabled) | text/disabled | — | — | #C2C6CF |
| Description | text/secondary | #90A8D0 | #90A8D0 | — |
| Icon placeholder | icon/placeholder | #C2C6CF | #C2C6CF | #C2C6CF |
| Chevron | icon-chevron | #005CE5 | #005CE5 | #C2CFE5 |
Header row (56px) + content-body panel (56px SLOT)=112px total height. Trailing chevron points up. Content-body background uses surface/content token.
Expanded adds the surface/content token for the content-body panel background.
| Role | Token | Default | Pressed | Disabled |
|---|---|---|---|---|
| Header bg | surface/default | #FFFFFF | — | — |
| Pressed bg | surface/pressed | — | #F4F7FB | — |
| Disabled bg | surface/disabled | — | — | #F8F9FB |
| Content bg | surface/content | #F4F7FB | #F4F7FB | #F8F9FB |
| Border | border/subtle | #E5EBF4 | #E5EBF4 | #E5EBF4 |
| Label | text/primary | #0A2757 | #0A2757 | — |
| Label (disabled) | text/disabled | — | — | #C2C6CF |
| Description | text/secondary | #90A8D0 | #90A8D0 | — |
| Icon placeholder | icon/placeholder | #C2C6CF | #C2C6CF | #C2C6CF |
| Chevron | icon-chevron | #005CE5 | #005CE5 | #C2CFE5 |
iOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios" // Or in Package.swift: .package( url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0" )
Android — Gradle (Kotlin DSL)
// build.gradle.kts (app) dependencies { implementation("com.eastblue.ds:accordion:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.accordion.* // Compose
Package not yet published. These are the planned distribution paths. API shape is final — native implementation is pending.
Every row maps a Figma component property to its native equivalent.
| Figma Property | SwiftUI | Compose |
|---|---|---|
Type=Collapsed | isExpanded: false | isExpanded=false |
Type=Expanded | isExpanded: true | isExpanded=true |
State=Disabled | .disabled(true) | enabled=false |
leadingIcon=true | leadingIcon: Image? | leadingIcon: @Composable (() -> Unit)? |
description=true | description: String? | description: String? |
Content-Body (SLOT) | content: () -> some View | content: @Composable () -> Unit |
// Basic EBAccordion("Settings", isExpanded: $isExpanded) { Text("Content goes here") } // With leading icon EBAccordion("Settings", isExpanded: $isExpanded, leadingIcon: Image(systemName: "gear") ) { Text("Content") } // With description EBAccordion("Settings", description: "Manage your preferences", isExpanded: $isExpanded ) { Text("Content") } // Disabled EBAccordion("Settings", isExpanded: $isExpanded) { Text("Content") } .disabled(true)
// Basic EBAccordion( title = "Settings", isExpanded = isExpanded, onExpandedChange = { isExpanded = it } ) { Text("Content goes here") } // With leading icon EBAccordion( title = "Settings", isExpanded = isExpanded, onExpandedChange = { isExpanded = it }, leadingIcon = { Icon(Icons.Filled.Settings, null) } ) { Text("Content") } // Disabled EBAccordion( title = "Settings", isExpanded = isExpanded, onExpandedChange = {}, enabled = false ) { Text("Content") }
| Requirement | iOS | Android |
|---|---|---|
| Min touch target | 44 × 44pt (full header row) | 48 × 48dp (full header row) |
| Expand/collapse | .accessibilityAction(.default) toggles | onClick handler on header |
| State announcement | .accessibilityValue("expanded"/"collapsed") | expandedState semantics |
| Disabled | .disabled(true) — announced by VoiceOver | enabled=false |
| Content-body | Automatically read by screen reader when expanded | |
| Chevron icon | .accessibilityHidden(true) — decorative | contentDescription=null — decorative |
Do
Use Accordion for progressive disclosure — hiding secondary content until the user needs it.
Don't
Nest Accordions more than one level deep — it creates confusing navigation.
Do
Use description text for context that helps users decide whether to expand.
Don't
Put critical information inside collapsed Accordions — users may miss it.
Do
Use leadingIcon to reinforce the section's topic — gears for settings, bell for notifications.
Don't
Use Accordion for content the user needs to compare side-by-side — use tabs instead.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic names across all variants: container, icon-leading, content, trailing-icon. |
| C2 | Variant & Property Naming | Ready | Boolean properties use true/false. Variant keys use key=value syntax. leadingIcon and description are boolean visibility props. |
| C3 | Token Coverage | Ready | 10 tokens bound across all 6 variants. All colors, spacing, and typography fully tokenized. |
| C4 | Native Mappability | Ready | Header + content-body SLOT maps cleanly to DisclosureGroup (SwiftUI) and AnimatedVisibility (Compose). |
| C5 | Interaction State Coverage | Ready | Default, pressed, and disabled states covered across all 6 variants. Focus ring N/A — mobile OS handles natively. |
| C6 | Asset & Icon Quality | Ready | Chevrons are vector component instances. Leading icon is a SLOT placeholder accepting any icon instance. |
| C7 | Code Connect Linkability | Needs Refinement | No Code Connect mappings registered. Property structure is clean and ready for mapping — suggested paths below. |
| Aspect | Status | Notes |
|---|---|---|
| Component type | Ready | Proper Figma component set. |
| Variant naming | Ready | key=value syntax with true/false booleans. |
| Property naming | Ready | Clean 1:1 mapping to native params. |
| Layer naming | Ready | container, icon-leading, content, trailing-icon. |
| Token coverage | Ready | All 10 tokens bound — colors, spacing, and typography. |
| Asset quality | Ready | Chevrons are vector component instances. Icon slot is SLOT type. |
| Code Connect | Not Mapped | No mappings registered. Suggested paths below. |
2 Type values × 3 State values. leadingIcon and description are boolean visibility properties, not variant axes.
| Type | State | Node ID |
|---|---|---|
| Collapsed | Default | 16870:9289 |
| Expanded | Default | 16870:9298 |
| Collapsed | Pressed | 16919:864 |
| Expanded | Pressed | 16919:877 |
| Collapsed | Disabled | 16919:956 |
| Expanded | Disabled | 16919:969 |
Placeholder reverted after v1.3.0 restructure. Re-applied icon-leading name across all 6 current variants. FixedProximaSoft-Bold.ttf (700, TTF, GPOS kerning, 959 glyphs) and BarkAda-SemiBold.ttf (600, TTF, GPOS kerning, 1050 glyphs) confirmed native-ready. BarkAda uses PostScript name BarkAda-SemiBold for iOS registration. ValidatedleadingIcon and description converted from variant axes to boolean visibility properties. Component set now has 2 Type values (Collapsed / Expanded) × 3 State values (Default / Pressed / Disabled)=6 variants total. Refinedsurface/default, border/subtle, text/primary, text/secondary, icon/placeholder, icon-chevron, surface/pressed, surface/content, surface/disabled, text/disabled. Fully resolves C3. FixedlabelDescription → description for cleaner 1:1 mapping to native params. Refinedcontent-body frame (360×80px) added at y=62 inside each container. Background: #F4F7FB (surface/content token). Border: #E5EBF4. Fully resolves C4. Fixedstate property added with values default / pressed / disabled. Fully resolves C5. FixedFrame to container. FixedPlaceholder to icon-leading. Fixedleading icon and label description converted from yes/no to true/false. Fully resolves C2. Fixedcontent-body frame added to all 12 expanded variants. Fixed in 1.2.0Stacked/overlapping avatars for participant lists — conversation members, shared documents, collaboration indicators. 4 variants: layout=pair (2), layout=trio (3), layout=quad (4), and layout=overflow (3 + "+N" badge). Fixed 48×48 container with 24×24 inner avatars.
layout with semantic values ✓. Overflow variant added ✓. Inner avatars repointed to canonical Avatar via instance swap ✓. Only C7 (Code Connect) remains — tracked universally across all components.How the avatar group appears in a real product screen — conversation list where grouped chats display stacked avatars (DX Team, David's Surprise Party) alongside single-avatar threads.
Toggle count to see the avatar group update in real time.
no. of initals → layout with semantic values (pair/trio/quad/overflow). C2 resolved. Inherits the main/avatar/brand/intials typo from Avatar's shared variable collection (tracked under Avatar, not an Avatar Group blocker).17143:4488). Changes to Avatar now propagate to Avatar Group automatically. Compositional inheritance restored.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| 2 avatars | Yes | Yes | layout=pair | Diagonal overlap — top-left + bottom-right |
| 3 avatars | Yes | Yes | layout=trio | Triangle arrangement |
| 4 avatars | Yes | Yes | layout=quad | 2×2 grid |
| Overflow (5+) | Yes | Yes | layout=overflow | 3 avatars + "+N" badge in bottom-right slot. Uses the same default/light style as the avatar it replaces. |
| Pressed / Disabled | N/A | N/A | — | Display-only. Tap behavior handled by parent container. |
- Property renamed:
no. of initals→layoutwith semantic values (pair/trio/quad/overflow). Fixes typo, removes spaces/dots, replaces pseudo-numeric strings with true enum values. Maps cleanly to SwiftUIEBAvatarGroupLayout.pair/.trio/.quad/.overflow/ ComposeEBAvatarGroupLayout.Pairetc. C2 Fixed - Overflow variant
layout=overflowadded — bottom-right slot shows "+N" badge instead of a 4th avatar. Handles groups larger than 4. C5 Fixed - Inner avatars repointed via instance swap to the canonical Avatar component (
17143:4488). Previously referenced a duplicate Avatar at21:94766— now all 4 variants inherit from the canonical source. Compositional pattern restored: changes to Avatar will propagate here automatically. C6 Fixed
- Code Connect CLI mappings not registered. C7
- Consider adding size variants (e.g.
groupSize=small/medium/large) — current 48×48 is one size only. Bigger groups may need larger containers for readability. Suggested - Consolidate the duplicate Avatar component at
21:94766— now that Avatar Group points at the canonical17143:4488, the duplicate should be deprecated and eventually removed in a future DS cleanup pass. DS Hygiene
4 variants (pair / trio / quad / overflow). All use a 48×48 fixed container with 24×24 inner avatars arranged with intentional overlap to indicate grouping. Overflow replaces the 4th avatar with a "+N" badge for groups larger than 4.
Two avatars placed diagonally. Top-left uses dark-initials (brand), bottom-right uses initials-light (default).
Three avatars in a triangular arrangement. Two on top (dark + dark), one default at bottom.
Four avatars in a 2×2 grid. Top row: brand + brand. Bottom row: default + default.
Overflow variant — 3 avatars plus a "+N" badge in the bottom-right position. Use when group has 5 or more members. The "+N" uses the default/light avatar style with overridable text content.
Inner avatars use the same tokens as the main Avatar component. See Avatar / Style tab for the full token reference.
| Role | Token | Value |
|---|---|---|
| Brand avatar bg | main/avatar/brand/bg | #005CE5 |
| Brand avatar border | main/avatar/brand/border | #E5EBF4 |
| Brand avatar initials | main/avatar/brand/intials library typo | #FFFFFF |
| Default avatar bg | main/avatar/default/bg | #F6F9FD |
| Default avatar border | main/avatar/default/border | #E5EBF4 |
| Default avatar initials | main/avatar/default/initials | #2340A9 |
| Property | Value |
|---|---|
| Container size | 48 × 48 |
| Inner avatar size | 24 × 24 |
| Inner avatar radius | 12px |
| Inner avatar border | 1.5px solid |
| Overlap offset (2 avatars) | 16px diagonal |
| Overlap offset (3 avatars) | 12px horizontal, 24px vertical |
| Overlap offset (4 avatars) | 24px grid step |
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:avatar:1.0.0") }
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| layout=pair | .ebLayout(.pair) | layout=EBAvatarGroupLayout.Pair | 2 avatars, diagonal overlap |
| layout=trio | .ebLayout(.trio) | layout=EBAvatarGroupLayout.Trio | 3 avatars, triangle |
| layout=quad | .ebLayout(.quad) | layout=EBAvatarGroupLayout.Quad | 4 avatars, 2×2 grid |
| layout=overflow | .ebLayout(.overflow) | layout=EBAvatarGroupLayout.Overflow | 3 avatars + "+N" badge |
// Pair / trio / quad — pass avatars, layout auto-detected from count EBAvatarGroup(avatars: [ EBAvatar(initials: "DM"), EBAvatar(initials: "LM"), EBAvatar(initials: "AB") ]) // Overflow — pass full list + max visible count EBAvatarGroup(avatars: allAvatars, maxVisible: 3) .ebLayout(.overflow) // renders 3 avatars + "+N" if allAvatars.count > 3
// Pair / trio / quad — pass avatars, layout auto-detected from count EBAvatarGroup( avatars = listOf( Avatar(initials = "DM"), Avatar(initials = "LM"), Avatar(initials = "AB") ) ) // Overflow — pass full list + maxVisible EBAvatarGroup( avatars = allAvatars, maxVisible = 3, layout = EBAvatarGroupLayout.Overflow )
| Requirement | iOS | Android |
|---|---|---|
| Accessibility label | .accessibilityLabel("3 participants: Dara, Lara, Alex") | contentDescription="3 participants: ..." |
| Role | Decorative if not tappable — use .accessibilityHidden(true) on individual avatars | Same — prefer single group-level semantic |
| Tap target | 48 × 48 container meets iOS HIG when whole group is tappable | Meets Material 48dp minimum |
Do
Use Avatar Group for 2–4 participants in a list item, header, or shared-with indicator.
Don't
Use for counts above 4 without an overflow "+N" badge — users can't infer total count from a cluster alone.
Do
Provide a single group-level accessibility label listing all participants.
Don't
Let each avatar announce separately — creates VoiceOver/TalkBack noise.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic names: Avatar, container. Top-level component set uses "Avatar Group" — clean. |
| C2 | Variant & Property Naming | Needs Fix | Property no. of initals has typo, spaces, and uses string values. Rename to count with integer values. |
| C3 | Token Coverage | Ready | All colors bound to Avatar's tokens. Inherits the same typo in intials — tracked under Avatar's open issues. |
| C4 | Native Mappability | Partial | Maps to stacked avatars via ZStack (iOS) / Box + offset (Compose). Fixed 48×48 and count=2/3/4 don't match a dynamic native list — API should accept an array. |
| C5 | Interaction State Coverage | Needs Fix | No overflow state for 5+ avatars. Common DS pattern ("+N" badge) is missing. |
| C6 | Asset & Icon Quality | Partial | Inner avatars are hardcoded 24px containers, not Avatar component instances. Breaks compositional inheritance — changes to Avatar won't propagate. |
| C7 | Code Connect Linkability | Pending | No CLI mappings registered yet. |
| layout | Node ID | Size | Notes |
|---|---|---|---|
| pair | 18276:4555 | 48 × 48 | 2 avatars — diagonal |
| trio | 18276:4558 | 48 × 48 | 3 avatars — triangle |
| quad | 18276:4562 | 48 × 48 | 4 avatars — 2×2 grid |
| overflow | 18276:4585 | 48 × 48 | 3 avatars + "+N" overflow badge |
no. of initals → layout. Values changed from pseudo-numeric strings (2/3/4/5+) to semantic enum values (pair/trio/quad/overflow). Fixes typo, spaces, and dots in one pass. Clean native enum mapping. Fixedlayout=overflow displays 3 avatars + a "+N" badge in the 4th slot. Handles groups larger than 4. Added21:94766. All 4 variants now use instances of the canonical Avatar at 17143:4488. Compositional inheritance restored. Swappedno. of initals: missing second "i", contains dot + space. Values are strings instead of integers. Blocks native enum mapping. Fixed in 1.1.0A circular display element showing user initials or a profile image. Supports 7 sizes (20px-90px) and 3 types (dark initials, light initials, image). Used when a profile image is unavailable or for visual user identification.
How the avatar appears in a real product screen — Contacts list with Favorites row (brand fill + default fill avatars in circular display).
Toggle type and size to see the avatar update in real time.
main/avatar/...). Variant naming verified correct (type=initials-light). Border-radius tokenized to radius/radius-round. One C2 issue remaining: token main/avatar/brand/intials has typo (should be initials) — manual rename needed in Figma Variables panel.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | type + size | Display-only. All 3 types fully defined across 7 sizes. |
| Pressed | N/A | N/A | -- | Display-only component. Tap behavior handled by parent container. |
| Disabled | N/A | N/A | -- | Display-only component. No disabled state. |
| Focused (a11y) | N/A | N/A | -- | Display-only. Focus rings rendered by parent interactive container if needed. |
Avatar is display-only. Interaction states (pressed, disabled, focused) are N/A and handled by parent containers.
- Border-radius: bound to
radius/radius-round(99999) across all sizes — previously hardcoded per size (C3) - Border-width: confirmed fixed per size by design — not a token gap (C3)
- Raster backgrounds replaced with vector ELLIPSE layers across all 5 affected initials variants (C6)
- Avatar Group compound component created (previously a design recommendation) — see sibling component under Avatar group (C2)
- Variant property value naming verified on recheck: variant names are correctly hyphenated as
type=initials-lightin Figma source. Earlier "spaces" report was an MCP output artifact (TypeScript enum generation converts hyphens to spaces). No action required. C2 Verified
- Code Connect CLI mappings not registered C7
- Token name typo:
main/avatar/brand/intialsstill missing second "i" — should beinitials. Lives in the shared Variables collection, not in this component — so it's tracked here for visibility but not counted as an Avatar-level blocker. Fix requires renaming in Figma → Variables panel. C2 Noted
- Add a
badgeoverlay slot for status indicators (online/offline dot, notification count) — common in chat, contacts, and profile lists Suggested
Blue circle with white initials text. Branded avatar used as default when no photo is available.
Display-only component. No interaction states. All colors bound to main/avatar/brand/ tokens.
| Role | Token | Value |
|---|---|---|
| Circle bg | main/avatar/brand/bg | #005CE5 |
| Circle border | main/avatar/brand/border | #E5EBF4 |
| Initials text | main/avatar/brand/initials | #FFFFFF |
Light circle with blue initials text. Neutral variant for non-branded contexts.
Display-only component. No interaction states. All colors bound to main/avatar/default/ tokens.
| Role | Token | Value |
|---|---|---|
| Circle bg | main/avatar/default/bg | #F6F9FD |
| Circle border | main/avatar/default/border | #E5EBF4 |
| Initials text | main/avatar/default/initials | #2340A9 |
User profile photo in a circle clip. Falls back to placeholder when image fails to load.
Display-only component. Placeholder colors shown when image has not loaded. All colors bound to main/avatar/placeholder/ tokens.
| Role | Token | Value |
|---|---|---|
| Placeholder bg | main/avatar/placeholder/bg | #C2CFE5 |
| Placeholder border | main/avatar/placeholder/border | #E5EBF4 |
iOS -- Swift Package Manager
// In Xcode: File -> Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios" // Or in Package.swift: .package( url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0" )
Android -- Gradle (Kotlin DSL)
// build.gradle.kts (app) dependencies { implementation("com.eastblue.ds:avatar:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.avatar.* // Compose
Package not yet published. These are the planned distribution paths. API shape is final -- native implementation is pending.
Every row maps a Figma component property to its native equivalent.
| Figma Property | SwiftUI | Compose |
|---|---|---|
type=dark-initials | .darkInitials | AvatarType.DarkInitials |
type=initials-light | .lightInitials | AvatarType.LightInitials |
type=image | .image(url:) | AvatarType.Image(url) |
size=20px...90px | size: AvatarSize | size: AvatarSize |
// Dark initials EBAvatar("DM", type: .darkInitials, size: .large)
// Dark initials EBAvatar( initials = "DM", type = AvatarType.DarkInitials, size = AvatarSize.Large )
// Light initials EBAvatar("LM", type: .lightInitials, size: .medium)
// Light initials EBAvatar( initials = "LM", type = AvatarType.LightInitials, size = AvatarSize.Medium )
// Image EBAvatar(imageURL: profileURL, size: .large)
// Image EBAvatar( imageUrl = profileUrl, type = AvatarType.Image, size = AvatarSize.Large )
| Requirement | iOS | Android |
|---|---|---|
| Alt text | accessibilityLabel("User avatar") | contentDescription="User avatar" |
| Decorative mode | isAccessibilityElement=false (in lists) | importantForAccessibility=no |
| Image loading | AsyncImage with placeholder | SubcomposeAsyncImage with placeholder |
Do
Use dark-initials as default when no photo is available.
Don't
Use image type with placeholder -- use initials instead.
Do
Match avatar size to context (20px in dense lists, 90px in profiles).
Don't
Mix initials types in the same context.
Do
Always pass 2-letter initials (first + last).
Don't
Show single-letter or empty initials.
Do
Provide alt text for image avatars.
Don't
Skip accessibility labels.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Layers named container, background, replace here - image. Simple hierarchy. Minor: some sizes have a child also named container. |
| C2 | Variant & Property Naming | Ready | Variant naming resolved (initials-light). Token name typo fixed (main/avatar/brand/initials). Size values use px suffix (minor, no impact on native mapping). |
| C3 | Token Coverage | Partial | 8 color tokens, full typography tokens, and radius/radius-round connected. Border-width is fixed per size (by design). |
| C4 | Native Mappability | Ready | Maps to custom Circle-clipped view on both platforms. No web-only patterns. |
| C5 | Interaction State Coverage | N/A | Display-only component. No interactive states needed. |
| C6 | Asset & Icon Quality | Ready | All initials variants now use vector ELLIPSE layers. Image type rasters are expected (user photos). |
| C7 | Code Connect Linkability | Needs Refinement | Usage descriptions attached. Variant naming now clean. Token typo remains. No CLI mappings. |
Maps type and size properties to native parameters. 21 variants (3 types x 7 sizes). Display-only -- no state dimension.
| Figma | SwiftUI | Compose |
|---|---|---|
| dark-initials | type: .darkInitials | AvatarType.DarkInitials |
| initials-light | type: .lightInitials | AvatarType.LightInitials |
| image | imageURL: URL | AvatarType.Image(url) |
| Figma | SwiftUI | Compose |
|---|---|---|
| 20px | size: .xxSmall | AvatarSize.XXSmall |
| 24px | size: .xSmall | AvatarSize.XSmall |
| 32px | size: .small | AvatarSize.Small |
| 40px | size: .medium | AvatarSize.Medium |
| 48px | size: .large | AvatarSize.Large |
| 64px | size: .xLarge | AvatarSize.XLarge |
| 90px | size: .xxLarge | AvatarSize.XXLarge |
3 type × 7 size=21 variants. No interaction state axis (display-only component).
| Type | Sizes | Notes | Count |
|---|---|---|---|
| dark-initials | 20, 24, 32, 40, 48, 64, 90 px | Brand background, white initials | 7 |
| initials-light | 20, 24, 32, 40, 48, 64, 90 px | Light background, dark initials | 7 |
| image | 20, 24, 32, 40, 48, 64, 90 px | Photo fill (raster expected) | 7 |
View full Type × Size breakdown (21 rows)
| Type | Size | Raster? | Node ID |
|---|---|---|---|
| dark-initials | 20px | -- | 17143:4489 |
| dark-initials | 24px | -- | 17143:4497 |
| dark-initials | 32px | -- | 17143:4505 |
| dark-initials | 40px | -- | 17143:4513 |
| dark-initials | 48px | -- | 17143:4523 |
| dark-initials | 64px | -- | 17143:4531 |
| dark-initials | 90px | -- | 17143:4539 |
| initials-light | 20px | -- | 17143:4492 |
| initials-light | 24px | -- | 17143:4500 |
| initials-light | 32px | -- | 17143:4508 |
| initials-light | 40px | -- | 17143:4517 |
| initials-light | 48px | -- | 17143:4526 |
| initials-light | 64px | -- | 17143:4535 |
| initials-light | 90px | -- | 17143:4542 |
| image | 20px | expected | 17143:4495 |
| image | 24px | expected | 17143:4503 |
| image | 32px | expected | 17143:4511 |
| image | 40px | expected | 17143:4521 |
| image | 48px | expected | 17143:4529 |
| image | 64px | expected | 17143:4546 |
| image | 90px | expected | 17143:4548 |
Raster column: ✗=initials background exported as raster image instead of vector. "expected"=image type uses photos by design.
type=initials - light renamed to type=initials-light across all 7 variants. Now matches dark-initials hyphen style. Fixedmain/avatar/brand/intials corrected to main/avatar/brand/initials. Token now maps correctly to native implementations. Fixedradius/radius-round (99999) instead of hardcoded per-size values (45.213px, 24px, 16px, 12px, 10px). FixedA colored pill/tag used to highlight an item's status for quick recognition. 68 variants across State (Primary/Brand/Info/Success/Warning/Danger/Disabled) x Level (Heavy/Medium/Light) x Type (Default/Voucher/Transaction/Dashboard). Display-only component with no interaction states.
Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle State, Level, and Type to see the badge update in real time.
main/badge/{semantic}/{level}/). Minor issues: State property names don't match token names (Info vs information, Success vs positive). Danger/Heavy and Disabled/Heavy Transaction variants have hardcoded opacity: 0.90.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State + Level + Type | Display-only. All 7 states x 3 levels fully defined per type. |
Badge is display-only. No interaction states (pressed, focused) exist. Disabled is a visual variant, not an interactive state -- it represents muted/inactive content styling.
- Hardcoded
opacity: 0.90removed from Danger/Heavy Transaction (21:111576) and Disabled/Heavy Transaction (3714:3863) inner containers. Both now at opacity 1, consistent with the other 66 variants. C3 Fixed - State property values renamed to match token semantic names across all 60 affected variants:
Info→Information,Success→Positive,Warning→Notice,Danger→Negative,Disabled→Muted. Figma State values now align 1:1 with token namespace — cleaner Code Connect mapping, no translation layer needed. C2 Fixed
- Code Connect CLI mappings not registered C7
- Remove hardcoded
opacity: 0.90from Danger/Heavy and Disabled/Heavy Transaction variants to ensure consistency across all variants Suggested - Consider aligning State property names with token semantic names (e.g.
State=Informationinstead ofState=Info) to reduce Code Connect mapping friction Suggested - Add an icon slot (leading) for badges that need visual reinforcement -- common in status badges across fintech apps Suggested
- Primary and Brand states only support Heavy level -- consider adding Medium/Light levels if these states need lower-emphasis variants in future Suggested
4 types with different shapes and sizing: Default (pill), Voucher (bottom-right radius only), Transaction (rounded rect), Dashboard (compact rounded rect). Each type supports 7 states x 3 levels (Primary and Brand are Heavy only).
Pill-shaped badge with full border-radius (99px). Standard status indicator for general use.
Badge with bottom-right radius only (4px). Used on voucher cards and promotional items. Fixed 18px height.
Rounded rectangle badge (4px radius). Used in transaction lists and history screens. Compact padding.
Compact rounded rectangle badge (4px radius) with smaller typography (10px). Used in dashboard widgets and summary cards.
Display-only component. All colors bound to main/badge/{semantic}/{level}/ tokens. Primary and Brand states only support Heavy level.
| State | Level | Role | Token | Value |
|---|---|---|---|---|
| Primary | Heavy | bg | main/badge/primary/heavy/background | #005CE5 |
| Primary | Heavy | label | main/badge/primary/heavy/label | #FFFFFF |
| Brand | Heavy | bg | main/badge/brand/heavy/background | #1972F9 |
| Brand | Heavy | label | main/badge/brand/heavy/label | #FFFFFF |
| Info | Light | bg | main/badge/information/light/background | #E5F1FF |
| Info | Light | label | main/badge/information/light/label | #005CE5 |
| Info | Medium | bg | main/badge/information/medium/background | #D2E5FF |
| Info | Medium | label | main/badge/information/medium/label | #005CE5 |
| Info | Heavy | bg | main/badge/information/heavy/background | #2340A9 |
| Info | Heavy | label | main/badge/information/heavy/label | #FFFFFF |
| Success | Light | bg | main/badge/positive/light/background | #E7F8F0 |
| Success | Light | label | main/badge/positive/light/label | #048570 |
| Success | Medium | bg | main/badge/positive/medium/background | #CAF2E0 |
| Success | Medium | label | main/badge/positive/medium/label | #048570 |
| Success | Heavy | bg | main/badge/positive/heavy/background | #12AF80 |
| Success | Heavy | label | main/badge/positive/heavy/label | #FFFFFF |
| Warning | Light | bg | main/badge/notice/light/background | #FCF0CA |
| Warning | Light | label | main/badge/notice/light/label | #966F0B |
| Warning | Medium | bg | main/badge/notice/medium/background | #F7D96E |
| Warning | Medium | label | main/badge/notice/medium/label | #966F0B |
| Warning | Heavy | bg | main/badge/notice/heavy/background | #CA970C |
| Warning | Heavy | label | main/badge/notice/heavy/label | #FFFFFF |
| Danger | Light | bg | main/badge/negative/light/background | #F8E6E6 |
| Danger | Light | label | main/badge/negative/light/label | #B50707 |
| Danger | Medium | bg | main/badge/negative/medium/background | #F4C7C9 |
| Danger | Medium | label | main/badge/negative/medium/label | #8D0710 |
| Danger | Heavy | bg | main/badge/negative/heavy/background | #D61B2C |
| Danger | Heavy | label | main/badge/negative/heavy/label | #FFFFFF |
| Disabled | Light | bg | main/badge/muted/light/background | #C2C5CA |
| Disabled | Light | label | main/badge/muted/light/label | #FFFFFF |
| Disabled | Medium | bg | main/badge/muted/medium/background | #9A9FA7 |
| Disabled | Medium | label | main/badge/muted/medium/label | #FFFFFF |
| Disabled | Heavy | bg | main/badge/muted/heavy/background | #717883 |
| Disabled | Heavy | label | main/badge/muted/heavy/label | #FFFFFF |
| Property | Default | Voucher | Transaction | Dashboard |
|---|---|---|---|---|
| Height | auto | 18px (fixed) | auto | auto |
| Padding H | 8px | 8px | 4px | 4px |
| Padding V | 2px (top) / 4px (bottom) | 2px (top) / 4px (bottom) | 1px (top) / 3px (bottom) | 1px |
| Corner radius | 99px (pill) | 0/0/4px/0 (BR only) | 4px | 4px |
| Type | Text Style | Font | Size | Tracking | Line-height |
|---|---|---|---|---|---|
| Default / Voucher / Transaction | Primary/Label/Fine | Proxima Soft Bold | 12px | 0.5px | 12px |
| Dashboard | Primary/Label/Tiny | Proxima Soft Bold | 10px | 0.25px | 10px |
iOS -- Swift Package Manager
// In Xcode: File -> Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios" // Or in Package.swift: .package( url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0" )
Android -- Gradle (Kotlin DSL)
// build.gradle.kts (app) dependencies { implementation("com.eastblue.ds:badge:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.badge.* // Compose
Package not yet published. These are the planned distribution paths. API shape is final -- native implementation is pending.
Every row maps a Figma component property to its native equivalent.
| Figma Property | SwiftUI | Compose | Notes |
|---|---|---|---|
State=Primary | state: .primary | BadgeState.Primary | Heavy level only |
State=Brand | state: .brand | BadgeState.Brand | Heavy level only |
State=Info | state: .info | BadgeState.Info | Token: information |
State=Success | state: .success | BadgeState.Success | Token: positive |
State=Warning | state: .warning | BadgeState.Warning | Token: notice |
State=Danger | state: .danger | BadgeState.Danger | Token: negative |
State=Disabled | state: .disabled | BadgeState.Disabled | Token: muted |
Level=Heavy | level: .heavy | BadgeLevel.Heavy | Solid fill, white label |
Level=Medium | level: .medium | BadgeLevel.Medium | Mid-tone fill, dark label |
Level=Light | level: .light | BadgeLevel.Light | Subtle fill, dark label |
Type=Default | type: .default | BadgeType.Default | Pill shape (99px radius) |
Type=Voucher | type: .voucher | BadgeType.Voucher | Bottom-right radius only |
Type=Transaction | type: .transaction | BadgeType.Transaction | Rounded rect (4px) |
Type=Dashboard | type: .dashboard | BadgeType.Dashboard | Compact, smaller font |
// Success badge -- heavy level EBBadge("Completed", state: .success, level: .heavy) // Info badge -- light level EBBadge("Pending", state: .info, level: .light)
// Success badge -- heavy level EBBadge( text = "Completed", state = BadgeState.Success, level = BadgeLevel.Heavy ) // Info badge -- light level EBBadge( text = "Pending", state = BadgeState.Info, level = BadgeLevel.Light )
EBBadge("50% OFF", state: .danger, level: .heavy, type: .voucher)
EBBadge( text = "50% OFF", state = BadgeState.Danger, level = BadgeLevel.Heavy, type = BadgeType.Voucher )
EBBadge("Failed", state: .danger, level: .heavy, type: .transaction)
EBBadge( text = "Failed", state = BadgeState.Danger, level = BadgeLevel.Heavy, type = BadgeType.Transaction )
EBBadge("Active", state: .success, level: .light, type: .dashboard)
EBBadge( text = "Active", state = BadgeState.Success, level = BadgeLevel.Light, type = BadgeType.Dashboard )
| Requirement | iOS | Android |
|---|---|---|
| Accessibility label | accessibilityLabel("Status: Completed") | contentDescription="Status: Completed" |
| Decorative mode | isAccessibilityElement=false (when status is conveyed elsewhere) | importantForAccessibility=no |
| Color contrast | Heavy levels meet WCAG AA (4.5:1+) | Heavy levels meet WCAG AA (4.5:1+) |
| Non-color indicator | Badge text conveys meaning alongside color | Badge text conveys meaning alongside color |
Do
Use semantic states that match the content meaning (Success for completed, Danger for failed, Warning for pending).
Don't
Use badges for interactive elements -- badges are display-only status indicators.
Do
Use Heavy level for primary status indicators and Light/Medium for secondary or supporting context.
Don't
Use multiple Heavy badges in the same row -- visual noise. Use one Heavy + rest Light/Medium.
Do
Match badge Type to context: Default for general, Voucher for promos, Transaction for history, Dashboard for summaries.
Don't
Mix badge Types within the same list or group.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Simple single-layer structure: container with text child. Semantic naming. |
| C2 | Variant & Property Naming | Ready | Clean enum properties: State, Level, Type. Minor: State names don't match token semantic names. |
| C3 | Token Coverage | Ready | All colors bound to main/badge/ tokens. Note: 2 variants have hardcoded opacity: 0.90. |
| C4 | Native Mappability | Ready | Maps to custom EBBadge on both platforms. Simple text + background shape. |
| C5 | Interaction State Coverage | N/A | Display-only component. No interactive states needed. |
| C6 | Asset & Icon Quality | N/A | No icons or assets. Text-only component. |
| C7 | Code Connect Linkability | Needs Refinement | No CLI mappings registered yet. Property naming is clean and ready for mapping. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Ready | Clean enum properties: State (7), Level (3), Type (4). Ready for Code Connect mapping. |
| Token coverage | Ready | All colors token-bound. Minor opacity inconsistency on 2 variants. |
| State coverage | N/A | Display-only. No interaction states. |
| Native component file | Pending | EBBadge.swift / EBBadge.kt not yet created |
7 State values x 3 Level values x 4 Type values=84 theoretical. Primary and Brand only support Heavy level, so actual count is (2 x 1 x 4) + (5 x 3 x 4)=8 + 60=68 variants.
| State | Level | Types | Notes |
|---|---|---|---|
| Primary | Heavy | Default, Voucher, Transaction, Dashboard | 4 variants |
| Brand | Heavy | Default, Voucher, Transaction, Dashboard | 4 variants |
| Info | Light / Medium / Heavy | Default, Voucher, Transaction, Dashboard | 12 variants |
| Success | Light / Medium / Heavy | Default, Voucher, Transaction, Dashboard | 12 variants |
| Warning | Light / Medium / Heavy | Default, Voucher, Transaction, Dashboard | 12 variants |
| Danger | Light / Medium / Heavy | Default, Voucher, Transaction, Dashboard | 12 variants |
| Disabled | Light / Medium / Heavy | Default, Voucher, Transaction, Dashboard | 12 variants |
opacity: 0.90 removed from Danger/Heavy Transaction (21:111576) and Disabled/Heavy Transaction (3714:3863) inner containers. Both now at opacity 1, consistent with the other 66 variants. FixedInfo ->Information, Success ->Positive, Warning ->Notice, Danger ->Negative, Disabled ->Muted. Figma State property now aligns 1:1 with token namespace (main/badge/{information|positive|notice|negative|muted}/{level}/). Cleaner Code Connect mapping with no translation layer. Fixedmain/badge/{semantic}/{level}/ tokens. Documentedopacity: 0.90 on container instead of using token-driven values. Inconsistent with other variants. Fixed in 1.1.0Used to trigger an action when tapped. The button's Call to Action describes the action that will occur. The Large/Medium Buttons are the default size for the GCash app.
How the button appears in a real product screen — primary and secondary actions in a bottom sheet.

Toggle properties and appearance modes to see the button update in real time.
Leading Container and Trailing Container SLOT nodes in every variant.Property=Value naming across all 60 variants. Size, State, and Style are orthogonal variant dimensions. Appearance is a variable mode — no naming conflicts. 12 color variables bound consistently.Button, Style=Outline → OutlinedButton, Style=Text → TextButton. SLOT nodes support icon+label compositions. Each size has its own text style — clean native mapping.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | All four appearance modes fully defined. |
| Pressed | Yes | Yes | State=Pressed | Darker fill/border using pressed tokens. |
| Disabled | Yes | Yes | State=Disabled | Muted color tokens applied across all appearances. |
| Destructive | Yes | Yes | Appearance mode: Destructive | Red tokens via variable mode. Applies to all 3 styles (Filled/Outline/Text). |
| Focused (a11y) | N/A | N/A | — | Mobile-only component. Focus rings rendered natively by iOS (UIKit/SwiftUI) and Android (Material a11y). No Figma state required. |
| Loading | Yes | Yes | Native modifier | Handled as an interaction modifier in native code — .ebLoading(true) (SwiftUI) / isLoading=true (Compose). Removed as a Figma state in v4.0. |
| Icon Only (a11y) | Yes | Yes | Icon Placement=Icon Only | Square target matches size height. Requires accessibilityLabel / contentDescription since no visible text. |
- Layer renamed from
.base/button/small→containeron compact disabled container (C1) - Icon slots (
Leading Container,Trailing Container) added to all variants as Figma SLOT nodes (C2) isErrorreplaced — Destructive is now an appearance variable mode, not a variant property (C2)- v2: Outlined and Text Link moved from appearance to
Stylevariant property (Filled/Outline/Text) (C2) - v2: Size moved from variable modes to variant dimension — each size has its own text style, eliminating font-size variable conflict (C2/C3)
- v3:
Buttonvariable collection created with 4 appearance modes (Default/Destructive/White/Subtle) — 12 color variables bound to all 60 variants (C3) - v3: Old
Button Sizeandbutton/variantcollections removed (C3) - v3.1: Loading state added — 12 new
State=Loadingvariants with dot indicators replacing label, disabled appearance colors (C5) - v4.0: Icon Placement promoted to component property — replaces
leadingIcon/trailingIconbooleans with a single 4-value enum (None/Leading/Trailing/Icon Only). AddsIcon Onlysquare variant for toolbars/navigation (previously a design recommendation). Handoff is now explicit — developers see icon placement as a first-class property. (C2) - v4.0: Appearance Mode documented in Figma component description with SwiftUI/Compose API mapping — addresses the Mode-invisibility handoff gap until Code Connect is implemented. (C7 partial)
- v4.0: State simplified to Default/Pressed/Disabled — Loading moved to a native interaction modifier rather than a Figma variant. (C5)
- v4.1:
button-containerwrapper layer removed — outermost component now holds fill/radius/auto-layout directly. Layer depth reduced from 4 to 3 (component → container → label/icon). Innercontainerretained for icon-label gap grouping. (C1) - v4.1: Large height reduced from 56px → 50px per design review feedback. (C3)
- v4.1: New Mode-driven token collection applied — all 60 Filled variants bound to
appearance/container/fill(+ pressed/disabled), all 60 Outline variants bound toappearance/stroke/color+ newappearance/label/on-surface/color, all 60 Text variants bound toappearance/label/on-surface/color. Switching the parent frame's Variable Mode (Default / Destructive / White / Subtle) now drives appearance across all 180 variants. (C3) - v4.1: New
appearance/label/on-surface/colorvariable created — semantic separation between labels on filled vs surface backgrounds. Eliminates token-purpose confusion between Filled labels (white-on-fill) and Outline/Text labels (color-on-surface). (C3) - v4.1: Text styles renamed to cleaner
Primary/Label/Large,Primary/Label/Base,Primary/Label/Small,Primary/Label/Fine(wasPrimary/Label/Light/*family). (C3)
- Code Connect CLI mappings not registered (C7)
- Document full-width (stretch) behavior as a layout guideline — consider a
isFullWidthboolean property for bottom-sheet CTAs and standalone action areas Suggested
Solid background with contrasting label. Primary action style. Colors change via Appearance variable mode.
Token names resolve to different hex values per mode. All 4 modes share the same 4 variables from the Button collection.
| Mode | Role | Enabled | Pressed | Disabled |
|---|---|---|---|---|
| Default | bg | #005CE5 | #2340A9 | #9BC5FD |
| label | #FFFFFF | #FFFFFF | #FFFFFF | |
| Destructive | bg | #D81E1E | #B01818 | #F5A3A3 |
| label | #FFFFFF | #FFFFFF | #FFFFFF | |
| White | bg | #FFFFFF | #EEF2F9 | #F5F7FA |
| label | #005CE5 | #005CE5 | #005CE5 | |
| Subtle | bg | #E5F1FF | #D2E5FF | #EEF5FF |
| label | #005CE5 | #005CE5 | #005CE5 |
appearance/container/fillappearance/container/fill-pressedappearance/container/fill-disabledappearance/label/colorappearance/label/color-pressedappearance/label/color-disabledTransparent background with border and accent-colored label. Secondary action style.
Outline uses border + label tokens — no background fill. All 4 modes share the same 3 variables from the Button collection.
| Mode | Role | Enabled | Pressed | Disabled |
|---|---|---|---|---|
| Default | border | #005CE5 | #2340A9 | #9BC5FD |
| label | #005CE5 | #2340A9 | #9BC5FD | |
| Destructive | border | #D81E1E | #B01818 | #F5A3A3 |
| label | #D81E1E | #B01818 | #F5A3A3 | |
| White | border | #005CE5 | #2340A9 | #9BC5FD |
| label | #005CE5 | #2340A9 | #9BC5FD | |
| Subtle | border | #005CE5 | #2340A9 | #9BC5FD |
| label | #005CE5 | #2340A9 | #9BC5FD |
appearance/stroke/colorappearance/stroke/color-pressedappearance/stroke/color-disabledappearance/label/on-surface/colorappearance/label/on-surface/color-pressedappearance/label/on-surface/color-disabledNo background or border. Label only. Tertiary action style.
Text style uses label-only tokens — no background or border. All 4 modes share the same 3 variables from the Button collection.
| Mode | Role | Enabled | Pressed | Disabled |
|---|---|---|---|---|
| Default | label | #005CE5 | #2340A9 | #9BC5FD |
| Destructive | label | #D81E1E | #B01818 | #F5A3A3 |
| White | label | #005CE5 | #2340A9 | #9BC5FD |
| Subtle | label | #005CE5 | #2340A9 | #9BC5FD |
appearance/label/on-surface/colorappearance/label/on-surface/color-pressedappearance/label/on-surface/color-disablediOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios" // Or in Package.swift: .package( url: "https://github.com/AY-Org/eb-ds-ios", from: "2.0.0" )
Android — Gradle (Kotlin DSL)
// build.gradle.kts (app) dependencies { implementation("com.eastblue.ds:button:2.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.button.* // Compose
Package not yet published. These are the planned distribution paths. API shape is final — native implementation is pending.
Every row maps a Figma component property to its native equivalent. When a developer selects a variant in Figma, Code Connect will output the corresponding native code using these mappings.
| Figma Property | SwiftUI | Compose |
|---|---|---|
Style=Filled | .ebAppearance(.filled) | EBButton {} |
Style=Outline | .ebAppearance(.outlined) | EBOutlinedButton {} |
Style=Text | .ebAppearance(.textLink) | EBTextButton {} |
Appearance=Default | (default — omit modifier) | (default — omit colors param) |
Appearance=Destructive | .ebColorScheme(.destructive) | colors=EBButtonDefaults.destructiveColors() |
Appearance=White | .ebColorScheme(.white) | colors=EBButtonDefaults.whiteColors() |
Appearance=Subtle | .ebColorScheme(.subtle) | colors=EBButtonDefaults.subtleColors() |
Size=Large…XSmall | controlSize: .large / .regular / .small / .compact / .mini | size=EBButtonSize.Large / Medium / Small / Compact / XSmall |
State=Disabled | .disabled(true) | enabled=false |
(Loading — runtime) | .ebLoading(true) | isLoading=true |
Icon Placement=None | (default — text only) | (default — text only) |
Icon Placement=Leading | Label("…", systemImage: "…") | leadingIcon={ Icon(…) } |
Icon Placement=Trailing | Label + trailing Image | trailingIcon={ Icon(…) } |
Icon Placement=Icon Only | EBButton(icon: Image(…), accessibilityLabel: "…") | EBButton(contentDescription="…") { Icon(…) } |
// Default appearance — Mode resolves at parent (.environment(\.ebAppearance, .default)) EBButton("Save Changes") .ebAppearance(.filled) .controlSize(.large) // Destructive appearance EBButton("Delete Account") .ebAppearance(.filled) .ebColorScheme(.destructive) // Icon Placement = Leading EBButton("Send Money", leadingIcon: Image(systemName: "arrow.up.right")) .ebAppearance(.filled) // Icon Placement = Trailing EBButton("Continue", trailingIcon: Image(systemName: "chevron.right")) .ebAppearance(.filled) // Icon Placement = Icon Only — square target, accessibility label required EBButton(icon: Image(systemName: "plus"), accessibilityLabel: "Add item") .ebAppearance(.filled) // Disabled EBButton("Submit") .ebAppearance(.filled) .disabled(true) // Loading — runtime only, not a Figma state EBButton("Submit") .ebAppearance(.filled) .ebLoading(true)
// Default appearance — Mode resolves at theme/parent EBButton( onClick = { /* action */ }, size = EBButtonSize.Large ) { Text("Save Changes") } // Destructive appearance EBButton( onClick = { /* action */ }, colors = EBButtonDefaults.destructiveColors() ) { Text("Delete Account") } // Icon Placement = Leading EBButton( onClick = { }, leadingIcon = { Icon(Icons.Default.Send, contentDescription = null) } ) { Text("Send Money") } // Icon Placement = Trailing EBButton( onClick = { }, trailingIcon = { Icon(Icons.Default.ChevronRight, contentDescription = null) } ) { Text("Continue") } // Icon Placement = Icon Only — contentDescription required EBButton( onClick = { }, contentDescription = "Add item" ) { Icon(Icons.Default.Add, contentDescription = null) } // Disabled EBButton( onClick = { }, enabled = false ) { Text("Submit") } // Loading — runtime only, not a Figma state EBButton( onClick = { }, isLoading = true ) { Text("Submit") }
// Default EBButton("Cancel") .ebAppearance(.outlined) // Destructive EBButton("Remove Item") .ebAppearance(.outlined) .ebColorScheme(.destructive) // Icon Placement = Leading EBButton("Filter", leadingIcon: Image(systemName: "line.3.horizontal.decrease")) .ebAppearance(.outlined) // Icon Placement = Icon Only EBButton(icon: Image(systemName: "square.and.arrow.up"), accessibilityLabel: "Share") .ebAppearance(.outlined) // Button pair HStack(spacing: 12) { EBButton("Cancel").ebAppearance(.outlined) EBButton("Save").ebAppearance(.filled) }
EBOutlinedButton( onClick = { /* action */ } ) { Text("Cancel") } // Icon Placement = Leading EBOutlinedButton( onClick = { }, leadingIcon = { Icon(Icons.Default.FilterList, contentDescription = null) } ) { Text("Filter") } // Icon Placement = Icon Only EBOutlinedButton( onClick = { }, contentDescription = "Share" ) { Icon(Icons.Default.Share, contentDescription = null) } // Button pair Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { EBOutlinedButton(onClick = {}) { Text("Cancel") } EBButton(onClick = {}) { Text("Save") } }
EBButton("Learn More") .ebAppearance(.textLink) .controlSize(.small) // Destructive EBButton("Remove") .ebAppearance(.textLink) .ebColorScheme(.destructive) // Icon Placement = Trailing (common for inline links) EBButton("Read more", trailingIcon: Image(systemName: "chevron.right")) .ebAppearance(.textLink)
EBTextButton( onClick = { /* action */ }, size = EBButtonSize.Small ) { Text("Learn More") } // Icon Placement = Trailing (common for inline links) EBTextButton( onClick = { }, trailingIcon = { Icon(Icons.Default.ChevronRight, contentDescription = null) } ) { Text("Read more") }
| Requirement | iOS | Android |
|---|---|---|
| Min touch target | 44 × 44pt | 48 × 48dp |
| Focus ring | Handled by UIKit/SwiftUI | Handled by Material ripple |
| Icon-only buttons | .accessibilityLabel("Send") | contentDescription="Send" |
| Destructive role | role: .destructive — announced by VoiceOver | Use semantics { role=Role.Button } |
| Loading state | .accessibilityLabel("Loading") + disable tap | semantics { stateDescription="Loading" } + disable click |
Do
Use one Filled button per screen area as the primary action. Pair with Outline or Text for secondary.
Don't
Place two filled buttons side by side — they compete for attention and neither reads as primary.
Do
Use Destructive appearance for irreversible actions (delete, remove). Always pair with a confirmation.
Don't
Use Destructive for actions that are simply "negative" but reversible (dismiss, close, decline).
Do
Use White appearance on brand-colored or dark surfaces (hero banners, promotional cards).
Don't
Use White appearance on a white background — the button disappears. Use Default or Subtle instead.
Do
Use Text style for inline or low-emphasis actions (Learn more, View terms, Skip).
Don't
Use Text style for primary form submission — it lacks the visual weight to signal the main action.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | All layers use semantic names. container, #label, leadingIcon, trailingIcon consistent across all 60 variants. |
| C2 | Variant & Property Naming | Ready | v3 clean orthogonal dimensions: Style (Filled/Outline/Text), Size (Large/Medium/Small/Compact/XSmall), State (Default/Pressed/Disabled). Appearance via 4 variable modes. leadingIcon/trailingIcon are SLOT nodes with Boolean component properties. |
| C3 | Token Coverage | Ready | All color values connected to semantic tokens. Layout/sizing driven by button/size variable collection (height, padding-h, padding-v, font-size). |
| C4 | Native Mappability | Ready | Maps to Button, OutlinedButton, TextButton. Destructive maps to role: .destructive / contentColor=errorColor. |
| C5 | Interaction State Coverage | Ready | Default, Pressed, Disabled, Loading covered across all 60 variants. Focus ring is N/A — mobile OS handles natively. Loading uses dot indicators with disabled appearance colors. |
| C6 | Asset & Icon Quality | Ready | Icon slots are Figma SLOT nodes accepting vector icon instances. Boolean properties control visibility. |
| C7 | Code Connect Linkability | Needs Refinement | No CLI mappings registered yet. Property structure is clean and ready for mapping. |
Maps v3.2 variant dimensions (Style × Size × State) and variable modes (Appearance) to native parameters. 60 variants × 4 appearance modes=240 visual states.
| Figma | SwiftUI | Compose |
|---|---|---|
| Filled | .ebAppearance(.filled) | EBButton {} |
| Outline | .ebAppearance(.outlined) | EBOutlinedButton {} |
| Text | .ebAppearance(.textLink) | EBTextButton {} |
| Figma | SwiftUI | Compose |
|---|---|---|
| Large 52px | .controlSize(.large) | size=EBButtonSize.Large |
| Medium 36px | .controlSize(.regular) | size=EBButtonSize.Medium |
| Small 28px | .controlSize(.small) | size=EBButtonSize.Small |
| Compact 28px | .controlSize(.compact) | size=EBButtonSize.Compact |
| XSmall 24px | .controlSize(.mini) | size=EBButtonSize.XSmall |
| Figma | SwiftUI | Compose |
|---|---|---|
| Default | (default) | (default) |
| Pressed | (system) | (system) |
| Disabled | .disabled(true) | enabled=false |
| (Loading — runtime) | .ebLoading(true) | isLoading=true |
| Figma | SwiftUI | Compose |
|---|---|---|
| Default | (omit — default) | (omit — default) |
| Destructive | .ebColorScheme(.destructive) | colors=…destructiveColors() |
| White | .ebColorScheme(.white) | colors=…whiteColors() |
| Subtle | .ebColorScheme(.subtle) | colors=…subtleColors() |
| Figma | SwiftUI | Compose |
|---|---|---|
| None | (text only — default) | (text only — default) |
| Leading | leadingIcon: Image(…) | leadingIcon={ Icon(…) } |
| Trailing | trailingIcon: Image(…) | trailingIcon={ Icon(…) } |
| Icon Only | EBButton(icon:, accessibilityLabel:) | EBButton(contentDescription=…) { Icon(…) } |
3 Style × 5 Size × 3 State × 4 Icon Placement=180 variants. Appearance is a variable mode (Default/Destructive/White/Subtle) that further multiplies visual states × 4=720 resolved visual states.
| Style | Sizes | States | Icon Placements | Count |
|---|---|---|---|---|
| Filled | Large, Medium, Small, Compact, XSmall | Default, Pressed, Disabled | None, Leading, Trailing, Icon Only | 60 |
| Outline | Large, Medium, Small, Compact, XSmall | Default, Pressed, Disabled | None, Leading, Trailing, Icon Only | 60 |
| Text | Large, Medium, Small, Compact, XSmall | Default, Pressed, Disabled | None, Leading, Trailing, Icon Only | 60 |
View full Style × Size breakdown (15 rows)
| Style | Size | Height | States × Icon Placements | Count |
|---|---|---|---|---|
| Filled | Large | 50px | 3 × 4 | 12 |
| Filled | Medium | 48px | 3 × 4 | 12 |
| Filled | Small | 36px | 3 × 4 | 12 |
| Filled | Compact | 28px | 3 × 4 | 12 |
| Filled | XSmall | 24px | 3 × 4 | 12 |
| Outline | Large | 50px | 3 × 4 | 12 |
| Outline | Medium | 48px | 3 × 4 | 12 |
| Outline | Small | 36px | 3 × 4 | 12 |
| Outline | Compact | 28px | 3 × 4 | 12 |
| Outline | XSmall | 24px | 3 × 4 | 12 |
| Text | Large | 50px | 3 × 4 | 12 |
| Text | Medium | 48px | 3 × 4 | 12 |
| Text | Small | 36px | 3 × 4 | 12 |
| Text | Compact | 28px | 3 × 4 | 12 |
| Text | XSmall | 24px | 3 × 4 | 12 |
appearance/container/fill (and pressed/disabled), Outline borders bound to appearance/stroke/color, all Outline + Text labels bound to new appearance/label/on-surface/color. Switching the parent frame's Variable Mode now drives appearance across the entire variant set. Validates the Mode → Property → API translation pattern for upcoming Code Connect work. Appliedappearance/label/on-surface/color variable created — 3 variants (color, color-pressed, color-disabled) × 4 modes. Provides semantic separation: label/color=labels on filled backgrounds (white-on-fill), label/on-surface/color=labels on transparent/surface backgrounds (color-on-surface). Eliminates the binding ambiguity for Outline/Text styles. Addedbutton-container wrapper layer removed — Visual properties (fill, radius, auto-layout, padding) lifted from inner button-container frame up to the variant component itself. Layer depth: 4 → 3. Native parity improved (the component IS the styled element, matching SwiftUI/Compose conventions). Inner container frame retained for icon-label gap grouping. RestructuredPrimary/Label/Large (was Primary/Label/Light/Base), Primary/Label/Base, Primary/Label/Small, Primary/Label/Fine. Cleaner semantic naming, removes the redundant "Light" prefix. RenamedleadingIcon, trailingIcon) caused handoff ambiguity. Now a single 4-value enum: None / Leading / Trailing / Icon Only. Adds Icon Only as a new square-button variant. Total variants: 60 → 180 (3 Styles × 3 States × 5 Sizes × 4 Icon Placements). RestructuredState now Default/Pressed/Disabled. Loading is handled as an interaction modifier in native code rather than a Figma variant. SimplifiedState=Loading variants (3 Styles × 4 Sizes). Dot indicators (● ● ●) replace label text. Uses disabled appearance colors. Tap is disabled during loading. AddedisError replaced with Variant: Brand | Destructive — True orthogonal property applied to all 24 variants. Destructive Default (filled) variants added for all 3 states. All 30 existing variants renamed. Fixedbutton/size variable collection with 4 modes: Large (52px), Medium (36px), Small (28px), XSmall (24px). Reduces variant count from 36 → 24 while expanding size coverage. Restructuredbutton/size variable collection created — 5 variables (height, font-size, padding-h, padding-v, icon-size) bound to all containers, labels, and icon slots across all variants. Fixed height binding prevents icon slot size from affecting button height. AddedleadingIcon and trailingIcon promoted from hidden frames to Figma SLOT nodes. Boolean component properties added for designer toggle control. UpgradedisError is not a true orthogonal boolean. Only applies to Outlined and Text Link, not Default. Recommendation: fold into Appearance as Outlined Error / Text Link Error. Resolved in v2.0.0. Resolved in 2.0.0leadingIcon + trailingIcon — Added to all 30 variants. Hidden by default. Upgraded to SLOT nodes with Boolean properties in v2.0.0. Fixed.base/button/small → container — Resolves C1. FixedA selection control for binary and partial choices. 33 variants across isSelected (true/false/indeterminate) × State (Default/Pressed/Focused/Disabled/Error) × Size (Small/Medium/Large). Code Connect registration pending.
Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle size and state to see the checkbox update in real time.
CheckboxItem compound component provides label + description pairing.icon-check child layer (C6 resolved). All 5 interaction states and indeterminate defined across 3 sizes (C5 resolved). Carries its own visual states and token bindings.main/checkbox/color/...). isSelected uses true/false/indeterminate. All property values follow boolean and enum standards (C2 resolved).CheckboxItem compound component wraps Checkbox + Label + Description for accessible form groups.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Unchecked | Yes | Yes | isSelected=false | Border-only container. 3 sizes. |
| Checked | Yes | Yes | isSelected=true | Blue fill + separable icon-check layer. |
| Indeterminate | Yes | Yes | isSelected=indeterminate | Blue fill + icon-indeterminate dash. |
| Disabled | Yes | Yes | State=Disabled | 40% opacity. Checked: #9BC5FD fill. |
| Pressed | Yes | Yes | State=Pressed | Unchecked: #EBF2FF bg. Checked: #0F57C8. |
| Focused | Yes | Yes | State=Focused | Blue #1972F9 border stroke. |
| Error | Yes | Yes | State=Error | Red border / red #D81E1E fill. |
isSelected=Yes/Norenamed toisSelected=true/falsein Figma — now maps correctly to SwiftBooland KotlinBooleanC2 Fixed- Checkmark rebuilt as a separable
icon-checkchild layer inside each checked container — engineers can now tint, swap, and reference it via Code Connect C6 Fixed - Added 27 new variants — State (Pressed/Focused/Disabled/Error) × isSelected (true/false) × Size, plus
isSelected=indeterminateper size withicon-indeterminatedash layer. 6 → 33 total variants C5 Fixed
- Code Connect CLI mappings not registered — all structural blockers resolved, registration can now proceed (C7)
CheckboxItemcompound component created — Checkbox instance + Label (Proxima Soft Bold) + Description (BarkAda Medium). 4 variants: isSelected (true/false) × Size (Small 14px label / Medium 18px label). Node:17734:161220Created
33 variants across 3 axes: State (Default/Pressed/Focused/Disabled/Error) × isSelected (true/false/indeterminate) × Size (Small/Medium/Large). All interaction states carry proper container fills, strokes, and separable icon layers.
Empty container with border stroke. Represents a deselected option.
Filled container with white checkmark. Represents a selected option. Checkmark is rendered via a separable icon-check child layer.
All interaction states now have defined colors. Token paths follow main/checkbox/color/{state}/{role} convention.
| State | Role | Token | DEFAULT | PRESSED | DISABLED | ERROR |
|---|---|---|---|---|---|---|
| Unchecked | Border | unselected/border | #D7E0EF | #1972F9 | #D7E0EF | #D81E1E |
| Unchecked | Container bg | unselected/bg | – | #EBF2FF | – | – |
| Checked | Container bg | selected/bg | #1972F9 | #0F57C8 | #9BC5FD | #D81E1E |
| Checked | Checkmark | selected/icon-check | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF |
| Indeterminate | Container bg | indeterminate/bg | #1972F9 | – | – | – |
| Indeterminate | Dash icon | indeterminate/icon | #FFFFFF | – | – | – |
| Focused | Border | focused/border | #1972F9 (all isSelected values) | |||
iOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios" // Or in Package.swift: .package( url: "https://github.com/AY-Org/eb-ds-ios", from: "2.0.0" )
Android — Gradle (Kotlin DSL)
// build.gradle.kts (app) dependencies { implementation("com.eastblue.ds:checkbox:2.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.checkbox.* // Compose
Package not yet published. These are the planned distribution paths. API shape is final — native implementation is pending.
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| isSelected | isOn: Binding<Bool> | checked: Boolean | true/false |
| isSelected=indeterminate | toggleIndeterminate | TriStateCheckbox | Partial selection |
| Size | .controlSize() | size=EBCheckboxSize.* | Small 16px, Medium 20px, Large 24px |
| State=Disabled | .disabled(true) | enabled=false | 40% opacity |
| State=Pressed | — | interactionSource | Touch feedback |
| State=Focused | .focused() | interactionSource | Keyboard/switch nav |
| State=Error | .ebError(true) | isError=true | Form validation |
// Unchecked (default state) EBCheckbox(isOn: $isSelected) .controlSize(.regular) // Small size EBCheckbox(isOn: $isSelected) .controlSize(.mini)
// Unchecked (default state) EBCheckbox( checked = false, onCheckedChange = { isSelected = it }, size = EBCheckboxSize.Medium ) // Small size EBCheckbox( checked = false, onCheckedChange = { isSelected = it }, size = EBCheckboxSize.Small )
// Checked (selected state) EBCheckbox(isOn: .constant(true)) .controlSize(.regular) // Bound to state @State private var isChecked = true EBCheckbox(isOn: $isChecked) .controlSize(.large)
// Checked (selected state) EBCheckbox( checked = true, onCheckedChange = { isSelected = it }, size = EBCheckboxSize.Medium ) // Bound to state var isChecked by remember { mutableStateOf(true) } EBCheckbox( checked = isChecked, onCheckedChange = { isChecked = it } )
// Indeterminate (partial selection) EBCheckbox(isOn: $isSelected) .controlSize(.regular) .toggleIndeterminate(true)
// Indeterminate (partial selection) TriStateCheckbox( state = ToggleableState.Indeterminate, onClick = { /* cycle state */ }, modifier = Modifier.size(EBCheckboxSize.Medium) )
// Disabled unchecked EBCheckbox(isOn: $isSelected) .controlSize(.regular) .disabled(true) // Disabled checked EBCheckbox(isOn: .constant(true)) .controlSize(.regular) .disabled(true)
// Disabled unchecked EBCheckbox( checked = false, onCheckedChange = {}, enabled = false, size = EBCheckboxSize.Medium ) // Disabled checked EBCheckbox( checked = true, onCheckedChange = {}, enabled = false, size = EBCheckboxSize.Medium )
// Error state (form validation) EBCheckbox(isOn: $isSelected) .controlSize(.regular) .ebError(true)
// Error state (form validation) EBCheckbox( checked = false, onCheckedChange = { isSelected = it }, isError = true, size = EBCheckboxSize.Medium )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 x 44 pt | 48 x 48 dp |
| Accessibility label | .accessibilityLabel("Accept terms") | semantics { contentDescription="Accept terms" } |
| Checked state announcement | VoiceOver reads "checked" / "unchecked" automatically via Toggle | TalkBack reads state automatically via Checkbox semantics |
| Indeterminate | toggleIndeterminate reads "mixed" | TriStateCheckbox reads "partially checked" |
Component is icon-only. Wrapping containers must provide an accessible label. Use padding to meet minimum touch target sizes for the smaller variants.
Do
Pair with a visible label adjacent to the checkbox. Checkboxes must always have associated text.
Don't
Use for a single binary toggle — use Switch/Toggle instead. Checkboxes are for multi-select scenarios.
Do
Use for multi-select scenarios — forms, filter lists, settings, and select-all patterns.
Don't
Use a standalone Checkbox without an accessible label. Pair with CheckboxItem or an adjacent text label for form use.
Do
Expand touch area via padding when using Small (16px) size — minimum touch target is 44pt / 48dp.
Don't
Omit an accessible label. Use .accessibilityLabel / contentDescription when no visible label is present.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Root frame named container. Simple, semantic hierarchy. No generic layer names. |
| C2 | Variant & Property Naming | Ready | isSelected now uses true/false — corrected in Figma. Maps directly to Swift Bool / Kotlin Boolean. indeterminate property is a C5 concern (missing state variant). |
| C3 | Token Coverage | Ready | All states have defined color values. Default, Pressed, Disabled, Error, Focused, and Indeterminate containers use distinct fills/strokes. Separable icon-check and icon-indeterminate layers present. |
| C4 | Native Mappability | Ready | Maps to Toggle(.checkbox) / Checkbox. Indeterminate maps to TriStateCheckbox. Label pairing via CheckboxItem compound component. |
| C5 | Interaction State Coverage | Ready | All 5 interaction states defined (Default, Pressed, Focused, Disabled, Error) × isSelected (true/false) × 3 sizes. Indeterminate added as isSelected=indeterminate with icon-indeterminate dash layer. 33 total variants. |
| C6 | Asset & Icon Quality | Ready | Checkmark rebuilt as a separable icon-check child vector layer inside each checked container. Can be tinted via selected/icon-check token and swapped natively. |
| C7 | Code Connect Linkability | Needs Refinement | All structural blockers resolved (C2, C5, C6). Ready for CLI mapping registration. No mappings registered yet. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Ready | isSelected=true/false — maps directly to Swift Bool and Kotlin Boolean |
| Icon/asset quality | Ready | icon-check is now a named, separable child layer — can be mapped to a native icon slot via Code Connect |
| State coverage | Ready | All interaction states defined — Default, Pressed, Focused, Disabled, Error, plus Indeterminate |
| Usage descriptions | Ready | All 33 variants have usage descriptions attached in Figma |
| Native component file | Pending | EBCheckbox.swift / EBCheckbox.kt not yet created |
5 State × 3 isSelected × 3 Size=45 theoretical. isSelected=indeterminate only ships State=Default, so actual count is (5 × 2 × 3) + (1 × 1 × 3)=33 variants.
| isSelected | States | Sizes | Count |
|---|---|---|---|
| false | Default, Pressed, Focused, Disabled, Error | Small, Medium, Large | 15 |
| true | Default, Pressed, Focused, Disabled, Error | Small, Medium, Large | 15 |
| indeterminate | Default only | Small, Medium, Large | 3 |
View full State × isSelected × Size breakdown (33 rows)
| isSelected | State | Size | Node ID |
|---|---|---|---|
| false | Default | Small | 17143:2465 |
| false | Pressed | Small | 17733:968 |
| false | Focused | Small | 17733:971 |
| false | Disabled | Small | 17733:974 |
| false | Error | Small | 17733:977 |
| true | Default | Small | 17143:2468 |
| true | Pressed | Small | 17733:980 |
| true | Focused | Small | 17733:984 |
| true | Disabled | Small | 17733:988 |
| true | Error | Small | 17733:992 |
| false | Default | Medium | 17143:2471 |
| false | Pressed | Medium | 17733:996 |
| false | Focused | Medium | 17733:998 |
| false | Disabled | Medium | 17733:1000 |
| false | Error | Medium | 17733:1002 |
| true | Default | Medium | 17143:2473 |
| true | Pressed | Medium | 17733:1004 |
| true | Focused | Medium | 17733:1008 |
| true | Disabled | Medium | 17733:1012 |
| true | Error | Medium | 17733:1016 |
| false | Default | Large | 17143:2476 |
| false | Pressed | Large | 17733:1020 |
| false | Focused | Large | 17733:1022 |
| false | Disabled | Large | 17733:1024 |
| false | Error | Large | 17733:1026 |
| true | Default | Large | 17143:2478 |
| true | Pressed | Large | 17733:1028 |
| true | Focused | Large | 17733:1032 |
| true | Disabled | Large | 17733:1036 |
| true | Error | Large | 17733:1040 |
| indeterminate | Default | Small | 17733:1044 |
| indeterminate | Default | Medium | 17733:1048 |
| indeterminate | Default | Large | 17733:1052 |
Proxima Soft Bold, 14px/18px) + Description (BarkAda Medium, 12px). Wraps atomic Checkbox with label pairing for accessible form use. CreatedisSelected=X, Size=Y, State=Z to State=X, isSelected=Y, Size=Z. State is now the first property axis in the component set. Section renamed from "Claude Testing" to "Checkbox". Updated#0F57C8 (checked); Focused uses blue border; Disabled uses 40% opacity; Error uses red border / #D81E1E fill. FixedisSelected=indeterminate variants for Small, Medium, and Large. Each contains a blue container frame with a named icon-indeterminate horizontal dash child layer. Maps to ToggleableState.Indeterminate (Android) and toggleIndeterminate (iOS). FixedisSelected=true variants. Created clean blue container frames (4px radius) with a named icon-check child vector inside each. Engineers can now tint via selected/icon-check token and map to a native icon slot. Nodes: 17721:962 (Small), 17721:963 (Medium), 17721:964 (Large). FixedisSelected=Yes/No to isSelected=true/false. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. C2 criterion resolved. FixedisChecked. Figma metadata confirms the property is isSelected. All documentation updated. FixedisSelected=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Fixed in v1.2.0A generic dropdown component with a select-style trigger and an overlay item list. 8 variants across variant (Text/Error/Amount/Mobile) × type (Collapsed/Expanded). Text and Error variants use a standard label + select field trigger. Amount adds a peso sign prefix. Mobile bundles a country code selector with a phone number input field. Optional subtext slot for helper/error messages.
selected uses yes/no instead of true/false (C2). Amount variant Peso Sign uses BOOLEAN_OPERATION (C6).Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle variant and type to see the dropdown update in real time.
selected uses yes/no string instead of true/false boolean (C2). Property type is a generic name — could conflict with platform keywords. Layer dropdowncontainer inconsistent with kebab-case convention.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Collapsed (Default) | Yes | Yes | type=Collapsed | Gray #D7E0EF border, white bg. Chevron down. |
| Expanded (Active) | Yes | Yes | type=Expanded | Blue #005CE5 border, chevron up. Dropdown list overlay with shadow. |
| Error | Yes | Yes | variant=Error | Red border — weak #F4C7C9 (collapsed), strong #D61B2C (expanded). |
| Disabled | No | No | — | Not defined. Required for form accessibility. |
| Pressed | No | No | — | Not defined. Touch feedback expected on mobile. |
- DropdownItem
selectedproperty usesyes/nostring — should betrue/falsefor SwiftBool/ KotlinBooleanmapping C2 - No disabled state — form dropdowns must support non-interactive state for accessibility and conditional form flows C5
- No pressed state — touch feedback expected on mobile platforms (iOS highlight, Android ripple) C5
- Amount variant Peso Sign uses
shape_fullBOOLEAN_OPERATION — not a clean vector path C6 - Code Connect CLI mappings not registered C7
- Add a
Disabledstate to the variant matrix — required for form accessibility and conditional logic Suggested - Rename
typeproperty toisExpanded(true/false) — avoids platform keyword conflicts and aligns with boolean convention Suggested - Rename DropdownItem
selectedfromyes/nototrue/falsefor direct boolean mapping Suggested - Consider extracting Mobile variant into a separate
CountryCodeDropdowncomponent — it bundles too many concerns (dropdown + phone input) for a generic dropdown Suggested - Add a
selectedvisual state for DropdownItem (checkmark or background highlight) to indicate current selection Suggested
8 variants across 2 axes: variant (Text/Error/Amount/Mobile) × type (Collapsed/Expanded). Text and Error share the same trigger structure. Amount adds a peso sign. Mobile adds a label row with info icon, plus a phone input field.
Default text dropdown. Label header, select trigger with placeholder text and chevron, optional subtext. Used for general-purpose list selection.
Error state dropdown with red border. Collapsed uses weak border (#F4C7C9), expanded uses strong border (#D61B2C). Subtext turns red for error messaging.
Amount selection with peso sign prefix. Same trigger structure as Text but with a currency indicator for monetary value selection.
Country code dropdown with phone number input. Bundles a label row (with info icon), a select field for country code, and a Labeled Field for phone number entry. Product-specific to GCash mobile number flows.
Trigger field and dropdown list colors. Border color is the primary state indicator. Error variant uses distinct border tokens.
| Role | Token | DEFAULT | ACTIVE | ERROR (collapsed) | ERROR (expanded) |
|---|---|---|---|---|---|
| Trigger border | selected-field/color/{state}/border | #D7E0EF | #005CE5 | #F4C7C9 | #D61B2C |
| Trigger bg | selected-field/color/{state}/bg | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF |
| Placeholder | selected-field/color/{state}/placeholder | #90A8D0 | #90A8D0 | #90A8D0 | #90A8D0 |
| Chevron icon | selected-field/color/{state}/icon | #005CE5 | #005CE5 | #005CE5 | #005CE5 |
| Peso sign (Amount) | selected-field/color/{state}/icon-currency | #183462 | #183462 | – | – |
| Header label | formgroup-header/color/label | #0A2757 | #0A2757 | #0A2757 | #0A2757 |
| Item label | dropdown-item/color/default/label | – | #0A2757 | – | #0A2757 |
| Item border | dropdown-item/color/default/border | – | #E5EBF4 | – | #E5EBF4 |
| Dropdown bg | bg/color-bg-main | – | #FFFFFF | – | #FFFFFF |
| Subtext | text/color-text-weak | #445C85 | #445C85 | – | – |
| Error subtext | border/color-border-destructive | – | – | #D61B2C | #D61B2C |
| Property | Value |
|---|---|
| Trigger height | 46px |
| Corner radius | 6px (radius-2) |
| Trigger padding | 6px top, 8px bottom, 12px horizontal |
| Chevron size | 32 × 32 |
| Peso sign size (Amount) | 15 × 15 |
| Item padding | 16px vertical, 12px left, 16px right |
| Dropdown corner radius | 6px |
| Dropdown shadow | 0 6px 12px rgba(2,14,34,0.16) |
| Header padding bottom | 8px |
| Layer | Text Style | Font | Size | Tracking | Line-height |
|---|---|---|---|---|---|
| Header label | Primary/Label/Light/Small | Proxima Soft Semibold | 14px | 0.25px | 14px |
| Trigger placeholder | Primary/Label/Light/Small | Proxima Soft Semibold | 14px | 0.25px | 14px |
| Dropdown item | Primary/Label/Light/Large | Proxima Soft Semibold | 18px | 0.25px | 18px |
| Subtext | — | BarkAda Semibold | 12px | 0px | 18px |
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:dropdown:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.dropdown.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| variant=Text | EBDropdown(label:items:) | EBDropdown(label, items) | Default text selection |
| variant=Error | .ebError(true) | isError=true | Validation failed state |
| variant=Amount | .ebStyle(.amount) | style=EBDropdownStyle.Amount | Shows peso sign prefix |
| variant=Mobile | .ebStyle(.mobile) | style=EBDropdownStyle.Mobile | Country code + phone input |
| type=Collapsed | — | — | Default closed state (managed internally) |
| type=Expanded | isPresented: Binding<Bool> | expanded: Boolean | Dropdown list visible |
| subtext (boolean) | helperText: String? | helperText: String? | Optional helper/error text |
EBDropdown("Category", selection: $category) { ForEach(categories) { item in Text(item.name) } }
EBDropdown( label = "Category", items = categories, selectedItem = selected, onItemSelected = { selected = it } )
EBDropdown("Category", selection: $category) { ForEach(categories) { item in Text(item.name) } } .ebError(true) .ebHelperText("Please select a category")
EBDropdown( label = "Category", items = categories, selectedItem = selected, onItemSelected = { selected = it }, isError = true, helperText = "Please select a category" )
EBDropdown("Amount", selection: $amount) { ForEach(amounts) { item in Text(item.formatted) } } .ebStyle(.amount)
EBDropdown( label = "Amount", items = amounts, selectedItem = selected, onItemSelected = { selected = it }, style = EBDropdownStyle.Amount )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 × 44 pt | 48 × 48 dp |
| Accessibility label | .accessibilityLabel("Select category") | contentDescription |
| Role | .accessibilityAddTraits(.isButton) | semantics { role=Role.DropdownList } |
| Expanded state | VoiceOver: "collapsed" / "expanded" | TalkBack: announce expansion state |
| Item selection | .accessibilityValue(selectedItem) | semantics { stateDescription } |
Do
Use Dropdown for selecting from a predefined list of options. Label the trigger clearly so users know what they're selecting.
Don't
Use Dropdown for free-text entry — use Input Field instead. Dropdown is for constrained selection only.
Do
Show error state with helper text below the field explaining the validation issue.
Don't
Use the Mobile variant for generic dropdown needs — it bundles phone-specific UI that adds complexity without value.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic names: label, container, text-container, peso-sign, Chevron Up/Down. Minor: dropdowncontainer missing separator. |
| C2 | Variant & Property Naming | Partial | DropdownItem selected uses yes/no instead of true/false. type is a generic property name. |
| C3 | Token Coverage | Ready | All colors bound to design tokens. Space, radius, typography, and elevation tokens all present. |
| C4 | Native Mappability | Partial | Text/Error/Amount map to Menu (iOS) / ExposedDropdownMenuBox (Android). Mobile variant needs custom composition. |
| C5 | Interaction State Coverage | Needs Fix | Missing disabled and pressed states. Only Collapsed, Expanded, and Error defined. |
| C6 | Asset & Icon Quality | Partial | Chevrons are vector instances. Amount variant Peso Sign uses BOOLEAN_OPERATION (shape_full). |
| C7 | Code Connect Linkability | Pending | No CLI mappings registered yet. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Partial | DropdownItem selected needs boolean rename; type is generic |
| Asset quality | Partial | Peso Sign BOOLEAN_OPERATION in Amount variant |
| State coverage | Blocked | Missing disabled/pressed states blocks complete mapping |
| Native component file | Pending | EBDropdown.swift / EBDropdown.kt not yet created |
4 variant values × 2 type values (Collapsed/Expanded). subtext boolean toggleable on all variants.
| variant | type | Node ID |
|---|---|---|
| Text | Collapsed | 23:199481 |
| Text | Expanded | 23:199486 |
| Error | Collapsed | 883:30631 |
| Error | Expanded | 883:30686 |
| Amount | Collapsed | 23:199491 |
| Amount | Expanded | 23:199496 |
| Mobile | Collapsed | 23:199501 |
| Mobile | Expanded | 6388:4818 |
selected=yes/no instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Openshape_full is a BOOLEAN_OPERATION, not a clean vector path. May render inconsistently on native platforms. OpenA basic text input field with border stroke and placeholder text. 8 variants across State (Default/Active/Error/Disabled) × isFilled (Yes/No). Part of the Form Elements group — serves as the base primitive for Labeled Field, Select Field, and Recipient Field.
Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle state and fill to see the input field update in real time.
isFilled now uses true/false. C1 resolved — text layer renamed from #text-label to #label, consistent with sibling fields.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | Gray #D7E0EF border, white bg. |
| Active (Focused) | Yes | Yes | State=Active | Blue #005CE5 border. |
| Error | Yes | Yes | State=Error | Red #D61B2C border. |
| Disabled | Yes | Yes | State=Disabled | #EEF2F9 bg, border hidden. |
isFilledproperty renamed fromYes/Nototrue/false— now maps directly to SwiftBool/ KotlinBooleanC2 Fixed- Text layer renamed from
#text-labelto#label— now consistent with sibling fields C1 Fixed
- Code Connect CLI mappings not registered C7
- Continue standardizing text layer naming across all Form Elements — Input Field now uses
#label; verify remaining fields follow suit Suggested - Consider adding a
helperTextslot below the field for validation messages and hints — currently handled externally Suggested - Add a
leadingIconandtrailingIconboolean slot to Input Field (Labeled Field already has these) for search inputs and clear buttons Suggested
8 variants across 2 axes: State (Default/Active/Error/Disabled) × isFilled (Yes/No). All share the same 366×46px container with 6px corner radius.
Idle state with gray border. Text color depends on whether the field has a value.
Focused state with blue border indicating active input.
Validation error state with red border.
Non-interactive state with gray background and hidden border.
All states share the same container structure. Border color is the primary state indicator.
| Role | Token | DEFAULT | ACTIVE | ERROR | DISABLED |
|---|---|---|---|---|---|
| Border | field/border | #D7E0EF | #005CE5 | #D61B2C | hidden |
| Background | field/bg | #FFFFFF | #FFFFFF | #FFFFFF | #EEF2F9 |
| Text (filled) | field/text/filled | #0A2757 | #0A2757 | #0A2757 | #90A8D0 |
| Text (empty) | field/text/placeholder | #90A8D0 | #90A8D0 | #90A8D0 | #C2CFE5 |
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:form-elements:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.form.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| isFilled (Yes/No) | text: Binding<String> | value: String | Derived from text content |
| State=Default | — | — | Default idle state |
| State=Active | .focused() | interactionSource | Keyboard active |
| State=Error | .ebError(true) | isError=true | Validation failed |
| State=Disabled | .disabled(true) | enabled=false | Non-interactive |
EBInputField("Placeholder", text: $value)
EBInputField( value = text, onValueChange = { text = it }, placeholder = "Placeholder" )
EBInputField("Placeholder", text: $value) .ebError(true)
EBInputField( value = text, onValueChange = { text = it }, placeholder = "Placeholder", isError = true )
EBInputField("Placeholder", text: $value) .disabled(true)
EBInputField( value = text, onValueChange = { text = it }, placeholder = "Placeholder", enabled = false )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 x 44 pt | 48 x 48 dp |
| Accessibility label | .accessibilityLabel("Input") | contentDescription |
| Error announcement | VoiceOver reads error via .accessibilityValue | TalkBack reads error via semantics { error() } |
Do
Pair with a visible label above or inside the field. Use placeholder text to hint at expected input format.
Don't
Use placeholder text as the only label — it disappears on focus and fails accessibility.
Do
Show error state with a helper text message below the field explaining what needs to be corrected.
Don't
Use Input Field for selection — use Select Field instead. Input Field is for free-text entry only.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Layer renamed to #label, now consistent with sibling fields. |
| C2 | Variant & Property Naming | Ready | isFilled now uses true/false. Boolean naming resolved. |
| C3 | Token Coverage | Partial | Colors appear correct but token binding not verified. |
| C4 | Native Mappability | Ready | Maps to TextField (SwiftUI) / OutlinedTextField (Compose). |
| C5 | Interaction State Coverage | Ready | All 4 states defined: Default, Active, Error, Disabled. |
| C6 | Asset & Icon Quality | Ready | No icons in base Input Field — text only. |
| C7 | Code Connect Linkability | Pending | No CLI mappings registered yet. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Ready | isFilled uses true/false — maps directly to native booleans |
| State coverage | Ready | All 4 states defined |
| Native component file | Pending | EBInputField.swift / EBInputField.kt not yet created |
4 State values × 2 isFilled values.
| State | isFilled | Node ID |
|---|---|---|
| Default | true | 17758:3688 |
| Default | false | 17758:3691 |
| Active | true | 17758:3694 |
| Active | false | 17758:3697 |
| Error | true | 17758:3700 |
| Error | false | 17758:3703 |
| Disabled | true | 17758:3706 |
| Disabled | false | 17758:3709 |
isFilled=Yes/No updated to isFilled=true/false in Figma. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. Fixed#text-label renamed to #label in Figma. Now consistent with sibling fields (Labeled Field, Select Field, Recipient Field). FixedisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. OpenAn enhanced form field with leading icon, label + value text container, an XSmall action button, and trailing icon. 8 variants across State (Default/Active/Error/Disabled) × isFilled (true/false). Part of the Form Elements group — extends the base Input Field pattern with icon slots and an embedded action button.
Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle state and fill to see the labeled field update in real time.
isFilled now uses true/false and property renamed to State. Action button layer renamed to action-button (C1 resolved).trailing-icon uses a rectangle placeholder instead of a swappable icon instance (C6), limiting icon customization at the consumer level.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | Gray #D7E0EF border, white bg. |
| Active (Focused) | Yes | Yes | State=Active | Blue #005CE5 border. |
| Error | Yes | Yes | State=Error | Red #D61B2C border. |
| Disabled | Yes | Yes | State=Disabled | #EEF2F9 bg, border hidden. |
isFilledrenamed fromYes/Nototrue/false— now maps directly to SwiftBool/ KotlinBooleanC2 Fixed- Property
staterenamed toState(capitalized) — consistent with sibling Form Elements fields C2 Fixed Button - XSmalllayer renamed toaction-button— now a semantic slot name for flexible consumer customization C1 Fixed- Trailing icon uses shared Placeholder component instance — swappable by design. Internal RECTANGLE is the default visual, replaced by designers when consuming the component C6 Closed
- Code Connect CLI mappings not registered C7
- Replace
Button - XSmallhardcoded instance with a namedactionslot so consumers can swap in different button variants or remove the action entirely Suggested - Replace
icon-placeholderRECTANGLE intrailing-iconwith a swappable icon instance — enables consumers to override the trailing icon via Figma instance swap Suggested - Consider adding a
helperTextslot below the field for validation messages — currently handled externally Suggested
8 variants across 2 axes: State (Default/Active/Error/Disabled) × isFilled (true/false). All share the same 46px height container with 6px corner radius. Each variant includes leading icon, text container (label + value), XSmall action button, and trailing icon.
Idle state with gray border. Leading/trailing icon placeholders, label + value text container, and XSmall action button.
Focused state with blue border indicating active input.
Validation error state with red border.
Non-interactive state with gray background and hidden border.
All states share the same container structure. Border color is the primary state indicator. Text colors depend on isFilled (true/false).
| Role | Token | DEFAULT | ACTIVE | ERROR | DISABLED |
|---|---|---|---|---|---|
| Border | field/border | #D7E0EF | #005CE5 | #D61B2C | hidden |
| Background | field/bg | #FFFFFF | #FFFFFF | #FFFFFF | #EEF2F9 |
| Label (filled) | field/text/label | #0A2757 | #0A2757 | #0A2757 | #90A8D0 |
| Value (filled) | field/text/value | #0A2757 | #0A2757 | #0A2757 | #C2CFE5 |
| Value (empty) | field/text/placeholder | #90A8D0 | #90A8D0 | #90A8D0 | #C2CFE5 |
| Height | 46px |
| Corner radius | 6px |
| Leading icon | 24 × 24 |
| Trailing icon | 24 × 24 |
| Action button | 60 × 24 (radius 99) |
| Font | Proxima Soft |
| Weight | Semibold (600) |
| Size | 14px |
| Letter spacing | 0.25 |
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:form-elements:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.form.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| isFilled (true/false) | text: Binding<String> | value: String | Derived from text content |
| State=Default | — | — | Default idle state |
| State=Active | .focused() | interactionSource | Keyboard active |
| State=Error | .ebError(true) | isError=true | Validation failed |
| State=Disabled | .disabled(true) | enabled=false | Non-interactive |
| #label (TEXT) | label: String | label: String | Field label text |
| #value (TEXT) | text: Binding<String> | value: String | Field value text |
| leading-icon | leadingIcon: Image? | leadingIcon: @Composable? | 24×24 icon slot |
| trailing-icon | trailingIcon: Image? | trailingIcon: @Composable? | 24×24 icon slot (C6: rectangle placeholder) |
| action-button | action: EBFieldAction? | action: @Composable? | Semantic slot name (C1 resolved) |
EBLabeledField("Label", text: $value) .leadingIcon(Image("icon-placeholder")) .trailingIcon(Image("chevron-right"))
EBLabeledField( value = text, onValueChange = { text = it }, label = "Label", leadingIcon = { Icon(Icons.Default.Placeholder, null) }, trailingIcon = { Icon(Icons.Default.ChevronRight, null) } )
EBLabeledField("Label", text: $value) .leadingIcon(Image("icon-placeholder")) .ebError(true)
EBLabeledField( value = text, onValueChange = { text = it }, label = "Label", leadingIcon = { Icon(Icons.Default.Placeholder, null) }, isError = true )
EBLabeledField("Label", text: $value) .leadingIcon(Image("icon-placeholder")) .disabled(true)
EBLabeledField( value = text, onValueChange = { text = it }, label = "Label", leadingIcon = { Icon(Icons.Default.Placeholder, null) }, enabled = false )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 x 44 pt | 48 x 48 dp |
| Accessibility label | .accessibilityLabel("Label") | contentDescription |
| Error announcement | VoiceOver reads error via .accessibilityValue | TalkBack reads error via semantics { error() } |
| Action button label | .accessibilityLabel("Action") on button | contentDescription on button |
Do
Use Labeled Field when the input needs a persistent label above the value, a leading icon for context, and an optional action button.
Don't
Use Labeled Field for simple text entry — use Input Field instead. Labeled Field is for complex form rows with icon context.
Do
Provide meaningful icons in the leading and trailing slots — they help users identify the field purpose at a glance.
Don't
Leave the icon placeholders as-is in production — always swap in a contextual icon or hide the slot.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Layer renamed to action-button, now a semantic slot name. |
| C2 | Variant & Property Naming | Ready | isFilled uses true/false. Property renamed to State (capitalized). Both fixes confirmed in Figma. |
| C3 | Token Coverage | Partial | Colors appear correct but token binding not verified. |
| C4 | Native Mappability | Ready | Maps to custom EBLabeledField on both platforms. |
| C5 | Interaction State Coverage | Ready | All 4 states defined: Default, Active, Error, Disabled. |
| C6 | Asset & Icon Quality | Partial | trailing-icon uses icon-placeholder RECTANGLE — not a swappable icon instance. |
| C7 | Code Connect Linkability | Pending | No CLI mappings registered yet. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Ready | isFilled=true/false and State (capitalized) — C2 fixed in Figma, ready for mapping |
| State coverage | Ready | All 4 states defined |
| Icon slots | Partial | leading-icon uses Placeholder instance (OK). trailing-icon uses RECTANGLE (blocked). |
| Action slot | Ready | Renamed to action-button — semantic slot name, ready for Code Connect mapping |
| Native component file | Pending | EBLabeledField.swift / EBLabeledField.kt not yet created |
4 State values × 2 isFilled values.
| State | isFilled | Node ID |
|---|---|---|
| Default | true | 17758:3714 |
| Default | false | 17758:3723 |
| Active | true | 17758:3732 |
| Active | false | 17758:3741 |
| Error | true | 17758:3750 |
| Error | false | 17758:3759 |
| Disabled | true | 17758:3768 |
| Disabled | false | 17758:3777 |
Button - XSmall renamed to action-button. Now uses a semantic slot name, enabling flexible consumer customization and clean Code Connect mapping. FixedisFilled values changed from Yes/No to true/false. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. Fixedstate renamed to State (capitalized) to align with sibling Form Elements fields (Input Field, etc.). FixedisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Openstate uses lowercase, inconsistent with other Form Elements using State (capitalized). OpenButton - XSmall is not a named action slot, limiting consumer customization. Openicon-placeholder in trailing-icon is a RECTANGLE, not a swappable icon instance. OpenA GCash-specific two-line input field for recipient/contact entry in money transfer flows (Send Money, Pay Bills, etc.). 8 variants across State (Default/Active/Error/Disabled) × isFilled (true/false). Features a small label on top, value/placeholder below, and two trailing action icons. Notably taller than other Form Elements at 56px (vs 46px for Input Field, Labeled Field, etc.) with 6px corner radius.
Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle state and fill to see the recipient field update in real time. Note the 56px height (taller than the standard 46px form field).
isFilled now uses true/false (C2 fixed). Value layer renamed to #value (C1 fixed) — now consistent with sibling fields.icon-placeholder RECTANGLEs (C6) — not swappable icon instances. Cannot compose different icon actions (phonebook, scan QR) without editing the component.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | Gray #D7E0EF border, white bg. Label + placeholder/value visible. |
| Active (Focused) | Yes | Yes | State=Active | Blue #005CE5 border. |
| Error | Yes | Yes | State=Error | Red #D61B2C border. |
| Disabled | Yes | Yes | State=Disabled | #EEF2F9 bg, border hidden. Muted label and text. |
isFilledproperty renamed fromYes/Nototrue/false— now maps directly to SwiftBool/ KotlinBooleanC2 Fixed- Text layer renamed from
#text-placeholderto#value— now consistent with sibling fields (Input Field, Labeled Field) C1 Fixed - Both trailing icons use shared Placeholder component instances — swappable by design. Internal RECTANGLE is the default visual, replaced by designers when consuming the component C6 Closed
- Code Connect CLI mappings not registered C7
- Replace
icon-placeholderRECTANGLEs with swappable icon component instances — enables phonebook, scan QR, or other action icons without editing the base component Suggested - Document the 56px height rationale — clarify in DS guidelines why Recipient Field is taller than the standard 46px form fields (two-line layout: label + value) Suggested
- Consider adding an
errorMessageslot below the field for inline validation text, consistent with other form elements Suggested
8 variants across 2 axes: State (Default/Active/Error/Disabled) × isFilled (true/false). All share the same container with 6px corner radius. Height is 56px — taller than other Form Elements (46px) to accommodate the two-line label + value layout.
Idle state with gray border. Two-line layout: small label above, value/placeholder below. Two trailing icon placeholders.
Focused state with blue border indicating active input.
Validation error state with red border.
Non-interactive state with gray background and hidden border. Muted label and text colors.
All states share the same two-line container (56px height, 6px radius). Border color is the primary state indicator. Two trailing icon placeholders use a fixed fill across all states.
| Role | Token | DEFAULT | ACTIVE | ERROR | DISABLED |
|---|---|---|---|---|---|
| Border | field/border | #D7E0EF | #005CE5 | #D61B2C | hidden |
| Background | field/bg | #FFFFFF | #FFFFFF | #FFFFFF | #EEF2F9 |
| Label text | field/label | #0A2757 | #0A2757 | #0A2757 | #90A8D0 |
| Value (filled) | field/text/filled | #0A2757 | #0A2757 | #0A2757 | #90A8D0 |
| Value (empty) | field/text/placeholder | #90A8D0 | #90A8D0 | #90A8D0 | #C2CFE5 |
| Icon placeholder | field/icon | #C2C6CF | #C2C6CF | #C2C6CF | #C2C6CF |
| Height | 56px (vs 46px standard) |
| Corner radius | 6px |
| Border | 1px stroke |
| Icon group | 68 × 32px |
| Icon size | 32 × 32px each |
| Icon radius | 41px (circular) |
| #label (top line) | |
| Font | Proxima Soft Semibold |
| Size | 12px |
| Tracking | 0.5 |
| #text-placeholder (bottom line) | |
| Font | Proxima Soft Semibold |
| Size | 14px |
| Tracking | 0.25 |
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:form-elements:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.form.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| isFilled (true/false) | text: Binding<String> | value: String | Derived from text content |
| #label | label: String | label: String | Small top label (12px) |
| #text-placeholder | placeholder: String | placeholder: String | Value text or placeholder (14px) |
| icon-group (2x icons) | trailingIcons: [Image] | trailingIcons: @Composable | Two action icon slots |
| State=Default | — | — | Default idle state |
| State=Active | .focused() | interactionSource | Keyboard active |
| State=Error | .ebError(true) | isError=true | Validation failed |
| State=Disabled | .disabled(true) | enabled=false | Non-interactive |
EBRecipientField( label: "Mobile Number", text: $recipientNumber, placeholder: "Enter number or name", trailingIcons: [ Image(systemName: "person.crop.circle"), Image(systemName: "qrcode.viewfinder") ] )
EBRecipientField( label = "Mobile Number", value = recipientNumber, onValueChange = { recipientNumber = it }, placeholder = "Enter number or name", trailingIcons = { IconButton(onClick = onContactsClick) { Icon(Icons.Default.Person, "Contacts") } IconButton(onClick = onScanClick) { Icon(Icons.Default.QrCode, "Scan QR") } } )
EBRecipientField( label: "Mobile Number", text: $recipientNumber, placeholder: "Enter number or name" ) .ebError(true)
EBRecipientField( label = "Mobile Number", value = recipientNumber, onValueChange = { recipientNumber = it }, placeholder = "Enter number or name", isError = true )
EBRecipientField( label: "Mobile Number", text: $recipientNumber, placeholder: "Enter number or name" ) .disabled(true)
EBRecipientField( label = "Mobile Number", value = recipientNumber, onValueChange = { recipientNumber = it }, placeholder = "Enter number or name", enabled = false )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 x 44 pt (56px field exceeds) | 48 x 48 dp (56px field exceeds) |
| Accessibility label | .accessibilityLabel("Recipient") | contentDescription |
| Error announcement | VoiceOver reads error via .accessibilityValue | TalkBack reads error via semantics { error() } |
| Trailing icon labels | .accessibilityLabel("Contacts") per icon | contentDescription per icon button |
Do
Use Recipient Field for contact/number entry in money transfer flows (Send Money, Pay Bills, Buy Load). The two-line layout with trailing icons is purpose-built for this context.
Don't
Use Recipient Field for general text input — use Input Field or Labeled Field instead. The 56px height and icon slots add unnecessary weight for simple text entry.
Do
Provide meaningful trailing icons (e.g. contacts picker, QR scanner) that match the field's purpose. Both slots should have distinct actions.
Don't
Leave icon placeholders as-is in production — they are design placeholders only. Always replace with real icon instances before handoff.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Layer renamed to #value, now consistent with sibling fields. |
| C2 | Variant & Property Naming | Ready | isFilled=true/false — correctly uses native boolean values. |
| C3 | Token Coverage | Partial | Colors appear correct but token binding not verified. Icon placeholders use hardcoded #C2C6CF. |
| C4 | Native Mappability | Ready | Maps to custom EBRecipientField (SwiftUI + Compose). Two-line layout with trailing icon slots. |
| C5 | Interaction State Coverage | Ready | All 4 states defined: Default, Active, Error, Disabled. |
| C6 | Asset & Icon Quality | Needs Fix | Both trailing icons are icon-placeholder RECTANGLEs — not swappable icon instances. |
| C7 | Code Connect Linkability | Pending | No CLI mappings registered yet. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Ready | isFilled=true/false — boolean values map directly to native types |
| Layer naming | Blocked | #text-placeholder needs rename to #value |
| Icon slots | Blocked | RECTANGLE placeholders — need swappable icon instances |
| State coverage | Ready | All 4 states defined |
| Native component file | Pending | EBRecipientField.swift / EBRecipientField.kt not yet created |
4 State values × 2 isFilled values (true/false).
| State | isFilled | Node ID |
|---|---|---|
| Default | true | 17758:3868 |
| Default | false | 17758:3875 |
| Active | true | 17758:3882 |
| Active | false | 17758:3889 |
| Error | true | 17758:3896 |
| Error | false | 17758:3903 |
| Disabled | true | 17758:3910 |
| Disabled | false | 17758:3917 |
#text-placeholder to #value. Now consistent with sibling fields (Input Field, Labeled Field) for direct native property mapping. C1 FixedisFilled updated from Yes/No to true/false. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. C2 FixedisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Fixed in 1.1.0#text-placeholder instead of #value used by other Form Elements. Fixed in 1.2.0icon-placeholder RECTANGLEs, not component instances. OpenA currency/amount selection field specific to GCash. Includes a peso sign, label, value text, Philippine flag indicator, and a chevron down affordance. 8 variants across State (Default/Active/Error/Disabled) × isFilled (true/false). Part of the Form Elements group — used for currency and amount selection contexts.
Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle state and fill to see the select field update in real time.
isFilled now uses true/false (C2 fixed). Peso sign still uses shape_full BOOLEAN_OPERATION instead of a clean vector (C6).| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | Gray #D7E0EF border, white bg. Peso sign #183462. |
| Active (Focused) | Yes | Yes | State=Active | Blue #005CE5 border. |
| Error | Yes | Yes | State=Error | Red #D61B2C border. |
| Disabled | Yes | Yes | State=Disabled | #EEF2F9 bg, border hidden. Peso sign #7E96BE. |
isFilledrenamed fromYes/Nototrue/falsefor direct SwiftBool/ KotlinBooleanmapping C2 Fixed- Peso Sign
shape_fullBOOLEAN_OPERATION flattened to a single vector path across all 8 variants C6 Fixed - Field Trailing Flag replaced from raster IMAGE fill to vector SVG across all 8 variants C6 Fixed
- Code Connect CLI mappings not registered C7
- Replace the
shape_fullBOOLEAN_OPERATION in Peso Sign with a single flattened vector path for consistent cross-platform rendering Suggested - Provide a vector version of the Philippine flag or use an icon instance from the DS icon library instead of a raster IMAGE fill Suggested
- Consider generalizing the field to support other currency symbols and flags via configurable slots — would increase reuse across multi-currency flows Suggested
- Add a
helperTextslot below the field for validation messages — currently error state has no text guidance Suggested
8 variants across 2 axes: State (Default/Active/Error/Disabled) × isFilled (true/false). All share the same 46px height container with 6px corner radius. Includes peso sign (15×15), label/value text, Philippine flag (25×16), and chevron down (32×32).
Idle state with gray border. Peso sign in dark navy, flag visible, chevron down affordance.
Focused state with blue border indicating active selection.
Validation error state with red border.
Non-interactive state with gray background, hidden border, and muted peso sign.
All states share the same container structure. Border color is the primary state indicator. Peso sign and text colors shift in disabled state.
| Role | Token | DEFAULT | ACTIVE | ERROR | DISABLED |
|---|---|---|---|---|---|
| Border | field/border | #D7E0EF | #005CE5 | #D61B2C | hidden |
| Background | field/bg | #FFFFFF | #FFFFFF | #FFFFFF | #EEF2F9 |
| Label text | field/text/label | #0A2757 | #0A2757 | #0A2757 | #0A2757 |
| Value (filled) | field/text/filled | #0A2757 | #0A2757 | #0A2757 | #90A8D0 |
| Value (empty) | field/text/placeholder | #90A8D0 | #90A8D0 | #90A8D0 | #C2CFE5 |
| Peso sign | field/icon/peso | #183462 | #183462 | #183462 | #7E96BE |
| Property | Value |
|---|---|
| Height | 46px |
| Corner radius | 6px |
| Peso sign size | 15 × 15 |
| Flag size | 25 × 16 |
| Flag corner radius | 2px |
| Chevron size | 32 × 32 |
| Layer | Property | Value |
|---|---|---|
| #label | Font | Proxima Soft Semibold |
| Size | 16px | |
| #value | Font | Proxima Soft |
| Size | 14px |
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:form-elements:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.form.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| isFilled (true/false) | selection: Binding<String?> | selectedValue: String? | Derived from selection content |
| State=Default | — | — | Default idle state |
| State=Active | .focused() | interactionSource | Selection active |
| State=Error | .ebError(true) | isError=true | Validation failed |
| State=Disabled | .disabled(true) | enabled=false | Non-interactive |
EBSelectField("Amount", selection: $amount)
EBSelectField( label = "Amount", selectedValue = amount, onValueChange = { amount = it } )
EBSelectField("Amount", selection: $amount) .ebError(true)
EBSelectField( label = "Amount", selectedValue = amount, onValueChange = { amount = it }, isError = true )
EBSelectField("Amount", selection: $amount) .disabled(true)
EBSelectField( label = "Amount", selectedValue = amount, onValueChange = { amount = it }, enabled = false )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 x 44 pt | 48 x 48 dp |
| Accessibility label | .accessibilityLabel("Select amount") | contentDescription |
| Role hint | .accessibilityHint("Double tap to select") | semantics { role=Role.DropdownList } |
| Error announcement | VoiceOver reads error via .accessibilityValue | TalkBack reads error via semantics { error() } |
Do
Use Select Field for currency amount selection where the peso sign and flag indicator provide essential context for the user.
Don't
Use Select Field for free-text entry — use Input Field instead. Select Field is for predefined selection only.
Do
Show error state with a helper text message below the field explaining the validation issue (e.g. "Minimum amount is 1.00").
Don't
Hide the peso sign or flag — these are essential visual cues that distinguish this field from a generic dropdown.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic layer names: container, peso-sign, text-container, flag-container, Chevron Down. |
| C2 | Variant & Property Naming | Ready | isFilled=true/false — correct boolean convention for native mapping. |
| C3 | Token Coverage | Partial | Colors appear correct but token binding not fully verified. |
| C4 | Native Mappability | Ready | Maps to custom EBSelectField (SwiftUI) / EBSelectField (Compose). |
| C5 | Interaction State Coverage | Ready | All 4 states defined: Default, Active, Error, Disabled. |
| C6 | Asset & Icon Quality | Needs Fix | Peso sign uses shape_full BOOLEAN_OPERATION (not a vector). Flag uses raster IMAGE fill. |
| C7 | Code Connect Linkability | Pending | No CLI mappings registered yet. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Ready | isFilled=true/false — boolean convention now correct for Code Connect mapping |
| Asset quality | Blocked | Peso sign BOOLEAN_OPERATION and raster flag need replacement |
| State coverage | Ready | All 4 states defined |
| Native component file | Pending | EBSelectField.swift / EBSelectField.kt not yet created |
4 State values × 2 isFilled values (true/false).
| State | isFilled | Node ID |
|---|---|---|
| Default | true | 17758:3787 |
| Default | false | 17758:3797 |
| Active | true | 17758:3807 |
| Active | false | 17758:3817 |
| Error | true | 17758:3827 |
| Error | false | 17758:3837 |
| Disabled | true | 17758:3847 |
| Disabled | false | 17758:3857 |
Bool / Kotlin Boolean mapping for Code Connect. FixedisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Fixed in 1.1.0shape_full is a BOOLEAN_OPERATION, not a clean vector path. May render inconsistently on native platforms. Openflag-container uses a raster IMAGE fill instead of a vector. May degrade on high-density displays. OpenAn app-level navigation title bar used at the top of every screen. Includes an iOS status bar stub (44px), a centered title row with optional leading icon (back arrow), trailing icon, leading control ("Done" text), subtext (URL), and an optional title block for large headers. 20 variants across 5 boolean properties. Background is brand blue (#1972F9), all text and icons white.
Contexts are illustrative. Final screens will reference actual GCash patterns.
Toggle properties to see the title bar update in real time.
yes/no instead of true/false (C2). leading control only available when leading icon=yes and trailing icon=no -- implicit dependency not expressed in property schema.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | 5 boolean properties | Navigation bar. No interaction states beyond tap targets on icons and control text. |
Title Bar is a navigation container. Interaction states (pressed, focused) apply to the individual tap targets (leading icon, trailing icon, leading control) rather than the bar itself. These states are not represented as separate variants in the component.
- Boolean properties (
leading icon,trailing icon,leading control,subtext,title block) useyes/noinstead oftrue/false-- incompatible with SwiftBooland KotlinBooleanC2 - Trailing icon uses
icon-placeholderRECTANGLE (24x24) instead of a swappable icon instance -- blocks native icon slot mapping C6 - Code Connect CLI mappings not registered C7
- Rename boolean properties from
yes/nototrue/falsefor direct native boolean mapping Suggested - Replace trailing icon
icon-placeholderRECTANGLE with a swappable icon instance from the DS icon library (e.g. more/ellipsis, share, search) Suggested - Make
leading controldependency explicit -- consider a separate property or document that it requiresleading icon=yesandtrailing icon=noSuggested - Consider adding a dark/transparent variant for screens with hero images or gradient backgrounds Suggested
- Add
showAssetproperty documentation -- currently only available whentitle block=yes; its purpose (background image) should be explicitly described in the component spec Suggested
20 variants across 2 structural configurations: Standard (no title block, 10 variants) and With Title Block (titleBlock=yes, 10 variants). Each configuration has the same 10 boolean property combinations for leading icon, trailing icon, leading control, and subtext.
Standard title bar without title block. Status bar (44px) + title row with optional icons, control, and subtext. Height ranges from 84px to 100px depending on subtext.
Title bar with expanded header block (72px) below the title row. Used for screens with prominent section headers. Adds "Header" text at 26px Semibold.
Single color scheme -- no appearance modes. All colors bound to main/title-bar/color/ tokens. Display/navigation component with no state-driven color changes.
| Role | Token | Value |
|---|---|---|
| Background | main/title-bar/color/bg | #1972F9 |
| Title label | main/title-bar/color/label-title | #FFFFFF |
| Header label | main/title-bar/color/label-header | #FFFFFF |
| Subtext / URL | main/title-bar/color/label-url | #F6F9FDCC (80% opacity) |
| CTA text | main/title-bar/color/label-cta | #FFFFFF |
| Icon | main/title-bar/color/icon | #FFFFFF |
| Property | Value |
|---|---|
| Status bar height | 44px |
| Title row padding H | 20px |
| Title row padding V | 12px |
| Leading icon size | 24 x 24 |
| Trailing icon size | 24 x 24 |
| Title block height | 72px |
| Title block padding H | 24px |
| Total height (no subtext, no block) | ~84px |
| Total height (with subtext, no block) | ~100px |
| Total height (with block) | ~156--172px |
| Layer | Text Style | Font | Size | Tracking | Line-height |
|---|---|---|---|---|---|
| Title | Primary/Label/Light/Base | Proxima Soft Semibold | 16px | 0.25px | 16px |
| Subtext | Primary/Label/Light/Fine | Proxima Soft Semibold | 12px | 0.5px | 12px |
| Header | Primary/Headlines/Light/Area | Proxima Soft Semibold | 26px | 0.85px | 31px |
| CTA (control) | Primary/Label/Light/Small | Proxima Soft Semibold | 14px | 0.25px | 14px |
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:titlebar:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.titlebar.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI | Compose | Notes |
|---|---|---|---|
| leading icon (yes/no) | .ebLeadingIcon(Image?) | leadingIcon: @Composable (() -> Unit)? | Back arrow, optional |
| trailing icon (yes/no) | .ebTrailingIcon(Image?) | trailingIcon: @Composable (() -> Unit)? | Action icon, optional |
| leading control (yes/no) | .ebLeadingControl("Done") | leadingControlText: String? | CTA text, replaces trailing icon |
| subtext (yes/no) | .ebSubtext("m.gcash.com") | subtext: String? | URL or secondary text below title |
| title block (yes/no) | .ebTitleBlock("Header") | titleBlock: String? | Large header below title row |
EBTitleBar("Send Money")
EBTitleBar( title = "Send Money" )
EBTitleBar("Send Money") .ebLeadingIcon(Image(systemName: "arrow.left"))
EBTitleBar( title = "Send Money", leadingIcon = { Icon(Icons.Default.ArrowBack, "Back") } )
EBTitleBar("GCash") .ebLeadingIcon(Image(systemName: "arrow.left")) .ebTrailingIcon(Image(systemName: "ellipsis")) .ebSubtext("m.gcash.com") .ebTitleBlock("My Wallet")
EBTitleBar( title = "GCash", leadingIcon = { Icon(Icons.Default.ArrowBack, "Back") }, trailingIcon = { Icon(Icons.Default.MoreVert, "More") }, subtext = "m.gcash.com", titleBlock = "My Wallet" )
EBTitleBar("Edit Profile") .ebLeadingIcon(Image(systemName: "arrow.left")) .ebLeadingControl("Done")
EBTitleBar( title = "Edit Profile", leadingIcon = { Icon(Icons.Default.ArrowBack, "Back") }, leadingControlText = "Done" )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 x 44 pt (icons and control) | 48 x 48 dp (icons and control) |
| Back button label | .accessibilityLabel("Back") | contentDescription="Navigate back" |
| Trailing icon label | .accessibilityLabel("More options") | contentDescription="More options" |
| Heading semantics | .accessibilityAddTraits(.isHeader) on title | semantics { heading() } on title |
Do
Use EBTitleBar as the top-level navigation element on every screen. Keep the title short and descriptive.
Don't
Nest a title bar inside scrollable content or use it as a section header within a page -- use a section heading component instead.
Do
Use the title block for high-level section headers like "My Wallet" or "Dashboard" where the large text reinforces the current context.
Don't
Show both trailing icon and leading control simultaneously -- they occupy the same trailing slot. Use one or the other.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic layer names: title, Title Bar, title-block, Leading Icon, Placeholder. |
| C2 | Variant & Property Naming | Needs Refinement | All 5 boolean properties use yes/no instead of true/false. leading control has implicit dependency on other properties. |
| C3 | Token Coverage | Ready | All 6 color roles bound to main/title-bar/color/ tokens. |
| C4 | Native Mappability | Ready | Maps to NavigationBar (iOS) / TopAppBar (Android, Material 3). |
| C5 | Interaction State Coverage | Ready | Navigation bar -- no interaction states needed beyond individual tap targets on icons and control. |
| C6 | Asset & Icon Quality | Needs Refinement | Trailing icon uses icon-placeholder RECTANGLE instead of a swappable icon instance. |
| C7 | Code Connect Linkability | Needs Refinement | No CLI mappings registered yet. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Needs Fix | All booleans use yes/no -- must be renamed to true/false before Code Connect mapping |
| Asset quality | Needs Fix | Trailing icon placeholder RECTANGLE needs replacement with icon instance |
| State coverage | Ready | Navigation bar -- no interaction states needed |
| Native component file | Pending | EBTitleBar.swift / EBTitleBar.kt not yet created |
5 boolean properties (leading icon, trailing icon, leading control, subtext, title block) with implicit constraints yield 20 variants: 10 without title block + 10 with title block.
| title block | Combinations covered | Count |
|---|---|---|
| no | 10 combos of leading/trailing icon + leading control + subtext | 10 |
| yes | Same 10 combos with title block enabled | 10 |
View full property combination breakdown (20 rows)
| leading icon | trailing icon | leading control | subtext | title block | Node ID |
|---|---|---|---|---|---|
| no | no | no | no | no | 23:175149 |
| no | no | no | yes | no | 23:175365 |
| no | yes | no | no | no | 23:175415 |
| no | yes | no | yes | no | 23:175427 |
| yes | yes | no | no | no | 23:175377 |
| yes | yes | no | yes | no | 23:175389 |
| yes | no | no | no | no | 23:175487 |
| yes | no | no | yes | no | 23:175499 |
| yes | no | yes | no | no | 23:175449 |
| yes | no | yes | yes | no | 23:175461 |
| no | no | no | no | yes | 23:175159 |
| no | no | no | yes | yes | 23:175169 |
| no | yes | no | no | yes | 23:175179 |
| no | yes | no | yes | yes | 23:175189 |
| yes | yes | no | no | yes | 23:175199 |
| yes | yes | no | yes | yes | 23:175209 |
| yes | no | no | no | yes | 23:175219 |
| yes | no | no | yes | yes | 23:175229 |
| yes | no | yes | no | yes | 23:175239 |
| yes | no | yes | yes | yes | 23:175249 |
main/title-bar/color/ tokens. Documentedleading icon, trailing icon, leading control, subtext, title block) use yes/no instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Openicon-placeholder is a 24x24 RECTANGLE instead of a swappable icon instance from the DS icon library. Blocks native icon slot mapping. OpenA modal with a hero image, title, description, and one or more CTAs. Used to display additional information, gather user input, or seek confirmation for critical actions. 3 variants: Default (single CTA), 2 CTA (primary + secondary), Version 2 (preamble, close icon, content-first layout for onboarding).
Default / 2 CTA / Version 2 (C2). Hero image is a raster placeholder with "Replace me" overlay instead of a swappable image slot (C6). No destructive/error/loading state coverage (C5). Code Connect mappings not registered (C7).Contexts are illustrative. Final screens will reference actual GCash patterns. Visual Popup overlays the app surface to confirm critical actions or onboard users to a new feature.
Toggle Type to see each variant. Hero image, title, description, and CTA(s) update accordingly.
main/modal-popup/color/bg), Shadow/Depth 0, radius/radius-2, and 24px padding. Composes Button instances rather than redefining button styles.Default (generic), 2 CTA (count), Version 2 (version). Native enums need a single semantic axis — e.g. single-cta / dual-cta / dismissible. C2| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Single CTA | Yes | Yes | Type=Default | Hero (180px) + title + description + primary CTA. Use for info or single-action confirm. |
| Dual CTA | Yes | Yes | Type=2 CTA | Adds a secondary outline + tertiary text button below primary. Use for cancel/confirm pairs. |
| Dismissible (V2) | Yes | Yes | Type=Version 2 | Preamble label + title with close icon + content-first layout. Use for onboarding/tutorial popups. |
| Destructive / Error / Loading | N/A | N/A | — | No variants for destructive confirms or async/loading states. C5 |
- Variant naming mixes paradigms —
Default(generic),2 CTA(count),Version 2(version). Native enum mapping needs one semantic axis. Suggested:single-cta/dual-cta/dismissible. C2 - No destructive/error/loading state coverage — engineers must improvise these for "Cancel transaction?", error confirms, and async submit flows. C5
- Hero image is a flat raster placeholder with a "Replace me" overlay. Should be a swappable Image slot (component instance) so product teams override per-popup without editing the master. C6
- Code Connect CLI mappings not registered. C7
- Rename property
Typevalues to a single semantic axis:single-cta,dual-cta,dismissible. Eliminates the version/count/default mix and maps cleanly toEBVisualPopupKindenum. Restructure - Replace the raster
Modals Assetwith a swappable Image slot — a component placeholder that product teams can instance-swap with their illustration. Matches the pattern Avatar uses for theimagetype. Asset slot - Add a
destructiveboolean (orkind=destructivemode) so destructive confirms (Cancel / Logout / Delete) can use red CTAs without bespoke overrides. State coverage - Consider a
loadingstate for the primary CTA — async submits in popups currently have no documented affordance. Could reuse the planned Button loading state. State coverage
3 layouts. Default and 2 CTA share the same hero-on-top structure; Version 2 inverts to content-first with the hero embedded inside a light-gray container.
Hero image (320 × 180, 16:9) + title + 2-line description + single primary CTA. Use for informational modals or single-action confirms ("Okay").
Same hero + title + description as Default, then a secondary outline button on top of a tertiary text button. Use for confirm/cancel pairs.
Onboarding/tutorial layout. The popup itself is a single light-gray (bg/color-bg) container — preamble label, title with close icon, description, a 280×180 hero image with 10px radius, then primary CTA. (The outer white frame has zero padding, so only the gray container is visible.)
Modal popup ships display-only color tokens — no pressed/disabled states (the popup itself doesn't have interaction states; CTAs handle that via Button tokens).
| Role | Token | Value |
|---|---|---|
| Modal background | main/modal-popup/color/bg | #FFFFFF |
| Title label | main/modal-popup/color/label | #0A2757 |
| Description label | main/modal-popup/color/label-primary | #6780A9 |
| Preamble (V2) | main/modal-popup/color/label-preamble | #90A8D0 |
| Close icon (V2) | main/modal-popup/color/icon-close | #6780A9 |
| V2 inner container | bg/color-bg | #F6F9FD |
| Property | Token | Value |
|---|---|---|
| Default / 2 CTA width | — | 320px |
| Version 2 width | — | 312px |
| Hero image (Default / 2 CTA) | — | 320 × 180 (16:9) |
| Hero image (V2) | — | 280 × 180, 10px radius |
| Body padding | space/space-24 | 24px |
| CTA group padding (vertical) | space/space-24 | 24px |
| 2 CTA gap between buttons | space/space-8 | 8px |
| V2 inner container padding | space/space-16 | 16px h, 16t / 24b |
| Corner radius | radius/radius-2 | 6px |
| Shadow | Shadow/Depth 0 | 0 0 4px #E8EEF2C9 |
| Close icon (V2) | — | 24 × 24 |
| Element | DS text style | Spec |
|---|---|---|
| Title | Primary/Headlines/Section | Proxima Soft Bold · 22 / 26 |
| Description | Secondary/Default/Base | BarkAda Medium · 14 / 20 |
| Preamble (V2) | Primary/Label/Tiny | Proxima Soft Bold · 10 / 10 · +0.25 |
| CTA label | Primary/Label/Large | Proxima Soft Bold · 18 / 18 · +0.25 |
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:visual-popup:1.0.0") }
| Figma Property | SwiftUI Param | Compose Param | Notes |
|---|---|---|---|
| Type=Default | .ebKind(.singleCTA) | kind=EBVisualPopupKind.SingleCTA | Hero + title + description + primary CTA |
| Type=2 CTA | .ebKind(.dualCTA) | kind=EBVisualPopupKind.DualCTA | Adds secondary outline + tertiary text button |
| Type=Version 2 | .ebKind(.dismissible) | kind=EBVisualPopupKind.Dismissible | Preamble + close icon + content-first layout |
| Hero image (raster) | heroImage: Image | heroImage: Painter | Currently a placeholder; should become a swappable slot |
| CTA buttons | primary / secondary / tertiary: EBButton | primary / secondary / tertiary: @Composable | Compose Button instances directly |
// Default — single CTA EBVisualPopup( title: "Cash In Successful", description: "₱500.00 added to your wallet.", heroImage: Image("cash-in-success"), primary: EBButton("Okay") { /* dismiss */ } ) .ebKind(.singleCTA) // 2 CTA — confirm/cancel EBVisualPopup( title: "Cancel transaction?", description: "This cannot be undone.", heroImage: Image("warning-illustration"), primary: EBOutlinedButton("Confirm") { /* confirm */ }, secondary: EBTextButton("Go Back") { /* dismiss */ } ) .ebKind(.dualCTA) // Version 2 — onboarding with close icon EBVisualPopup( preamble: "NEW", title: "Save your receipts", description: "Tap any transaction to save its receipt.", heroImage: Image("receipt-tutorial"), primary: EBButton("Got it") { /* dismiss */ }, onClose: { /* dismiss */ } ) .ebKind(.dismissible)
// Default — single CTA EBVisualPopup( kind = EBVisualPopupKind.SingleCTA, title = "Cash In Successful", description = "₱500.00 added to your wallet.", heroImage = painterResource(R.drawable.cash_in_success), primary = { EBButton("Okay", onClick = { /* dismiss */ }) } ) // 2 CTA — confirm/cancel EBVisualPopup( kind = EBVisualPopupKind.DualCTA, title = "Cancel transaction?", description = "This cannot be undone.", heroImage = painterResource(R.drawable.warning), primary = { EBOutlinedButton("Confirm", onClick = { /* confirm */ }) }, secondary = { EBTextButton("Go Back", onClick = { /* dismiss */ }) } ) // Version 2 — onboarding with close icon EBVisualPopup( kind = EBVisualPopupKind.Dismissible, preamble = "NEW", title = "Save your receipts", description = "Tap any transaction to save its receipt.", heroImage = painterResource(R.drawable.receipt_tutorial), primary = { EBButton("Got it", onClick = { /* dismiss */ }) }, onClose = { /* dismiss */ } )
| Requirement | iOS | Android |
|---|---|---|
| Modal trait / role | Present via .sheet or .alert — VoiceOver announces as modal | Dialog announces as modal; TalkBack focus trapped inside |
| Focus trap | Automatic with .sheet | Automatic with Dialog — set dismissOnClickOutside=false for confirm popups |
| Close button label (V2) | .accessibilityLabel("Close") | contentDescription="Close" |
| Hero image | If decorative: .accessibilityHidden(true). If informative: provide a label. | Same — contentDescription=null for decorative, otherwise describe |
| Tap targets | CTAs use Button which meets HIG 44pt | CTAs meet Material 48dp |
| Destructive role | Currently undefined — needs role: .destructive when state lands | Currently undefined — needs Button destructive colors when state lands |
Do
Use Visual Popup for critical confirms, success states with celebration, and onboarding moments — places where the hero image adds emotional weight.
Don't
Use for inline form errors or transient notifications — those belong in Toast, Banner, or inline error patterns, not a blocking modal.
Do
Use the Default variant when the popup has one obvious next step (Okay, Got it, Continue).
Don't
Use 2 CTA when one button is clearly more important than the other — that's still a Default with the secondary action elsewhere.
Do
Use Version 2 (with close icon) only for onboarding/tutorial popups where the user can dismiss without taking action.
Don't
Add a close icon to confirm/destructive popups — forces the user to consciously choose the CTA.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic names: Modals Asset, body, header, CTA - Base Button Group, Close. |
| C2 | Variant & Property Naming | Needs Fix | Property values mix paradigms: Default / 2 CTA / Version 2. Should be one semantic axis. |
| C3 | Token Coverage | Ready | All colors, spacing, radii, shadow, and typography bound to tokens. |
| C4 | Native Mappability | Ready | Maps to .sheet / .alert on iOS and Dialog / AlertDialog on Android. |
| C5 | Interaction State Coverage | Needs Fix | No destructive, error, or loading variants. Close affordance only on Version 2. |
| C6 | Asset & Icon Quality | Needs Fix | Hero is a flat raster placeholder with "Replace me" overlay. Should be a swappable Image slot. |
| C7 | Code Connect Linkability | Pending | No CLI mappings registered yet. |
| Type | Width | Hero | CTAs | Node ID |
|---|---|---|---|---|
| Default | 320px | 320 × 180 (raster) | 1 primary | 30:81517 |
| 2 CTA | 320px | 320 × 180 (raster) | 1 outline + 1 text | 7995:5265 |
| Version 2 | 312px | 280 × 180 (raster, in container) | 1 primary + close icon | 30:81515 |
Default (generic), 2 CTA (count), Version 2 (version). Should be a single semantic axis (single-cta / dual-cta / dismissible). OpenModals Asset image. Should be a swappable Image slot via instance swap. Open