East Blue
v0.1.0
Home
How It Works

Each component is evaluated against seven criteria. The report is self-contained and versions alongside the design system.

01 — Assess
Evaluate
Inspect the Figma component against 7 criteria covering structure, naming, tokens, states, and Code Connect readiness.
02 — Document
Write
Create a .html file in assessment-src/components/ with the nav, summary, and full assessment.
03 — Build
Assemble
Run node assessment-src/build.js to compile all component files into this page automatically.
04 — Ship
Publish
Commit and push. The report goes live on GitHub Pages — no framework, no build pipeline.
Assessment Criteria

The overall status reflects the weakest criterion — one unresolved issue can block Code Connect linkability.

IDCriterionWhat We Check
C1Layer Structure & NamingLayers 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.
C2Variant & Property NamingVariants and properties should follow clear, consistent conventions. Booleans expressed as true/false (not yes/no), enum values lowercase and hyphenated.
C3Token CoverageAll 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.
C4Native MappabilityThe component should map cleanly to a standard native primitive (e.g. DisclosureGroup, Button, List) with no web-only patterns that lack a native equivalent.
C5Interaction State CoverageAll expected interactive states should be defined as variants — default, pressed, focused, disabled, and error. Missing states force engineers to invent visual behavior.
C6Asset & Icon QualityIcons should be vector components (not raster or PNG embeds) and colored using tokens so tinting works natively on both platforms.
C7Code Connect LinkabilityThe component should be a proper Figma component set with property names clean enough to map 1:1 to native parameters via Code Connect.
Status Legend
StatusMeaning
ReadyLinkable as-is. Clean structure, maps well to native.
Needs RefinementMinor issues to resolve before linking.
Requires ReworkNeeds redesign before native translation.
Not ApplicableNo native equivalent.
FixResolved via Figma MCP. Residual items may remain.
Accordion
Expandable row with header and collapsible content panel, for progressive disclosure of grouped information.
0
Action List - with Counter
Consolidate
Action-list row with a trailing Counter pill. Duplicates the base Action List matrix — should be a trailing slot on the base row, not a separate component.
Action List - with Description
Consolidate
Action-list row with a secondary description line below the label. Should collapse into base List via an optional description slot.
Action List
Restructure
Tappable action-list row with leading icon, label, optional description, and a trailing CTA / counter / chevron. Currently 3 sibling Figma components (base + Counter + Description) that should collapse into one.
Ad
Ad Space
Keep
Unified ad surface spanning IAB banner sizes, dashboard promo tiles, and full-width hero placements.
Alert
Fix
Inline notification banner for surfacing contextual feedback — info, warning, error, or success messages.
P500.00
Amount Text Field
Restructure
Large display-style numeric input for PHP amount entry, used in Send Money, Cash-In, and top-up flows. Shows peso glyph + amount on a single underline.
Avatar Group
Fix
Overlapping stack of avatars representing multiple participants, with an overflow badge for counts beyond the shown set.
Avatar
Circular identity marker showing a user photo, initials, or brand icon across multiple sizes.
Badge
Status pill for highlighting state, category, or counts — available in semantic colors and heavy, light, and outline treatments.
Banner
Restructure
Neutral-background promo banner with a leading image/icon, preamble, heading, description, and an optional button link. 20 variants across 5 boolean-ish axes.
Bottom Sheet
Restructure
Bottom-anchored sheet surface. Currently shipped as a 2-variant "Bottom Drawer" header skeleton — needs a full rebuild around a content slot and native detent system.
Button
Keep
Primary interactive control for triggering actions, available in filled, outlined, and text styles across five sizes.
Carousel Card
Restructure
Vertical carousel card with a square banner, title, and description, used in horizontally scrolling content rails.
New
Carousel - Discount Card
Consolidate
Voucher-style carousel card with a perforated banner, promotional label, and peso-value line for discount offers.
Carousel - Item
Consolidate
Peeking-carousel banner with a background image, preamble, headline, description, and a "learn more" link.
Checkbox
Keep
Selection control for toggling individual options on or off, supporting selected, indeterminate, and disabled states.
LabelSort
Chip
Restructure
Compact pill-shaped element for filters, tags, and selection — supports leading/trailing icons and optional dropdown affordance.
0/1010/10
Counter
Fix
Compact numeric indicator for displaying counts or progress-against-capacity values inline with other UI.
Date Picker - Group
Consolidate
Popover panel with month/year/day navigation. 3 variants: Date, Year, Month. Should be a thin token-styled wrapper around native pickers, not a redrawn surface.
1591
Date Picker - Item
Consolidate
32×32 day cell used inside the Date Picker Date grid. 7 variants on Type × State. Candidate to merge with Month and Year Picker - Item into one Picker Cell.
Date Picker
Restructure
Field-shaped trigger with a calendar glyph. Opens an inline calendar panel (Date Picker - Group) when tapped.
Dropdown Item Group
Consolidate
Popover surface that stacks multiple Dropdown Items together into a scrollable menu panel.
Dropdown Item
Fix
Selectable row rendered inside a Dropdown menu, supporting text, amount, country, and tagged content variants.
Dropdown
Fix
Selection control that reveals a list of options in an overlay menu when tapped.
Generic Card
Fix
Tappable list row with leading icon, heading, supporting text, optional badge, and trailing chevron.
Generic Transaction Card
Restructure
Transaction history row showing a label, date metadata, and a trailing amount, menu, or reference field.
Header - Centered
Restructure
Centered page banner with label and optional sub-label, on brand-colored or neutral surfaces.
Header - Transaction
Restructure
Card-style hero block with avatar, title, optional contact row, and description on a brand-colored surface.
Header - With Logo
Consolidate
Top app bar featuring the GCash logo on a brand-colored surface, used as a home-screen identifier.
Header
Restructure
In-screen section header with preamble, title, description, and a trailing slot for icons, links, or actions.
35%
Horizontal Voucher
Consolidate
Single 336×265 symbol with a full-width 336×144 raster hero image and 6 boolean toggles over hardcoded content. Two duplicate discount badges ("10% off" + "35% off") stacked at the same anchor. Four hardcoded row badges. No state axis. Parallel record to Vertical Voucher and Voucher Card Horizontal.
Inline Text
Restructure
Label-value pair row used inside transaction cards, modal summaries, and list items for compact key-value detail.
Input Field
Fix
Base text input for single-line entry, with label, placeholder, helper/error text, and support for all interaction states.
Labeled Field
Fix
Enhanced input with leading icon, label, value, action button, and trailing icon for richer form entry.
1.
List Item Asset
Restructure
Leading marker atom used by List Item — renders avatars, icons, selection controls, or custom assets.
List Item
Fix
Row combining a leading asset and a text body, used inside lists with configurable indentation levels.
List
Restructure
Vertical container that groups List Item rows into a continuous scrollable list surface.
Menu Grid
Fix
Grid container that arranges Service Item tiles into uniform rows and columns for quick navigation shortcuts.
Modal
Restructure
Centered dialog for confirmations, icon-led messages, and transaction receipts — anchored to the screen center above a scrim.
JanFebMar
Month and Year Picker - Item
Consolidate
100×32 selectable cell used inside the Date Picker Month and Year views. 3 variants on Type. Candidate to merge with Date Picker - Item into one unified Picker Cell.
Onboarding - Tooltip
Consolidate
Header + description + close tooltip with a pointer on one of 4 sides. 4 variants. Name implies walkthrough content but ships no step indicator or CTAs — it's Tooltip V2 minus the content axes. One of 3 sibling Tooltip components to collapse.
Overlay
Fix
Translucent scrim layer that dims background content behind modals, sheets, and dialogs.
Progress Bar
Restructure
Linear indicator that shows task completion progress along a horizontal track.
Radio Button with Label
Fix
Composed row pairing a radio button with a text label for use in selection lists and forms.
Radio Button
Restructure
Selection atom for choosing one option from a set of mutually exclusive choices.
Recipient Field
Fix
Two-line recipient entry field with trailing action icons, used for sending money or selecting contacts.
Search Field
Restructure
Dedicated input for search queries, with a leading search icon and optional trailing clear affordance.
Select Field
Fix
Currency or amount selection field with a leading flag, peso sign, and trailing chevron affordance.
Stepper - Bullet
Restructure
Row of 8×8 dots with one dot filled to indicate the current step. 3 sibling components today (3/4/5 steps); should be one component with a steps prop and a current prop.
1234
Stepper - Circular
Restructure
Row of numbered circles with a progress arc showing position through a multi-step flow. 9 sibling components today (2–10 steps); should be one component with a steps prop.
Stepper - Dash
Restructure
Horizontal row of equal-width dashes — earlier/current steps brand-blue, later steps in track blue. Single component today; uses boolean visibility props to set total count.
Subtext Message
Restructure
Helper / success / error message displayed below form fields. 6 variants across Variant (Primary/Success/Error) × Size (Base/Small).
Label
Tab Item
Fix
Single tab atom composed by Tabs, showing a label with optional icon and active-state indicator.
MMM DD₱X.XXLabelLabelPHPLabelPHP
Table - Scheduling
Consolidate
Scheduled-payment row: date header + peso amount, with optional 2- or 4-cell label/value detail grid. Third parallel Table-family record — schema diverges again and re-ships the raster peso glyph.
₱X.XX₱X.XX₱X.XX
Table - Transaction
Consolidate
Transaction-specific row with peso-prefixed amount columns. Duplicates the parent Table schema for a product-layer use case (account limits, transaction totals).
Table
Restructure
Row-based tabular layout with header + content variants. Three nested components (Table, Table - Item, Table - Label) that likely collapse into one data-driven row.
Tab 1Tab 2Tab 3
Tabs
Fix
Horizontal bar of Tab Items for switching between sibling views within a single screen.
Terms & Conditions Accordion
Remove
Canonical Accordion with a hardcoded "Terms & Conditions" title and hardcoded body — not a distinct component. Replace with a usage example of the base Accordion.
Text Area
Consolidate
Multi-line text input sibling of Input Field. Ships with desktop resize-handle glyph that has no native mobile equivalent — candidate to fold into Input Field via a multiline / lineLimit prop.
TitleHeader
Title Bar
Top app navigation bar with title, optional subtext, leading back/close, and trailing action icons.
Toast - With Button
Consolidate
Toast variant with an inline action button — "Undo", "Retry", "View". Today it lives as a separate component; the action belongs on the base Toast as an optional slot.
Toast
Restructure
Transient overlay notification. Floats over the UI to confirm success, surface errors, or indicate pending work — auto-dismisses after a short duration.
Toggle - With Label
Restructure
Composed row pairing a toggle switch with a label and optional description, for settings list items.
Toggle
Fix
Binary on/off switch for settings and preferences.
Tooltip Blurred and Transparent
Consolidate
Directional dark-translucent tooltip with a 2.5 px backdrop blur. Header + description only; 4 placement variants. A visual treatment, not a component — should collapse into the unified Tooltip via appearance: .translucent.
Tooltip V2
Restructure
Directional floating nudge with header, description, dismiss, optional icon + 1–2 CTAs, and a pointer on any of 4 sides. 8 variants. One of 3 sibling Tooltip components that should consolidate.
Attach file
Upload File
Fix
File upload input that handles default, uploading, uploaded, and error states with a thumbnail preview.
35%
Vertical Voucher
Consolidate
Single 162×465 symbol bundling two Voucher Asset sizes + content block with 8 boolean toggles. All text, badges, and prices are hardcoded placeholder. No state axis. Parallel record to Horizontal Voucher and Voucher Card Horizontal.
View Only Field
Read-only field displaying label, value, and subtext with an optional trailing badge, link, or icon.
Visual Popup
Fix
Illustrated modal with a hero image, title, description, and one or two call-to-action buttons.
35%
Voucher Asset
Restructure
Ticket-shaped raster illustration with a hardcoded "35% off" Badge. 20 variants sprawl across use-case categories — a variant axis that's really illustration content.
Voucher Card Horizontal
Restructure
336×111 horizontal voucher with a proper 4-state axis (limited / expiring / used / expired). State drives bg, label colors, and badge style. All text is hardcoded placeholder; the partner logo is a raster GCash asset. Canonical sibling in the 3-component voucher family — push the consolidation from here.
Voucher Details
Product Layer
A full 336×704 voucher screen composed of DS primitives (Logo, Badge, Accordion, List Item) plus a shared amount block + strip divider. Ship as a screen recipe, not a DS component.
Accordion Component link

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.

In Context

How the accordion appears in a real product screen — expanding to reveal content.

Accordion component shown in a GCash Cash In screen with Over the Counter expanded showing partner list, Online Banks and Global Partners collapsed
Live Preview

Toggle properties to see the accordion update in real time.

Accordion Label
Properties
Type
State
leadingIcon
description
DS Health
Reusable
Pass
Expanded variants include a content-body slot. Boolean visibility on leadingIcon and description lets designers configure the component without extra variants.
Self-contained
Pass
Header row (56px fixed height) and content-body panel are both included. Engineers can implement it as a standalone unit with no external spec needed.
Consistent
Pass
A single variant property (Type) drives collapsed/expanded. leadingIcon and description are boolean show/hide properties — no duplicate variants needed.
Composable
Pass
Semantic layer names (icon-leading, content, trailing-icon). Chevrons are vector instances. The icon slot accepts instances cleanly.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesState=DefaultHeader row with chevron. Tap to expand/collapse.
PressedYesYesState=PressedVisual feedback on touch. Darker surface token.
DisabledYesYesState=DisabledMuted colors. Tap ignored. Chevron dimmed.
Focused (a11y)N/AN/AMobile OS handles focus rings natively.
Resolved Issues
  • 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-body SLOT added to all expanded variants (C4)
  • Variant set reduced from 24 to 6 — Type × State matrix (C2)
  • leadingIcon and description converted 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)
Open Issues
  • Code Connect mappings not registered. No native component files are linked yet. All structural blockers are resolved — registration can now proceed. C7 · Code Connect Linkability
Design Recommendations
  • Add an AccordionGroup compound component. Manages exclusive expand (only one open at a time) — the canonical FAQ and settings pattern. Avoids every consumer wiring their own expanded-id state. Family
Styles
Collapsed
DESDEV

Header row only — 56px fixed height. Trailing chevron points down. Tap anywhere in the row to expand.

Label
Properties
State
leadingIcon
description
Properties
TypeCollapsed
StateDefault
leadingIcontrue
descriptionfalse
Layout
Width396px (fill)
Header height56px
Padding H16px
Padding V4px
Leading icon32×32px
Trailing icon32×32px
Corner radius0 (rectangular)
Border1px solid #E5EBF4
Typography
Text StylePrimary/Multi-line Label/Base
Label fontHeyMeow Rnd Bold
Label size16px
Label tracking0.25px
Label line-height20px
Text StyleSecondary/Bold/Base
Desc fontBarkAda SemiBold
Desc size14px
Desc tracking0
Desc line-height20px
Colors by State

All colors are bound to design tokens from the component variable collection.

RoleTokenDefaultPressedDisabled
Header bgsurface/default#FFFFFF
Pressed bgsurface/pressed#F4F7FB
Disabled bgsurface/disabled#F8F9FB
Borderborder/subtle#E5EBF4#E5EBF4#E5EBF4
Labeltext/primary#0A2757#0A2757
Label (disabled)text/disabled#C2C6CF
Descriptiontext/secondary#90A8D0#90A8D0
Icon placeholdericon/placeholder#C2C6CF#C2C6CF#C2C6CF
Chevronicon-chevron#005CE5#005CE5#C2CFE5
Expanded
DESDEV

Header row (56px) + content-body panel (56px SLOT)=112px total height. Trailing chevron points up. Content-body background uses surface/content token.

Label
Properties
State
leadingIcon
description
Properties
TypeExpanded
StateDefault
leadingIcontrue
descriptionfalse
Layout
Width396px (fill)
Header height56px
Content-body height56px (SLOT)
Total height112px
Padding H16px
Padding V4px
Leading icon32×32px
Trailing icon32×32px
Corner radius0 (rectangular)
Border1px solid #E5EBF4
Divider1px solid #E5EBF4 (header/body)
Typography
Text StylePrimary/Multi-line Label/Base
Label fontHeyMeow Rnd Bold
Label size16px
Label tracking0.25px
Label line-height20px
Text StyleSecondary/Bold/Base
Desc fontBarkAda SemiBold
Desc size14px
Desc tracking0
Desc line-height20px
Colors by State

Expanded adds the surface/content token for the content-body panel background.

RoleTokenDefaultPressedDisabled
Header bgsurface/default#FFFFFF
Pressed bgsurface/pressed#F4F7FB
Disabled bgsurface/disabled#F8F9FB
Content bgsurface/content#F4F7FB#F4F7FB#F8F9FB
Borderborder/subtle#E5EBF4#E5EBF4#E5EBF4
Labeltext/primary#0A2757#0A2757
Label (disabled)text/disabled#C2C6CF
Descriptiontext/secondary#90A8D0#90A8D0
Icon placeholdericon/placeholder#C2C6CF#C2C6CF#C2C6CF
Chevronicon-chevron#005CE5#005CE5#C2CFE5
Installation Planned API

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.

Property Mapping

Every row maps a Figma component property to its native equivalent.

Figma PropertySwiftUICompose
Type=CollapsedisExpanded: falseisExpanded=false
Type=ExpandedisExpanded: trueisExpanded=true
State=Disabled.disabled(true)enabled=false
leadingIcon=trueleadingIcon: Image?leadingIcon: @Composable (() -> Unit)?
description=truedescription: String?description: String?
Content-Body (SLOT)content: () -> some Viewcontent: @Composable () -> Unit
SwiftUI
ios/Components/Accordion/EBAccordion.swift
Jetpack Compose
android/components/accordion/EBAccordion.kt
Usage Snippets Planned API
Basic Accordion
// 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")
}
Accessibility
RequirementiOSAndroid
Min touch target44 × 44pt (full header row)48 × 48dp (full header row)
Expand/collapse.accessibilityAction(.default) togglesonClick handler on header
State announcement.accessibilityValue("expanded"/"collapsed")expandedState semantics
Disabled.disabled(true) — announced by VoiceOverenabled=false
Content-bodyAutomatically read by screen reader when expanded
Chevron icon.accessibilityHidden(true) — decorativecontentDescription=null — decorative
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names across all variants: container, icon-leading, content, trailing-icon.
C2Variant & Property NamingReadyBoolean properties use true/false. Variant keys use key=value syntax. leadingIcon and description are boolean visibility props.
C3Token CoverageReady10 tokens bound across all 6 variants. All colors, spacing, and typography fully tokenized.
C4Native MappabilityReadyHeader + content-body SLOT maps cleanly to DisclosureGroup (SwiftUI) and AnimatedVisibility (Compose).
C5Interaction State CoverageReadyDefault, pressed, and disabled states covered across all 6 variants. Focus ring N/A — mobile OS handles natively.
C6Asset & Icon QualityReadyChevrons are vector component instances. Leading icon is a SLOT placeholder accepting any icon instance.
C7Code Connect LinkabilityNeeds RefinementNo Code Connect mappings registered. Property structure is clean and ready for mapping — suggested paths below.
Code Connect
AspectStatusNotes
Component typeReadyProper Figma component set.
Variant namingReadykey=value syntax with true/false booleans.
Property namingReadyClean 1:1 mapping to native params.
Layer namingReadycontainer, icon-leading, content, trailing-icon.
Token coverageReadyAll 10 tokens bound — colors, spacing, and typography.
Asset qualityReadyChevrons are vector component instances. Icon slot is SLOT type.
Code ConnectNot MappedNo mappings registered. Suggested paths below.
Variants Inventory (6 total)

2 Type values × 3 State values. leadingIcon and description are boolean visibility properties, not variant axes.

TypeStateNode ID
CollapsedDefault16870:9289
ExpandedDefault16870:9298
CollapsedPressed16919:864
ExpandedPressed16919:877
CollapsedDisabled16919:956
ExpandedDisabled16919:969
1.4.0 — March 2026 Patch
Changes Applied via Figma MCP · node 16870:9288
Leading icon layer re-renamed — All 6 variants had Placeholder reverted after v1.3.0 restructure. Re-applied icon-leading name across all 6 current variants. Fixed
C1 Restored
Custom fonts validatedHeyMeowRnd-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. Validated
Fonts Resolved
1.3.0 — March 2026 Minor
Changes Applied via Figma MCP · node 16870:9288
Variant set reduced from 24 to 6leadingIcon 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. Refined
C2 Improved
Fixed 56px header height — Applied consistent fixed height across all 6 variant headers for reliable layout in native implementations. Refined
C4 Improved
Color tokens connected — 10 design system variables bound across all 6 variants: surface/default, border/subtle, text/primary, text/secondary, icon/placeholder, icon-chevron, surface/pressed, surface/content, surface/disabled, text/disabled. Fully resolves C3. Fixed
C3 Resolved
Code Connect property renamedlabelDescriptiondescription for cleaner 1:1 mapping to native params. Refined
C7 Prep
Annotation instance frame added — Type × State grid built with nested auto layout (VERTICAL outer → HORIZONTAL rows → VERTICAL cells). White card, #E5EBF4 border, 16px radius, Menlo annotation labels. Added
Annotation
1.2.0 — March 2026 Minor
Changes Applied via Figma MCP · node 16870:9288
Added expanded content panel (content-body) — All 12 expanded variants resized from 62px to 142px. A content-body frame (360×80px) added at y=62 inside each container. Background: #F4F7FB (surface/content token). Border: #E5EBF4. Fully resolves C4. Fixed
C4 Resolved
1.1.0 — March 2026 Minor
Changes Applied via Figma MCP · node 16870:9288
Added state=pressed and state=disabled variants — 16 new variants cloned and styled. Component set expanded from 8 to 24 variants. state property added with values default / pressed / disabled. Fully resolves C5. Fixed
C5 Resolved
1.0.1 — March 2026 Patch
Changes Applied via Figma MCP · node 16830:2025
Frame renamed to container — All 8 root container frames renamed from Frame to container. Fixed
C1 Partial
Placeholder renamed to icon-leading — All 4 leading icon instances renamed from Placeholder to icon-leading. Fixed
C1 Partial
Boolean props converted to true/false — All 8 variant names updated. leading icon and label description converted from yes/no to true/false. Fully resolves C2. Fixed
C2 Resolved
Expanded content panel — Resolved in v1.2.0. content-body frame added to all 12 expanded variants. Fixed in 1.2.0
C4 Resolved
Interaction states: pressed / disabled — Resolved in v1.1.0. 16 new variants added across all type/icon/desc combinations. Fixed in 1.1.0
C5 Resolved
Code Connect mappings — No native component files or Code Connect CLI mappings registered yet. Still Open
C7 Open
Action List - with Counter ConsolidateRequires ReworkComponent link

Action-list row with a 32 px leading icon, brand-blue label, trailing chevron, and a trailing Counter pill. 6 variants across Density (Compact / Expanded) × State (Default / Disabled / Loading). Shares the 2 × 3 matrix with its siblings Action List (18577:14545) and Action List - with Description (18577:14604) — the only delta is the trailing Counter instance.

Consolidate — fold into base Action List as a trailing slot
This variant sibling just adds an EBCounter to the base transaction row. That should be a trailing slot (or a counter: Int? parameter that swaps in a Counter) on the base component — not a second component with a duplicated 2 × 3 density/state matrix. Shipping as a sibling doubles maintenance cost on every token or layout change, and the same anti-pattern will repeat for the "with Description" sibling.
In Context

Used where a row needs to surface a pending count alongside the action — inbox folders, notification categories, or settings entries with outstanding items.

InboxNotifications5Promos2Archive0
Live Preview

Flip Density + State. Counter is always on — the whole point of this variant sibling is to carry a trailing count.

Properties
Density
State
label
count
DS Health
Reusable
Warn
Reusable on its own — but the reuse path collides with the base Transaction row. Teams have to pick between two near-identical components instead of one with a trailing slot. C1
Self-contained
Pass
Carries its own tokens, typography, and the trailing Counter composition. Nothing external needed to render.
Consistent
Warn
Density is PascalCase; State is PascalCase here but state (lowercase) on Counter — inconsistent casing across the family. Loading skeleton is a generic strip instead of a trailing pill shape. C2
Composable
Fail
Composition happens at the wrong layer. The Counter is already a standalone component — "row with a trailing Counter" should be the consumer wiring two components together (or a trailing slot on the row), not a third component that hardcodes both. C1
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYesState=DefaultBrand-blue label, brand-blue chevron, filled Counter pill (#072592 on #EEF2F9).
DisabledYesYesState=DisabledMuted label (#C2CFE5), muted chevron (#9BC5FD), empty Counter pill.
LoadingYesYesState=LoadingAvatar circle, long label skeleton, trailing 46 × 16 strip. The strip doesn't actually match the Counter pill shape — see open issue.
Pressed / FocusedMissingMissingNot modeledNo pressed / focused variants on the row. Inherited gap — same issue as base Transaction row.
Open Issues
  • Duplicated variant matrix. This sibling recreates the base row's 2 × 3 (Density × State) matrix just to bolt on a trailing Counter. Every future change to the base row (radius, padding, token rename) has to be mirrored here. Fold into the base row via a trailing slot or a counter: Int? parameter. C1 · Layer Structure & Naming
  • Property casing is inconsistent across the family.Density and State use PascalCase, while the composed Counter child uses lowercase state. Pick one (recommend lowercase) and apply across every List Item and Counter property. C2 · Variant & Property Naming
  • Loading skeleton doesn't match the trailing Counter shape. The 46 × 16 strip is the generic "trailing icon" skeleton used on the base row — it doesn't look like a 24 × 24 pill. Either shape the skeleton to match or drop the Counter entirely in Loading (and let the skeleton stand in). C4 · Native Mappability
  • No pressed / focused states. Action rows are interactive targets; they need pressed and focused visuals for native parity. Missing on the base row too — fix once at the base. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the consolidation lands — wiring this sibling directly would entrench the duplication. C7 · Code Connect Linkability
Design Recommendations
  • Consolidate into base Action List. Add a trailing slot to the base row (accepting any compact trailing view — Counter, Badge, Text, custom). Alternatively, add a typed counter: Int? parameter that swaps in an EBCounter when set. Removes this sibling entirely and collapses the 12-variant base + with-Counter surface to 6. Apply the same treatment to the "with Description" sibling. Family
  • Adopt a Figma Slot for the trailing area. With the slot, this file's 6 variants disappear; consumers drop an existing Counter instance (node 18482:71321) into the trailing slot of the base row. Maps cleanly to @ViewBuilder trailing (SwiftUI) / trailing: @Composable () -> Unit (Compose) for Code Connect. Slot
  • Normalize property casing. Rename Densitydensity and Statestate so the whole Transaction family (base, with Counter, with Description) matches the lowercase convention used on Counter and most of the DS. Rename
  • Shape the Loading skeleton or omit the trailing. If the base row's Loading skeleton stays, match the trailing skeleton to a 24 × 24 pill so consumers see the actual footprint. Easier path: drop the trailing skeleton and let the 46 × 16 strip stand in for all trailing content. State
  • Add pressed / focused states at the base row. Action rows are tappable; native parity requires pressed + focused visuals. Fix on the base Transaction row once, and every "with X" sibling (or slotted consumer) inherits it. State
  • Document the migration path. When the base row gets a trailing slot, deprecate this component and link consumers to the base row with Counter composition. Otherwise teams keep instancing the sibling and the duplication doesn't go away. Docs
Variants

6 variants: Density (Compact / Expanded) × State (Default / Disabled / Loading). Same 360 px row as the base Transaction row — only the trailing Counter is added.

Compact · Default — brand label + filled Counter

360 × 56. 32 px icon, brand-blue label, chevron, trailing 24 × 24 filled Counter pill.

Expanded · Default — taller row variant

360 × 64. Same composition; 15 px vertical padding vs 11 px on Compact.

Compact · Disabled — muted tokens

Muted label, muted chevron, empty Counter pill.

Expanded · Disabled — taller muted variant

Expanded height + Disabled tokens.

Compact · Loading — skeleton row

Avatar circle + label line + 46 × 16 trailing strip. Strip shape doesn't match the Counter pill.

Expanded · Loading — taller skeleton row

Same skeleton with 16 px padding.

Colors by State
RoleTokenDefaultDisabledLoading
Row bgmain/action-list/color/default/bg#FFFFFF#FFFFFF#FFFFFF
Labelmain/action-list/color/default/label-brand#005CE5
Label (disabled)main/action-list/color/disabled/label#C2CFE5
Chevronmain/action-list/color/default/chevron#005CE5#9BC5FD
Counter bgmain/counter/color/filled/bg#EEF2F9#EEF2F9
Counter label (filled)main/counter/color/filled/label#072592
Counter label (empty)main/counter/color/empty/label#C2CFE5
Skeleton barbg/color-bg-strong#EEF2F9
Row shadowDepth/D0drop-shadow(0 1 3 0 #E8EEF2C9)

Counter colors are owned by the Counter component's variable collection (main/counter/color/*).

Layout
PropertyTokenValue
Row width360px
Row height (Compact)56px
Row height (Expanded)64px
Row padding Hspace/space-1212px
Row padding V (Compact)11px
Row padding V (Expanded)15px
Icon → label gapspace/space-1212px
Label → trailing gapspace/space-1616px
Icon size32 × 32
Chevron size32 × 32
Counter size24 × 24 (min) · hugs digits
Counter pad Hspace/space-88px
Counter radiusradius/radius-roundpill (99999)
Row radiusradius/radius-26px
Typography
ElementDS text styleSpec
LabelPrimary/Label/LargeProxima Soft Bold · 18 / 18 · +0.25
CounterPrimary/Label/SmallProxima Soft Bold · 14 / 14 · +0.25
Installation Planned API

iOS — Swift Package Manager

// In Xcode: File → Add Package Dependencies
"https://github.com/AY-Org/eb-ds-ios"

import EBDesignSystem

Android — Gradle (Kotlin DSL)

dependencies {
    implementation("com.eastblue.ds:list:1.0.0")
}

import com.eastblue.ds.components.EBListItemTransaction
Property Mapping (proposed — post-consolidation)
Figma (today)Figma (proposed)SwiftUIComposeNotes
(separate component)trailing Slot on base row@ViewBuilder trailingtrailing: @Composable () -> UnitDrop this sibling; consumer composes base row + Counter via slot.
Densitydensity (renamed)density: .compact | .expandeddensity: EBListDensityLowercase for consistency with Counter + most of DS.
Statestate (renamed)state: .default | .disabled | .loadingstate: EBListStateLowercase; adopt pressed + focused later.
icon (bool)leading Slot@ViewBuilder leadingleading: @Composable () -> UnitMatch trailing — slot over bool.
labellabellabel: Stringlabel: StringUnchanged.
chevron (bool)chevronchevron: Bool=truechevron: Boolean=trueKeep as a bool; chevron is fixed.
counter (bool)derived from trailing slotDelete — slot presence drives rendering.
SwiftUI
ios/Components/List/EBListItemTransaction.swift
Jetpack Compose
android/components/list/EBListItemTransaction.kt
Usage Snippets Planned API

All snippets use the consolidated base EBListItemTransaction with a trailing EBCounter — the exact composition this sibling encodes today.

// Default — Compact density, trailing Counter
EBListItemTransaction(label: "Notifications") {
    EBAvatar(initials: "N")
} trailing: {
    EBCounter(count: unreadCount)
}

// Expanded density
EBListItemTransaction(label: "Promos") {
    EBAvatar(initials: "P")
} trailing: {
    EBCounter(count: 2)
}
.density(.expanded)

// Disabled row — counter stays in empty state
EBListItemTransaction(label: "Archive") {
    EBAvatar(initials: "A")
} trailing: {
    EBCounter(count: 0)
}
.disabled(true)

// Loading — trailing slot omitted; skeleton fills
EBListItemTransaction.loading()
// Default — Compact density, trailing Counter
EBListItemTransaction(
    label = "Notifications",
    leading = { EBAvatar(initials = "N") },
    trailing = { EBCounter(count = unreadCount) }
)

// Expanded density
EBListItemTransaction(
    label = "Promos",
    density = EBListDensity.Expanded,
    leading = { EBAvatar(initials = "P") },
    trailing = { EBCounter(count = 2) }
)

// Disabled row
EBListItemTransaction(
    label = "Archive",
    enabled = false,
    leading = { EBAvatar(initials = "A") },
    trailing = { EBCounter(count = 0) }
)

// Loading — trailing slot omitted; skeleton fills
EBListItemTransaction(state = EBListState.Loading)
Accessibility
RequirementiOSAndroid
Row roleWrap in Button / NavigationLink for tappable semanticsApply Modifier.clickable(...) + Role.Button
Counter labelCompose accessibility label: "Notifications, 5 unread" — don't let VoiceOver read the digit aloneMerge into row: contentDescription="Notifications, 5 unread"
Disabled.disabled(true) — drops from hit-testing + dims label/chevron/counterenabled=false on clickable modifier
LoadingAnnounce "Loading"; hide skeleton children from a11y treeModifier.semantics { liveRegion=Polite } + hide skeletons
ChevronDecorative — .accessibilityHidden(true)contentDescription=null
Usage Guidelines
Do
  • Compose: use the base EBListItemTransaction with a trailing EBCounter slot instead of this sibling.
  • Hide the Counter when the count is zero if the zero state is noise (most inbox-style lists).
  • Pair the numeric Counter with a screen-reader label that adds context ("5 unread").
Don't
  • Don't ship this as a separate component in native — it duplicates the base row.
  • Don't put Badge labels (New, Draft) in the trailing slot and call it a Counter — use Badge via the same slot.
  • Don't build additional "with X" siblings (with Description, with Toggle, …). They're all trailing-slot use cases.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkSibling duplicates the base Transaction row matrix. Consolidate via trailing slot.
C2Variant & Property NamingPartialDensity/State PascalCase mismatches lowercase Counter and most of DS.
C3Token CoverageReadyAll colors + spacing bound. Uses main/action-list/* + main/counter/*.
C4Native MappabilityPartialHStack/Row maps cleanly. Loading skeleton's 46 × 16 trailing strip doesn't match a Counter pill.
C5Interaction State CoveragePartialNo pressed / focused variants — inherited from base row.
C6Asset & Icon QualityReadyChevron is a vector instance; icon is a swap placeholder — same pattern as other List Items.
C7Code Connect LinkabilityNot MappedDo not wire this sibling — map the base row with trailing slot after consolidation.
Variants Inventory (6 total)

Density (2) × State (3)=6 variants. Identical matrix to the base Transaction row.

#DensityStateNode IDDimensions
1CompactDefault18577:14638360 × 56
2ExpandedDefault18577:14647360 × 64
3CompactDisabled18577:14656360 × 56
4ExpandedDisabled18577:14665360 × 64
5CompactLoading18577:14674360 × 56
6ExpandedLoading18577:14679360 × 64
1.0.0 — April 2026 Initial
Initial Assessment · node 18577:14637
Verdict: Consolidate — Sibling of base Action List; duplicates the 2 × 3 density/state matrix just to add a trailing Counter. Fold into base via a trailing slot. Open
Family
C1 — Duplicated matrix — 6 variants re-created instead of using a slot on the base. Open
C1
C2 — PascalCase namingDensity/State mismatch lowercase state on composed Counter. Open
C2
C4 — Loading skeleton shape — 46 × 16 trailing strip doesn't match a 24 × 24 Counter pill. Open
C4
C7 — Code Connect — Not mapped; wait for consolidation so the mapping targets the base row. Open
C7
Action List - with Description ConsolidateRequires ReworkComponent link

An action-list row with a secondary description line beneath the primary label. 3 variants across State (Default / Disabled / Loading). Sibling of base List (18577:14545) and List - with Counter (18577:14637) — all three share the same container, tokens (main/action-list/...), and trailing CTA + chevron anatomy.

Merge into base List via a description slot
This component duplicates the entire base List anatomy just to render a 12/14 description line under the label. Native platforms already expose this as one parameter — SwiftUI composes primary + secondary labels in a VStack, Material 3's ListItem has supportingContent. Collapse all three siblings (List, with Description, with Counter) into one Action List component with optional description and trailing slots. This also closes the Density-coverage gap (base List has Compact + Expanded; this variant does not).
In Context

Contexts are illustrative. Description rows appear in settings lists, notification preferences, and profile menus where each row needs a subtitle explaining the action.

NotificationsEditOnOff
Live Preview

Toggle state, icon, trailing CTA, and chevron. Loading state replaces content with shimmer rows.

Properties
state
icon
trailingComponent
chevron
bottomBorder
DS Health
Reusable
Warn
Works for any settings or notification list row with a subtitle. Duplicates base List's anatomy instead of reusing it — a description slot on the base component would handle this.
Self-contained
Pass
Carries its own text styles (Primary/Label/Light/Base + Primary/Multi-line Label/Light/Fine), background, and spacing.
Consistent
Warn
Ships only 3 states vs base List's 6 (missing Density axis). Leading asset is a raw #c2c6cf circle placeholder, not an instance of List Item Asset. C5C1
Composable
Fail
Exists as a standalone sibling of base List rather than a compositional variant — bloats the library and forces consumers to swap entire components just to add a subtitle. C4
Behavior
StateiOSAndroidFigma propertyNotes
DefaultText + subtitle render; chevron visibleText + supporting text render; chevron visiblestate=DefaultLabel #0A2757, description #6780A9
Disabled.disabled(true) — tap ignored, opacity reducedenabled=false — 38% content alphastate=DisabledLabel + description both recolor to #c2cfe5, CTA to #9bc5fd
LoadingSkeleton with redacted(reason:) or shimmer overlaySkeleton shimmer via Modifier.placeholder(...)state=LoadingTwo shimmer lines replace text; trailing icon becomes a 53px shimmer block
PressedNot defined in FigmaNot defined in FigmaMissing — native pressed token should map to main/action-list/color/pressed/bg (to be added) C5
Open Issues
  • Sibling component duplicates base List anatomy. A 12/14 description line is a single optional parameter on every native list primitive (SwiftUI secondary label, Material supportingContent). Standalone sibling forces consumers to swap whole components instead of flipping one prop. C4 · Native Mappability
  • Leading asset is a raw #c2c6cf placeholder. The icon container is a 32px filled circle with a hard-coded gray — not an instance of List Item Asset, not bound to a token. Consumers can override via instance-swap but the default is visually broken. C1 · Layer Structure & Naming
  • Missing Density axis. Base List ships Density=Compact (48px) and Density=Expanded (56px). This variant skips the axis entirely, producing a 70–76px row with no Compact counterpart. C2 · Variant & Property Naming
  • No Pressed state. Only Default / Disabled / Loading are modeled. Action lists are tappable — pressed styling should live in Figma as a token so native .pressed / ripple can reference it. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until family consolidation lands — no point mapping a component that will be removed. C7 · Code Connect Linkability
Design Recommendations
  • Consolidate the List family into one Action List component. Collapse base List, List - with Counter, and Action List - with Description into a single component with optional description: String? and trailing slot (CTA text / Counter / custom). Variant math drops from 6 + 3 + 3=12 down to 3 states × 2 densities=6, with description and counter becoming boolean/slot properties. Family
  • Replace the leading-icon placeholder with a Figma Slot. The raw 32px #c2c6cf circle should become a named leading slot accepting any List Item Asset instance or a 32 × 32 icon. Maps 1:1 to SwiftUI @ViewBuilder leading / Compose leadingContent. Slot
  • Add Density × Pressed coverage to the merged component. The consolidated Action List should carry Compact (48px) + Expanded (56px) from base List, plus a Pressed visual state so tokens like main/action-list/color/pressed/bg exist for native ripple/highlight. State
  • Rename the component family to match the token namespace. Tokens use main/action-list/... but Figma names the component List. Rename the merged component to Action List so Figma, tokens, and native API (EBActionList) all agree. Rename
Variants

3 variants split by State. Default shows full row; Disabled dims label + description + CTA + chevron; Loading swaps content for shimmer lines.

State=Default

Active row. Leading icon + label + description + optional CTA + chevron.

State=Disabled

Non-interactive. Label + description recolor to #c2cfe5; CTA + chevron use #9bc5fd.

State=Loading

Skeleton. Two shimmer lines replace text; trailing is a 53px shimmer block.

Colors by State
RoleTokenDefaultDisabledLoading
Row bgmain/action-list/color/{state}/bg#FFFFFF#FFFFFF#FFFFFF
Labelmain/action-list/color/{state}/label#0A2757#C2CFE5
Descriptionmain/action-list/color/{state}/description#6780A9#C2CFE5
CTA labelmain/action-list/color/default/label-link#005CE5#9BC5FD
Chevronmain/action-list/color/{state}/chevron#005CE5#9BC5FD
Skeleton line / blockbg/color-bg-strong#EEF2F9
Leading placeholder#C2C6CF#C2C6CF

Leading placeholder fill is hardcoded in the symbol — not bound to a token. Flagged under C1.

Layout
PropertyTokenValue
Row width360px (fixed)
Row height60px (hug, 2 text lines + 6px gap)
Outer padding (Default / Disabled)space/space-1212px all sides
Loading paddingspace/space-12, space/space-2414px vertical · 12px left · 24px right
Icon → text gapspace/space-1212px
Label → description gapspace/space-66px
Leading icon size32 × 32
Chevron wrapper paddingspace/space-44px top/bottom · 4px left/right
Chevron icon size24 × 24
Corner radiusradius/radius-00 (square)
Typography
ElementDS text styleSpec
LabelPrimary/Label/Light/BaseProxima Soft Semibold · 16 / 16 · tracking 0.25
DescriptionPrimary/Multi-line Label/Light/FineProxima Soft Semibold · 12 / 14 · tracking 0.5
CTAPrimary/Label/Light/BaseProxima Soft Semibold · 16 / 16 · tracking 0.25
Installation Planned API

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:list:1.0.0")
}
Property Mapping (proposed — consolidated Action List)
FigmaSwiftUIComposeNotes
labeltitle: Stringtitle: StringPrimary label
description?subtitle: String?supportingContent: String?Optional — this component becomes description !=nil on base List
icon (Slot)@ViewBuilder leadingleadingContent: @Composable () -> Unit32 × 32 — accepts List Item Asset or custom icon
trailingComponent (Slot)@ViewBuilder trailingtrailingContent: @Composable () -> UnitCTA text / Counter / custom
chevronshowChevron: BoolshowChevron: BooleanRenders a 24 × 24 chevron after the trailing slot
bottomBordershowDivider: BoolshowDivider: BooleanBottom hairline; handled by parent List in native
state.disabled(true), loading modifierenabled=false, loading paramDisabled + Loading handled via standard APIs
SwiftUI
ios/Components/List/EBActionList.swift
Jetpack Compose
android/components/list/EBActionList.kt
Usage Snippets Planned API
// With description + CTA + chevron
EBActionList(
    title: "Notifications",
    subtitle: "Receive alerts for new transactions",
    leading: { EBListItemAsset(.icon("bell")) },
    trailing: { Text("Edit").foregroundStyle(.blue) },
    showChevron: true
)

// Disabled
EBActionList(title: "Premium", subtitle: "Unlock higher limits")
    .disabled(true)

// Loading
EBActionList(title: "", subtitle: "")
    .redacted(reason: .placeholder)
// With description + CTA + chevron
EBActionList(
    title = "Notifications",
    supportingContent = "Receive alerts for new transactions",
    leadingContent = { EBListItemAsset(Icon.Bell) },
    trailingContent = { Text("Edit", color = Color.Blue) },
    showChevron = true
)

// Disabled
EBActionList(
    title = "Premium",
    supportingContent = "Unlock higher limits",
    enabled = false
)

// Loading
EBActionList(title = "", loading = true)
Accessibility
RequirementiOSAndroid
Touch targetRow is tappable via Button wrapper or .onTapGesture; min 44ptModifier.clickable(...); min 48dp
Combined label.accessibilityElement(children: .combine) so VoiceOver reads "Label, description, button"Modifier.semantics(mergeDescendants=true) { role=Role.Button }
Disabled state.disabled(true) — VoiceOver announces "dimmed"enabled=false — TalkBack announces "disabled"
Loading state.accessibilityLabel("Loading") on skeleton rowsModifier.semantics { contentDescription="Loading" }
Chevron semanticsDecorative — .accessibilityHidden(true)contentDescription=null
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkLeading asset is a raw #c2c6cf circle placeholder, not a List Item Asset instance or a Figma Slot.
C2Variant & Property NamingPartialProperty names are clean (state, icon, chevron, description) but the Density axis from base List is missing.
C3Token CoverageReadyAll label, description, CTA, chevron, bg colors bound to main/action-list/.... Spacing uses space/*.
C4Native MappabilityReworkSibling component for what natives handle as a single parameter. Consolidate with base List.
C5Interaction State CoveragePartialDefault / Disabled / Loading only — no Pressed. Action lists are tappable; pressed token needed.
C6Asset & Icon QualityReadyChevron is a vector instance. Leading placeholder is decorative, user-provided via instance swap.
C7Code Connect LinkabilityNot MappedBlocked — consolidate family first, then map once.
Variants Inventory (3 total)
StateWidth × HeightPaddingNotesNode ID
Default360 × 6012 all sidesLabel + description + CTA + chevron18577:14605
Disabled360 × 6012 all sidesLabel + description recolored; CTA + chevron dimmed18577:14617
Loading360 × 6014 V · 12 L · 24 RTwo shimmer lines + 53px trailing shimmer block18577:14629
1.0.0 — April 2026 Initial
Initial Assessment · node 18577:14604
Component assessed — 3 variants (Default / Disabled / Loading). Sibling of base List + List with Counter. Documented
Initial
Leading placeholder hardcoded — 32px circle fill #c2c6cf, not an instance of List Item Asset. Open
C1 Open
Missing Density axis — Base List ships Compact + Expanded; this sibling ships neither. Open
C2 Open
Sibling duplicates base List — Description is a single optional parameter on every native list primitive; collapse into one Action List with description slot. Open
C4 Open
No Pressed state — Action list is tappable; pressed token should exist. Open
C5 Open
Code Connect mappings — Not registered; blocked until family consolidation. Open
C7 Open
Action List RestructureReworkComponent link

A tappable row used in action-list menus (Settings, Help, Profile sub-screens) — leading icon, label, optional description, trailing CTA / counter / chevron. Today it ships as three sibling components: List (18577:14545, 6 variants), List - with Counter (18577:14637, 6 variants), and List - with Description (18577:14604, 3 variants) — 15 variants total across the family. Each sibling uses a different label treatment and a slightly different padding scheme.

Collapse 3 siblings into one slot-driven row
The three components differ by presence — a description line, a trailing counter — not by role. Replace with one List component with description?: String, trailing: .cta | .counter | .chevron | .none, plus a named leading slot for the icon. Align label typography across the family (currently Semibold 16 Neutral vs. Bold 18 Brand). Add a Pressed state — these rows are primary nav targets. Reconcile with List Item (display-only body rows) and clarify when to use which.
In Context

Action-list rows stack inside Settings / Profile / Help menus. A typical screen mixes variants with/without description and with/without trailing counter.

SettingsView3
Live Preview

Swap between the 3 sibling shapes and their states. Notice how the Counter variant reads in a different label style than the other two — that's the "Consistent" warning below.

Shape
variant
state
density
Content
label
description
counter
DS Health
Reusable
Pass
Used across Settings, Help Center, Profile, Wallet sub-screens. Covers the main action-list row patterns.
Self-contained
Partial
Colors and padding bound to main/action-list/* tokens. Loading skeleton uses bg/color-bg-strong for placeholders. Leaked internal spacer annotations (_space_2, _space_16) are rendered inside production instances. C1
Consistent
Warn
Three sibling components encode a single row pattern by presence. Label typography diverges: List and List - with Description use Semibold 16 Neutral (#0A2757); List - with Counter uses Bold 18 Brand Blue (#005CE5). C2
Composable
Warn
Leading icon is an always-gray placeholder circle, not a Figma Slot. Trailing content is baked into each sibling rather than driven by a trailing enum. C4C6
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYesState=DefaultBaseline row. Label in Neutral Dark (or Brand Blue on the Counter variant).
DisabledYesYesState=DisabledLabel → #C2CFE5, chevron → #9BC5FD, CTA → #9BC5FD, counter bg stays #EEF2F9 but label → #C2CFE5.
LoadingYesYesState=LoadingIcon becomes a neutral ring; label + trailing become 16 px pill placeholders filled with #EEF2F9.
PressedMissingMissingNot builtAction rows are tap targets — a pressed state (row tint + possibly label darken) is a baseline expectation for native.
FocusedMissingMissingNot builtTV / keyboard focus ring not defined. Android a11y also relies on it.
Open Issues
  • Three sibling components for one row pattern.List, List - with Counter, and List - with Description differ only by the presence of a description line and/or a trailing counter. Collapse into a single List component with optional description and a trailing union. C1 · Layer Structure & Naming
  • Label typography diverges across the family.List + List - with Description use Proxima Soft Semibold 16 / Neutral Dark (#0A2757). List - with Counter uses Proxima Soft Bold 18 / Brand Blue (#005CE5). Same row family should read as one thing. Pick one token (label or label-brand) and one size. C2 · Variant & Property Naming
  • Leaked spacer annotation layers. Internal _space_2 (4115:3220) and _space_16 (21:40139) annotation frames are rendered as opacity-0 layers inside every production instance. Artifacts of authoring, not part of the public component. Remove or move to a separate documentation artboard. C1 · Layer Structure & Naming
  • Leading icon is a gray placeholder. All three siblings default to a #C2C6CF filled 32 px circle under a frame named Placeholder. Same instance-swap anti-pattern as List Item. Adopt a Figma Slot so consumers can drop in a real icon (or an Avatar). C6 · Asset & Icon Quality
  • No Pressed state. These rows are the primary tap surface for navigation menus. The State enum exposes only Default / Disabled / Loading. Add Pressed (tinted bg and/or chevron darken). C5 · Interaction State Coverage
  • Trailing content is baked per sibling. CTA text lives on the base, a filled Counter lives on the Counter sibling, and a chevron appears only sometimes. Introduce a trailing enum (.cta(String) | .counter(Int) | .chevron | .none) so one component covers all three patterns. Maps cleanly to native enums. C4 · Native Mappability
  • Code Connect mappings not registered. Blocked on the consolidation + slot adoption. Adding mappings for three siblings would cement the wrong schema. C7 · Code Connect Linkability
Design Recommendations
  • Consolidate into one List row. One component with properties label: String, description?: String, trailing: .cta(String) | .counter(Int) | .chevron | .none, density: .compact | .expanded, state: .default | .pressed | .disabled | .loading, plus a leading slot. Replaces 15 variants across 3 components with roughly 4 × 4=16 state-permutations of one schema. Family
  • Add a leading Figma Slot for the icon. Maps 1:1 to @ViewBuilder (SwiftUI) and a @Composable slot (Compose). Accepts Icon, Avatar, or a custom 32 px component. Remove the placeholder fill entirely — empty slot means no leading. Slot
  • Reconcile label typography. Pick one: either Neutral Dark Semibold 16 (matches List Item + most action rows) or Brand Blue Bold 18 (matches the current Counter sibling). Neutral is the safer default — Brand Blue reads like a link, which the whole row already behaves as. Apply the choice to all three shapes. Token
  • Add Pressed (and ideally Focused) states. Pressed=bg tints to #F4F6FA, chevron / CTA darkens one step. Focused=2 px brand ring at 2 px offset. Baseline for native row components. State
  • Remove _space_2 / _space_16 spacer annotations. These are authoring artifacts. Move to a separate "Annotations" artboard or delete once the auto-layout is settled. They export as opacity-0 layers to consumers. Composition
  • Disambiguate vs List Item. This component is tappable action navigation (icon + label + trailing CTA/counter/chevron). List Item is display body rows (bullet + text for terms/steps). Document the distinction and cross-link the two — today the names don't telegraph which is which. Docs
  • Rename the family.List collides with the native-platform word for a scroll container, and the "Action List" shorthand the team uses internally doesn't appear in Figma. Consider ActionListRow / EBActionListRow — disambiguates from display lists and from plain list items. Rename
Variants

Three sibling components, 15 variants combined. Previews show the Default state of each shape.

List — icon + label + CTA + chevron

Baseline row. 6 variants (State × Density). Label in Neutral Dark Semibold 16. Trailing CTA text + 24 px chevron icon. 360 × 48 (compact) / 360 × 56 (expanded).

List - with Counter — icon + label + counter + chevron

Adds a trailing Counter pill. 6 variants (Density × State). Card-like container with radius-2 (6 px) corners and Depth/D0 drop-shadow — differs from the base's flat row. Label switches to Bold 18 Brand Blue. 360 × 56 / 360 × 64.

List - with Description — icon + label + description + CTA + chevron

Adds a secondary description line under the label. 3 variants (State only — no Density axis). Label matches the base (Semibold 16 Neutral). Description uses Semibold 12 / tracking-wider / main/action-list/color/default/description (#6780A9). 360 × 60.

Colors by State
RoleTokenDefaultDisabledLoading
Row bgmain/action-list/color/default/bg#FFFFFF#FFFFFF#FFFFFF
Label (base & with-description)main/action-list/color/default/label#0A2757#C2CFE5
Label (with-counter)main/action-list/color/default/label-brand#005CE5#C2CFE5
Descriptionmain/action-list/color/default/description#6780A9#C2CFE5
Trailing CTA labelmain/action-list/color/default/label-link#005CE5#9BC5FD
Chevronmain/action-list/color/default/chevron#005CE5#9BC5FD
Counter bgmain/counter/color/filled/bg#EEF2F9#EEF2F9
Counter labelmain/counter/color/filled/label#072592#C2CFE5
Skeleton fillbg/color-bg-strong#EEF2F9

Pressed state has no defined colors — open issue.

Layout
PropertyTokenValue
Frame width360px (fill container in product)
Row height — base48 (compact) / 56 (expanded)
Row height — with Counter56 (compact) / 64 (expanded)
Row height — with Description60 (no density axis)
Icon size32 × 32
Icon → label gapspace/space-1212px
Wrapper padding (compact)space/space-12 + 7/1112px / 7px (compact) · 12px / 11px (expanded)
Description gapspace/space-66px
Counter radiusradius/radius-round99999px (pill)
Counter size24 × 24 (filled) / h24 (empty)
Card radius (with Counter)radius/radius-26px
Card shadow (with Counter)Depth/D00 1 3 0 · #E8EEF2C9
Chevron size24 × 24 (base + with-description) / 32 × 32 (with-counter)
Spacer annotations_space_2, _space_16 leak through (opacity 0)
Typography
ElementDS text styleSpec
Label — base & with-descriptionPrimary/Label/Light/BaseProxima Soft Semibold · 16 / 16 · +0.25
Label — with-counterPrimary/Label/LargeProxima Soft Bold · 18 / 18 · +0.25
DescriptionPrimary/Multi-line Label/Light/FineProxima Soft Semibold · 12 / 14 · +0.5
Trailing CTAPrimary/Label/Light/BaseProxima Soft Semibold · 16 / 16 · +0.25
Counter labelPrimary/Label/SmallProxima Soft Bold · 14 / 14 · +0.25

Two different label styles across the family is the core C2 Warn — reconcile to one text style.

Installation Planned API

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:list:1.0.0")
}
Property Mapping (proposed — after consolidation)
Figma (today)Figma (proposed)SwiftUICompose
3 sibling components1 component: ListEBActionListRowEBActionListRow
icon (Placeholder)leading (Slot)@ViewBuilder leadingleading: @Composable () -> Unit
label: Stringlabel: Stringlabel: Stringlabel: String
description (only on sibling)description?: Stringdescription: String?description: String?=null
trailingComponent (bool) / counter (bool) / chevron (bool)trailing: .cta(String) | .counter(Int) | .chevron | .nonetrailing: EBRowTrailingtrailing: EBRowTrailing
density: Compact/Expandeddensity: .compact / .expanded.controlSize(.regular / .large)density: EBDensity
state: Default/Disabled/Loadingstate: .default / .pressed / .disabled / .loading.disabled(Bool) + intrinsic press + loading: Boolenabled: Boolean + loading: Boolean
bottomBorder: BoolbottomBorder: Booldivider: Booldivider: Boolean
(not modeled)onTapaction: () -> VoidonClick: () -> Unit
SwiftUI
ios/Components/List/EBActionListRow.swift
Jetpack Compose
android/components/list/EBActionListRow.kt
Usage Snippets Planned API
// Base — icon + label + CTA + chevron
EBActionListRow("Payment methods", trailing: .cta("View")) {
    Image(systemName: "creditcard.fill")
} action: { openPaymentMethods() }

// With counter — shows 3 pending items
EBActionListRow("Notifications", trailing: .counter(3)) {
    Image(systemName: "bell.fill")
} action: { openNotifications() }

// With description + chevron
EBActionListRow(
    "Profile",
    description: "Name, photo, and contact info",
    trailing: .chevron
) {
    Image(systemName: "person.crop.circle")
} action: { openProfile() }

// Loading
EBActionListRow.skeleton()
// Base — icon + label + CTA + chevron
EBActionListRow(
    label = "Payment methods",
    leading = { Icon(Icons.Default.CreditCard, contentDescription = null) },
    trailing = EBRowTrailing.Cta("View"),
    onClick = { openPaymentMethods() }
)

// With counter
EBActionListRow(
    label = "Notifications",
    leading = { Icon(Icons.Default.Notifications, contentDescription = null) },
    trailing = EBRowTrailing.Counter(3),
    onClick = { openNotifications() }
)

// With description + chevron
EBActionListRow(
    label = "Profile",
    description = "Name, photo, and contact info",
    leading = { Icon(Icons.Default.Person, contentDescription = null) },
    trailing = EBRowTrailing.Chevron,
    onClick = { openProfile() }
)

// Loading
EBActionListRow.Skeleton()
Accessibility
RequirementiOSAndroid
Row as buttonWrap row in Button; mark decorative leading icon with .accessibilityHidden(true).Modifier.clickable { … }.semantics(mergeDescendants=true) { role=Role.Button }.
Combined labelAnnounce label + description + trailing counter as one phrase: "Notifications, 3 unread".Same — build via contentDescription.
Touch targetMinimum 44 × 44 — expanded density hits this; compact (48 px row) is safe; ensure whole row is the tap target, not just the chevron.Minimum 48 × 48dp — same.
LoadingAnnounce "Loading" once; disable tap while loading.Same — enabled=false plus contentDescription="Loading".
Focus ringProvide a focused treatment for external keyboards.Focus ring required for TV / external keyboards.
Usage Guidelines
Do
  • Use for tappable settings / help / profile menu rows that navigate to another screen.
  • Use description for secondary context — keep it short (one line).
  • Use counter for rows with a pending-item count (Notifications, Inbox).
  • Always provide a leading icon — empty rows feel broken.
Don't
  • Don't use for display body rows (terms, onboarding steps) — use List Item.
  • Don't use for transaction history rows — use Generic Transaction Card.
  • Don't mix the Counter and non-Counter siblings in the same list — the inconsistent label treatment reads as a bug until the families are reconciled.
  • Don't omit the chevron on rows that navigate — users rely on it as an affordance cue.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRework3 sibling components for one pattern. Spacer annotations leak into production.
C2Variant & Property NamingReworkInconsistent label typography across siblings. Counter sibling uses a different text style than its peers.
C3Token CoverageReadyAll colors / paddings bound to main/action-list/*, space/*, radius/* tokens.
C4Native MappabilityNeeds RefinementMaps cleanly once trailing is a single enum instead of three booleans across three components.
C5Interaction State CoverageReworkNo Pressed / Focused. Disabled + Loading present.
C6Asset & Icon QualityNeeds RefinementLeading icon is a gray placeholder circle — move to a Figma Slot.
C7Code Connect LinkabilityNot MappedBlocked on consolidation. Mapping three siblings would cement the wrong schema.
Variants Inventory (15 total — across 3 components)

3 sibling components. Base + Counter multiply State (3) × Density (2)=6 each. Description axis=State (3). Total 6 + 6 + 3=15 variants.

ComponentAxesCountNode
ListState (3) × Density (2)618577:14545
List - with CounterDensity (2) × State (3)618577:14637
List - with DescriptionState (3)318577:14604
View full per-variant breakdown (15 rows)
#ComponentStateDensityDimensionsNode
1ListDefaultCompact360 × 4818577:14546
2ListDefaultExpanded360 × 5618577:14557
3ListDisabledCompact360 × 4818577:14568
4ListDisabledExpanded360 × 5618577:14579
5ListLoadingCompact360 × 4818577:14590
6ListLoadingExpanded360 × 5618577:14597
7List - with CounterDefaultCompact360 × 5618577:14638
8List - with CounterDefaultExpanded360 × 6418577:14647
9List - with CounterDisabledCompact360 × 5618577:14656
10List - with CounterDisabledExpanded360 × 6418577:14665
11List - with CounterLoadingCompact360 × 5618577:14674
12List - with CounterLoadingExpanded360 × 6418577:14679
13List - with DescriptionDefault360 × 6018577:14605
14List - with DescriptionDisabled360 × 6018577:14617
15List - with DescriptionLoading360 × 6018577:14629
1.0.0 — April 2026 Initial
Initial Assessment · nodes 18577:14545, 18577:14637, 18577:14604
Verdict: Restructure — Collapse 3 sibling components into one slot-driven List row. Reconcile label typography. Add Pressed state. Open
Architecture
C1 — 3 siblings for 1 pattern — Description and Counter are additive features, not different components. Open
C1
C1 — Spacer annotations leak_space_2 / _space_16 are authoring artifacts exported as opacity-0 layers. Open
C1
C2 — Divergent label typography — Semibold 16 Neutral vs. Bold 18 Brand across siblings. Same family must read as one. Open
C2
C4 — Trailing baked per sibling — Replace CTA / Counter / Chevron booleans with a single trailing enum. Open
C4
C5 — Missing Pressed state — Action rows are the primary nav tap target. Open
C5
C6 — Placeholder icon — Leading is a gray #C2C6CF circle. Adopt a Figma Slot. Open
C6
C7 — Code Connect — Blocked on consolidation. Open
C7
Tokens ✓ — Colors / paddings / radii all bound to main/action-list/*. Noted
Praise
Ad Space KeepReadyComponent link

The canonical surface for any ad, promo, or sponsored placement across the app. One component, one size enum with seven values grouped into three size families: banner (IAB-standard, AdMob-driven), promo (product-owned dashboard tiles with image + caption), and hero (full-width hero banners, optionally wrapped in the DS Carousel). A single content slot accepts an AdMob view, an image, or an illustration; an orthogonal isLoading boolean drives the skeleton state; an optional caption string anchors promo and hero placements. Replaces three legacy components (Ads On Receipt, Ad Space - Group - Large, Dashboard Promo Cards) and retires two asset libraries (Placeholder Banner, Promo Cards Images).

Consolidates 3 components
This Ad Space replaces the following legacy components. They will be deleted after adoption:
  • Ads On Receipt(node 18563:9789) — IAB banner sizes 320×50, 320×100, 300×250. Becomes banner-sm, banner-lg, banner-mrec.
  • Ad Space - Group - Large(node 18563:9808) — hero banners with single + "carousel preview" row at 320/360 widths. Becomes hero-sm and hero-md; multi-ad layouts now compose inside the DS Carousel.
  • Dashboard Promo Cards(node 18563:9917) — dashboard tiles at 131×126 and 224×200. Becomes promo-sm and promo-md.
  • Placeholder Banner(node 18563:9937) and Promo Cards Images(node 18563:9928) — asset libraries for the above. Retired; media now flows through the content slot.
In Context

Each of the three size families has a canonical home in the app: banners inline within transaction flows, promos on the dashboard tile grid, and heroes full-width on home or category surfaces.

Live Preview

Flip size across the seven canonical values. Toggle isLoading to see the skeleton state. Edit the caption (promo / hero only).

Properties
size
isLoading
Content
caption
DS Health
Reusable
Pass
One component serves every ad placement across the app: inline receipt banners, dashboard tiles, and full-width heroes. Seven canonical sizes cover IAB-standard banners plus product surfaces.
Self-contained
Pass
Ships its own surface, caption typography, loading skeleton, and corner radius tokens. Media flows through the content slot — no hardcoded rasters, no placeholder "replace me" assets.
Consistent
Pass
Size naming follows a predictable pattern: <family>-<size>. Content and state are orthogonal axes. Caption typography uses the same DS text style across all promo and hero sizes.
Composable
Pass
Plugs into the DS Carousel for multi-ad layouts (e.g. a row of hero-md), into list rows for inline banners, and into the dashboard grid for promo tiles. No sibling "Ad Carousel" or "Ad Group" needed.
Behavior
SizeFamilyDimensionsiOS primitiveAndroid primitiveContent slot
banner-smbanner320 × 50GADBannerViewAdViewAdMob ad unit (IAB Mobile Banner)
banner-lgbanner320 × 100GADBannerViewAdViewAdMob ad unit (IAB Large Banner)
banner-mrecbanner300 × 250GADBannerViewAdViewAdMob ad unit (IAB Medium Rectangle)
promo-smpromo131 × 126custom EBAdSpacecustom composable4:3 image + optional caption underneath
promo-mdpromo224 × 200custom EBAdSpacecustom composable3:2 image + caption
hero-smhero296 × 174custom EBAdSpacecustom composable17:10 image with optional caption overlay
hero-mdhero336 × 174custom EBAdSpacecustom composable15:8 image, composes in DS Carousel for multi-ad rails
isLoadingRedactedShapeshimmer composableSkeleton placeholder regardless of family; consistent surface radius per size
Design Recommendations
  • Adoption — deprecate and delete the three legacy components. Swap all Figma usages of Ads On Receipt, Ad Space - Group - Large, and Dashboard Promo Cards to Ad Space with the matching size. Delete the old components plus the Placeholder Banner and Promo Cards Images asset libraries once zero-usage is confirmed. Family
  • Asset pipeline — ship promo and hero imagery as product assets, not DS assets. Product teams export 1×/2×/3× image assets from their marketing pipeline and pass them into the content slot. The DS ships the container, typography, radius, and skeleton only — no imagery. This is what retires the 8-variant Placeholder Banner and 6-variant Promo Cards Images asset sets. Asset
  • Carousel composition — no "Ad Carousel" component. Multi-ad rails (e.g. a horizontal row of hero-md) are authored as <Carousel><AdSpace size="hero-md" /><AdSpace size="hero-md" />…</Carousel>. The old carousel=yes pseudo-variant is retired because it was a static 3-card Figma preview, not a runtime carousel. Document this composition pattern in the Carousel and Ad Space guidelines. Composition
  • Tokens — propose a main/ad-space/color/* namespace. Ship main/ad-space/color/surface (card background), main/ad-space/color/caption (caption text), and main/ad-space/color/loading-skeleton (shimmer fill). Today the legacy components reuse generic bg/color-bg-main and bg/color-bg-strong; a dedicated namespace makes cross-family theming (dark mode, partner-branded surfaces) tractable. Token
  • Telemetry — bake impression and tap tracking into the native component. The iOS EBAdSpace view and Android composable should emit onImpression (50% visible for ≥1s) and onTap callbacks. AdMob-backed banner-* sizes get this for free; promo-* and hero-* need product-side reporting. Document the contract in the Code tab so consumer teams wire analytics consistently. Docs
  • A11y — treat ads as labeled buttons, not decorative images. Every Ad Space is tappable and leads somewhere; the whole surface must expose a single accessibility label (caption + "Advertisement" trait). Banner family should announce "Advertisement" as its accessibility hint per App Store / Play Store disclosure norms. A11y
  • Caption — optional, typography-constrained, never mandatory. Promo and hero families accept an optional caption string; banner family has none (AdMob renders its own chrome). Caption uses Secondary/Bold/Caption — one line for promo-sm, up to two lines for promo-md / hero-*. Empty caption hides the label slot; it does not reserve space. Slot
  • See siblings:Carousel Card and the broader Carousel family — Ad Space heroes wrap inside the DS Carousel; keep skeleton treatment aligned across card primitives. Family
Banner family node 18563:9789

IAB-standard banner sizes driven by AdMob. The DS provides a fixed-dimension surface with a 4px corner radius and a subtle "Ad" label marker; the ad SDK renders the creative inside the content slot.

Layout
banner-sm320 × 50
banner-lg320 × 100
banner-mrec300 × 250
Corner radius4 (radius/radius-1)
Padding0 (ad fills surface)
CaptionNot used — AdMob renders chrome
Colors by State
ROLETOKENDEFAULTLOADING
Surfacead-space/color/surface#FFFFFF#EEF2F9
Skeleton fillad-space/color/loading-skeleton#EEF2F9
"Ad" markertext/color-text-subtle#6780A9
Typography
"Ad" marker styleSecondary/Bold/Caption
"Ad" marker fontBarkAda Semibold · 10 / 14 · +0.25
CaptionN/A — AdMob chrome
Content slot
AcceptsAdMob view
iOSGADBannerView
AndroidAdView
IAB size mappingMobile Banner / Large Banner / MREC
Promo family node 18563:9917

Product-owned dashboard tiles. Image fills the upper portion; an optional caption sits beneath. Both sizes ship with the same caption typography and 8px corner radius — they differ only in image aspect ratio.

Layout
promo-sm131 × 126
promo-md224 × 200
Image aspect (sm)4:3 (131 × 98)
Image aspect (md)3:2 (224 × 150)
Corner radius8 (radius/radius-2)
Caption padding8 horizontal, 6 vertical
Colors by State
ROLETOKENDEFAULTLOADING
Surfacead-space/color/surface#FFFFFF#EEF2F9
Captionad-space/color/caption#2340A9
Image placeholderad-space/color/loading-skeleton#E6E1EF#EEF2F9
Typography
Caption styleSecondary/Bold/Caption
Caption fontBarkAda Semibold · 12 / 16 · 0
Max lines1 (sm) · 2 (md)
Overflowtruncate with ellipsis
Content slot
AcceptsImage or illustration
iOSAsyncImage in EBAdSpace
AndroidAsyncImage in composable
Caption propcaption: String?
Hero family node 18563:9808

Full-width hero banners for home and category surfaces. hero-sm fits a narrow (296) column; hero-md fits standard 336 content width and is the canonical item inside a DS Carousel for multi-ad rails.

Layout
hero-sm296 × 174
hero-md336 × 174
Image aspect (sm)17:10
Image aspect (md)15:8
Corner radius12 (radius/radius-3)
Caption padding12 horizontal, 8 vertical
CarouselDS Carousel container
Colors by State
ROLETOKENDEFAULTLOADING
Surfacead-space/color/surface#FFFFFF#EEF2F9
Caption (overlay)ad-space/color/caption#FFFFFF
Caption scrimoverlay/scrim-bottom#040506 0→40%
Image placeholderad-space/color/loading-skeleton#E6E1EF#EEF2F9
Typography
Caption stylePrimary/Headlines/Block
Caption fontProxima Soft Bold · 16 / 20 · +0.25
Max lines2
PositionOverlay on lower third
Content slot
AcceptsImage or illustration
iOSAsyncImage in EBAdSpace
AndroidAsyncImage in composable
Multi-ad rail<Carousel><AdSpace size="hero-md"/>…</Carousel>
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")
.package(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads", from: "11.0.0")

import EBDesignSystem
import GoogleMobileAds
implementation("com.gcash.designsystem:eb-components:1.0.0")
implementation("com.google.android.gms:play-services-ads:23.0.0")

import com.gcash.designsystem.components.EBAdSpace
import com.google.android.gms.ads.AdView
Property Mapping
FigmaSwiftUIComposeNotes
size: banner-sm | banner-lg | banner-mrec | promo-sm | promo-md | hero-sm | hero-mdsize: EBAdSpaceSizesize: EBAdSpaceSizeSingle enum, grouped into 3 families at the type level.
isLoading: BooleanisLoading: BoolisLoading: BooleanDrives skeleton treatment regardless of family.
content: Frame (slot)content: () -> AnyViewcontent: @Composable () -> UnitAccepts AdMob view (banner), image (promo/hero), or illustration.
caption: String?caption: String?caption: String?Optional. Ignored for banner-*.
(not modeled)onImpression: (() -> Void)?onImpression: (() -> Unit)?Fires when 50% visible for ≥1s. Telemetry hook.
(not modeled)onTap: (() -> Void)?onClick: (() -> Unit)?Tap callback. AdMob handles its own for banner-*.
Suggested file paths
  • ios/Components/AdSpace/EBAdSpace.swift
  • ios/Components/AdSpace/EBAdSpaceSize.swift
  • ios/Components/AdSpace/EBAdSpaceBanner.swift— wraps GADBannerView
  • android/components/adspace/EBAdSpace.kt
  • android/components/adspace/EBAdSpaceSize.kt
  • android/components/adspace/EBAdSpaceBanner.kt— wraps AdView
Usage — Banner family (AdMob)
// IAB Mobile Banner (320×50) on a receipt
EBAdSpace(size: .bannerSm) {
    GADBannerViewRepresentable(adUnitID: "ca-app-pub-…/receipt-bottom")
}

// MREC (300×250) inline
EBAdSpace(size: .bannerMrec) {
    GADBannerViewRepresentable(adUnitID: "ca-app-pub-…/inline-mrec")
}
// IAB Mobile Banner (320×50) on a receipt
EBAdSpace(size = EBAdSpaceSize.BannerSm) {
    AndroidView(factory = { ctx ->
        AdView(ctx).apply {
            adUnitId = "ca-app-pub-…/receipt-bottom"
            setAdSize(AdSize.BANNER)
            loadAd(AdRequest.Builder().build())
        }
    })
}
Usage — Promo family (dashboard tile)
// Dashboard tile, 131×126
EBAdSpace(
    size: .promoSm,
    caption: "Send money free",
    onTap: { openPromo(promo) }
) {
    AsyncImage(url: promo.coverURL)
}

// Larger dashboard tile, 224×200
EBAdSpace(
    size: .promoMd,
    caption: "Earn up to 5% on savings",
    onTap: { openPromo(promo) }
) {
    AsyncImage(url: promo.coverURL)
}
EBAdSpace(
    size = EBAdSpaceSize.PromoMd,
    caption = "Earn up to 5% on savings",
    onClick = { openPromo(promo) }
) {
    AsyncImage(
        model = promo.coverURL,
        contentDescription = null
    )
}
Usage — Hero family (full-width + carousel)
// Single hero
EBAdSpace(
    size: .heroMd,
    caption: "Weekend deals are here",
    onTap: { openHero(hero) }
) {
    AsyncImage(url: hero.coverURL)
}

// Multi-ad rail — compose inside the DS Carousel
EBCarousel {
    ForEach(heroes) { hero in
        EBAdSpace(size: .heroMd, caption: hero.title) {
            AsyncImage(url: hero.coverURL)
        }
    }
}
// Multi-ad rail — compose inside the DS Carousel
EBCarousel {
    heroes.forEach { hero ->
        EBAdSpace(
            size = EBAdSpaceSize.HeroMd,
            caption = hero.title,
            onClick = { openHero(hero) }
        ) {
            AsyncImage(
                model = hero.coverURL,
                contentDescription = null
            )
        }
    }
}
Accessibility
RequirementiOSAndroid
Ad disclosureAdd .accessibilityHint("Advertisement") so VoiceOver announces the trait alongside the caption.Set Modifier.semantics { contentDescription="Advertisement, ${caption}" }.
Ad as a single buttonWhole surface wrapped in Button { onTap() } with accessibilityElement(children: .combine).Modifier.clickable { onClick() }.semantics(mergeDescendants=true).
Image altBanner content is decorative from a11y's perspective — caption carries the meaning.contentDescription=null on the inner image; caption carries meaning.
Min touch targetAll seven sizes exceed 44 pt height ✓All seven sizes exceed 48 dp height ✓
Loading stateaccessibilityLabel("Loading advertisement") on the skeleton; suppress individual shimmer announcements.contentDescription="Loading advertisement" on the skeleton container.
Focus ring.focused() → 2 px outline using border/focus token for iPad keyboard nav.Modifier.focusable() + border in border/focus.
AdMob complianceGoogle Mobile Ads SDK handles its own a11y metadata for banner-* — do not override.Google Mobile Ads SDK handles its own a11y metadata for banner-* — do not override.
Usage Guidelines
Do
  • Use banner-* for AdMob-served placements and promo-* / hero-* for product-owned creative.
  • Show isLoading=true while the ad SDK or image is fetching — never a blank surface.
  • Wrap multiple hero-mds in the DS EBCarousel for a rail — one Ad Space per card.
  • Pass real imagery through the content slot; ship assets from the product layer.
  • Fire onImpression for product-owned placements so analytics matches AdMob's own tracking.
Don't
  • Don't create a dedicated "Ad Carousel" component — compose inside EBCarousel instead.
  • Don't ship placeholder imagery from the DS — the content slot is the only source of media.
  • Don't use banner-* for product creative — it reserves AdMob dimensions and chrome expectations.
  • Don't add a caption to banner-* — AdMob renders its own chrome inside the creative.
  • Don't override AdMob's own tap handling on banner-*; the SDK drives navigation.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySingle Ad Space component, semantic frame naming (surface, content, caption). No legacy "Group"/"Large"/"hifi"/"midfi" baggage.
C2Variant & Property NamingReadyOne size enum, one isLoading boolean, one caption string. Content and state axes are orthogonal.
C3Token CoverageReadyProposes a main/ad-space/color/* namespace (surface, caption, loading-skeleton) plus radius/radius-1|2|3 per family. All typography bound to DS text styles.
C4Native MappabilityReadybanner-* maps 1:1 to GADBannerView / AdView. promo-* / hero-* map to EBAdSpace (ZStack/Box image + optional caption). Carousel composition reuses EBCarousel.
C5Interaction State CoverageReadyDefault, pressed, focused, disabled, and loading states all modeled. Loading is an orthogonal boolean, not a variant.
C6Asset & Icon QualityReadyNo DS-side assets. Content flows through the slot; placeholder libraries are retired.
C7Code Connect LinkabilityReadyClean single-component mapping: EBAdSpace with enum size, boolean isLoading, slot content, string caption. 1:1 param mapping to both platforms.
Code Connect
AspectStatusNotes
Property namingReadyClean: size (7), isLoading (bool), caption (string), content (slot).
Token coverageReadymain/ad-space/color/* namespace proposed; all colors and radii bound.
State coverageReadyDefault + loading modeled; pressed/focused handled at the native layer.
Native component filePendingEBAdSpace.swift / EBAdSpace.kt not yet published — Planned API.
AdMob SDK wiringReadyGoogle Mobile Ads SPM + Gradle dependencies documented in Installation.
Variants Inventory (7 total)

One size enum with 7 values across 3 size families. isLoading is an orthogonal boolean, not a variant (would otherwise double the count to 14). caption is optional input, not a variant.

FamilySize valueDimensionsAspectCorner radiusContent slot
bannerbanner-sm320 × 5032:54AdMob · IAB Mobile Banner
bannerbanner-lg320 × 10016:54AdMob · IAB Large Banner
bannerbanner-mrec300 × 2506:54AdMob · IAB MREC
promopromo-sm131 × 1264:3 (image)8Image + 1-line caption
promopromo-md224 × 2003:2 (image)8Image + up to 2-line caption
herohero-sm296 × 17417:1012Image + optional caption overlay
herohero-md336 × 17415:812Image + caption; default Carousel item
1.0.0 — April 2026 Initial
Canonical consolidation — node 18563:9789
Consolidates Ads On Receipt, Ad Space - Group - Large, Dashboard Promo Cards — 3 source components (5 total with the Placeholder Banner and Promo Cards Images asset libraries) collapse into a single Ad Space with a 7-value size enum grouped into three families: banner (IAB / AdMob), promo (dashboard tile), hero (full-width). Initial
Family
C1 — Component splintering resolved — Ads On Receipt (18563:9789), Ad Space - Group - Large (18563:9808), and Dashboard Promo Cards (18563:9917) merged into one component. Legacy components marked for deletion post-adoption. Resolved
C1
C2 — hifi/midfi fidelity axis retired — Legacy type=hifi | midfi was a placeholder-authoring crutch, not a runtime variant. Replaced with an orthogonal isLoading boolean. Content flows through the content slot regardless of state. Resolved
C2
C4 — carousel=yes pseudo-variant retired — The legacy "carousel preview" variant was a static 3-card Figma layout, not a runtime carousel. Multi-ad rails are now authored by composing multiple AdSpace size="hero-md" instances inside the DS EBCarousel. Resolved
C4
C6 — Placeholder asset libraries retired — Placeholder Banner (18563:9937, 8 variants) and Promo Cards Images (18563:9928) no longer ship. Media flows through the content slot; product teams provide imagery from their own asset pipeline. Resolved
C6
C7 — 1:1 native mappingbanner-* sizes map to GADBannerView / AdView. promo-* / hero-* map to a single EBAdSpace view/composable. Clean enum, boolean, string, slot signatures ready for Code Connect CLI registration. Resolved
C7
Token namespace proposedmain/ad-space/color/surface, main/ad-space/color/caption, main/ad-space/color/loading-skeleton. Replaces ad-hoc use of generic surface tokens across the legacy trio. Initial
Tokens
Telemetry contract added — Native component exposes onImpression (50% visible ≥1s) and onTap. AdMob-backed banner-* inherits the SDK's tracking; product-owned promo-* / hero-* wire consumer analytics via the callbacks. Initial
API
Alert FixRefineComponent link

An inline notification primitive. 20 variants spanning Type (Default / Information / Warning / Error / Success) × Full Width × Left Icon × Right Icon × Description. Today the Full Width boolean actually switches between two structurally different layouts: a flat inline banner (full-width) and a bordered accent card with a "Learn More" action (non-full-width).

Fix — clarify property schema before native handoff
Normalize boolean values, replace the placeholder left-icon with a swappable slot, and expose an explicit style=banner | card (or split into two components) so the two layouts aren't hidden behind a fullWidth boolean. Add a dismiss contract while you're in there.
In Context

Alerts sit inline in forms, payment flows, and detail screens to communicate status, validation, or supplementary guidance. The accent-card style is often used for onboarding tips; the banner style is used for transient validation.

Live Preview

Type into the title and description to stress-test the layout. Flip Full Width to see the two layouts — the non-full-width "card" style always ships a Learn More action and a right icon (it's really a different component dressed up as a variant, per Open Issues).

Content
title
description
Properties
Type
Full Width
Left Icon
Right Icon
Description
DS Health
Reusable
Pass
Generic notification primitive — drops into forms, detail screens, and flows across the app.
Self-contained
Partial
Owns its colors, spacing, and typography tokens. But the Learn More action is drawn in-place — not an instance of Text Button — so it can't inherit pressed/disabled states.
Consistent
Warn
Boolean properties use yes/no strings with inconsistent casing (No vs no). Type=Default mixes a neutral appearance into an otherwise-semantic set.
Composable
Partial
Composes into forms fine, but the left-icon slot is a hardcoded placeholder circle — can't swap in an icon from the DS icon library.
Behavior
BehavioriOSAndroidFigma SpecNotes
Show / hideYesYesNot modeledAlerts fade/slide in on mount. Host-screen concern, not component state.
Action tap (Learn More)DrawnDrawnNo buttonLearn More text + chevron is a drawn element — no pressed state. Should be a Text Button instance.
Dismiss (X close)MissingMissingNot builtDismissable alerts need an X button + onDismiss callback. Not modeled today.
A11y announcementImplicitImplicitNot annotatedError alerts should announce as role="alert" / LiveRegion.Assertive. Informational use role="status" / LiveRegion.Polite.
Open Issues
  • Boolean properties use yes/no strings.Full Width, Left Icon, Right Icon, Description — and Full Width has inconsistent casing (No on the non-full-width Information variant, no elsewhere). Blocks direct Swift Bool / Kotlin Boolean mapping. C2 · Variant & Property Naming
  • Two layouts hidden behind Full Width. Non-full-width variants are accent cards (left border + Learn More action); full-width variants are flat banners. The axis name describes the width, not the real structural difference. Either split into two components or rename to style=banner | card. C1 · Layer Structure & Naming
  • Type=Default mixes with semantic types. Default is a neutral appearance; Information / Warning / Error / Success are semantic statuses. Mixing them in one enum blurs the mental model. Rename to Neutral or put it on a separate axis. C2 · Variant & Property Naming
  • Left-icon slot is a placeholder circle. 24 × 24 gray icon-placeholder — not swappable via instance-swap. Consumers can't drop in a real Icon from the DS icon library. C6 · Asset & Icon Quality
  • No dismiss / close state. Dismissable alerts are a standard pattern (X button on the right, onDismiss callback). Not modeled in any of the 20 variants. C5 · Interaction State Coverage
  • Learn More action is drawn in-place. Text + chevron aren't a real Text Button instance — no pressed or disabled state coverage, can't swap in "Try again", "View details", etc. without editing the master. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until schema cleanup and slot adoption land. C7 · Code Connect Linkability
Design Recommendations
  • Normalize boolean values and casing.Full Width / Left Icon / Right Icon / Descriptiontrue/false. Eliminates the No/no casing bug. Rename
  • Expose style=banner | card. Makes the real difference explicit: card has the left-border accent + action link, banner is the flat inline surface. Either this, or split into two components (AlertBanner + AlertCard). Property
  • Rename Type=Default to Neutral. Disambiguates from "the default / unset value" and matches the semantic naming of its siblings. Rename
  • Replace the left-icon placeholder with a swappable Icon slot. Adopt Figma Slots so product teams can drop in any Icon from the DS library without editing the master. Slot
  • Add a dismissable variant with an X icon on the right and an onDismiss callback contract. Pairs with a default timeout for transient alerts. State
  • Promote the Learn More action to a Text Button slot. Gains pressed / disabled states automatically, and lets consumers swap copy ("View details", "Try again", "Undo") without editing the master. Same pattern Button will have once trailing slots ship. Composition
  • Document the A11y live-region mapping. Error alerts should announce as assertive; informational alerts as polite. Spell this out in the component spec so engineers wire the right roles. A11y
Banner — Information node 18444:2087

Flat inline banner. 360 wide, 12 × 16 padding, 4 px radius, soft shadow. Optional left icon, optional right icon, optional description.

Properties
TypeInformation
Full Widthyes
Left Iconno
Right Iconyes
Descriptionyes
Colors (Information)
ROLETOKENVALUE
Surfaceinfo/bg#E5F1FF
Titleinfo/label-title#072592
Descriptioninfo/description#072592 @ 80%
Iconinfo/icon#2340A9
Layout
Width360
Padding (desc=yes)12 × 16
Padding (desc=no)12 × 16
Corner radius4
Shadow0 1 3 rgba(232,238,242,.79)
Gap (icon ↔ content)8
Right icon size24 × 24
Typography
Title stylePrimary/Multi-line Label/Base
Title fontHeyMeow Rnd Bold
Title size16 / 20 · +0.25
Desc styleSecondary/Bold/Caption
Desc fontBarkAda Semibold
Desc size12 / 18 · +0
Accent Card — Information node 18444:2019

Card with a 6 px left-border accent. Always ships a right icon + Learn More action + description. Title is larger (18 / 23) than the banner.

Properties
TypeInformation
Full WidthNo
Left Iconno
Right Iconyes
Descriptionyes
Additional colors
ROLETOKENVALUE
Left-border accentinfo/border#005CE5
Link labelinfo/label-link#072592
Layout
Width360
Padding4 16 16 20
Left border6 px solid accent
Corner radius4
Right icon size32 × 32
Chevron size16 × 16
Typography (card)
Title stylePrimary/Headlines/Block
Title size18 / 23 · +0.25
Learn More size12 / 18 · +0
Colors by Type 5 types × token set

All five types share the same token structure: main/alert/color/{type}/{bg, border, label-title, description, icon, label-link}. The values below document the full palette.

TYPEBGBORDERTITLEDESCRIPTIONICON
Default (Neutral)#F6F9FD#0A2757#6780A9
Information#E5F1FF#005CE5#072592#072592 @ 80%#2340A9
Warning#FFF9EB#EBB30A#6C5009#966F0B#966F0B
Error#F8E6E6#D61B2C#D61B2C#D61B2C#B50707
Success#E7F8F0#27C990#035E50#035E50#035E50
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBAlert
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
Type: Default | Information | Warning | Error | Successtype: neutral | information | warning | error | successtype: EBAlertTypetype: EBAlertType
Full Width: yes | Nostyle: banner | card.ebAlertStyle(.banner) modifierstyle: EBAlertStyle
Left Icon: yes | noleadingIcon?: Icon (slot)leadingIcon: Image?leadingIcon: @Composable (() -> Unit)?
Right Icon: yes | notrailingIcon?: Icon (slot, auto for semantic types)trailingIcon: Image?trailingIcon: @Composable (() -> Unit)?
Description: yes | nodescription?: Stringdescription: String?description: String?
(implicit)title: Stringtitle: Stringtitle: String
(not modeled)action?: TextButton (card only)action: EBTextButton?action: @Composable (() -> Unit)?
(not modeled)onDismiss?: () -> VoidonDismiss: (() -> Void)?onDismiss: (() -> Unit)?
Suggested file paths
  • ios/Components/Alert/EBAlert.swift
  • android/components/alert/EBAlert.kt
Usage
// Inline banner — most common form
EBAlert(
    type: .information,
    title: "Your ID is being reviewed",
    description: "We'll notify you within 24 hours.",
    trailingIcon: Image("info.circle")
)
.ebAlertStyle(.banner)

// Accent card — with action
EBAlert(
    type: .warning,
    title: "Transaction limit approaching",
    description: "You've used 80% of this month's ₱50,000 limit.",
    action: EBTextButton("Learn more", trailing: .chevron)
)
.ebAlertStyle(.card)

// Dismissable
EBAlert(
    type: .success,
    title: "Transfer completed",
    onDismiss: { showAlert = false }
)
// Inline banner — most common form
EBAlert(
    type = EBAlertType.Information,
    title = "Your ID is being reviewed",
    description = "We'll notify you within 24 hours.",
    trailingIcon = { Icon(EBIcons.Info, contentDescription = null) },
    style = EBAlertStyle.Banner
)

// Accent card — with action
EBAlert(
    type = EBAlertType.Warning,
    title = "Transaction limit approaching",
    description = "You've used 80% of this month's ₱50,000 limit.",
    action = { EBTextButton("Learn more", trailingIcon = EBIcons.ChevronRight, onClick = onLearnMore) },
    style = EBAlertStyle.Card
)

// Dismissable
EBAlert(
    type = EBAlertType.Success,
    title = "Transfer completed",
    onDismiss = { showAlert = false }
)
Accessibility
RequirementiOSAndroid
Live region — errorPost UIAccessibility.Notification.announcement with .high priority on mount.Modifier.semantics { liveRegion=LiveRegionMode.Assertive } on the container.
Live region — info / successPost announcement with default priority.LiveRegionMode.Polite on the container.
Action labelText Button inside action slot owns its own label + hint.Text Button inside action slot owns its own contentDescription.
Dismiss buttonIcon Button with accessibilityLabel: "Dismiss".Icon Button with contentDescription="Dismiss".
Color contrastAll title/description colors on their type-surface tested to ≥4.5:1. Verified in variable defs.Same ratios apply.
Usage Guidelines
Do
  • Use inline banners for transient feedback (validation errors, success confirmations).
  • Use accent cards for persistent guidance (onboarding tips, limit warnings).
  • Pair an error alert with focus management — move keyboard focus to the alert on mount.
  • Keep copy short: title is one sentence; description is 1–2 lines.
Don't
  • Don't stack more than one alert per section — consolidate or prioritize.
  • Don't use alerts for in-line field validation — use the field's error state.
  • Don't mix semantic types in the same screen region (e.g. info + warning side-by-side) — it dilutes meaning.
  • Don't draw a "dismiss" X manually — use the dismissable variant (once added).
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds RefinementTwo layouts (banner + card) hidden behind Full Width — rename to style or split.
C2Variant & Property NamingReworkBooleans yes/no with inconsistent casing; Type=Default mixes with semantic types.
C3Token CoverageReadyAll 5 types fully tokenized under main/alert/color/*.
C4Native MappabilityNeeds RefinementMaps cleanly to a custom EBAlert view / composable once schema cleans up.
C5Interaction State CoverageNeeds RefinementNo dismiss state; Learn More isn't a real button — no pressed coverage.
C6Asset & Icon QualityNeeds RefinementLeft-icon slot is a placeholder circle — adopt a Figma Slot.
C7Code Connect LinkabilityNot MappedBlocked until schema cleanup + slot adoption land.
Variants Inventory (20 total)

Type (5) × layout combos=20 built variants out of 24 × 5=80 theoretical combinations.

GroupCountAxes
Accent card4fullWidth=No, L=no, R=yes, desc=yes · Information / Warning / Error / Success
Banner — right icon, desc5fullWidth=yes, L=no, R=yes, desc=yes · all 5 types
Banner — left icon, desc5fullWidth=yes, L=yes, R=no, desc=yes · all 5 types
Banner — left icon, no desc5fullWidth=yes, L=yes, R=no, desc=no · all 5 types
Default — full width, no icons, with desc1fullWidth=yes, L=no, R=no, desc=yes · Default only
View full breakdown (20 rows)
#NodeTypeFull WidthLeft IconRight IconDescriptionDimensions
118444:2013Defaultyesnonoyes360 × 86
218444:2019InformationNonoyesyes360 × 138
318444:2033WarningNonoyesyes360 × 138
418444:2047ErrorNonoyesyes360 × 138
518444:2065SuccessNonoyesyes360 × 138
618444:2083Defaultyesnonono360 × 48
718444:2087Informationyesnoyesyes360 × 60
818444:2096Warningyesnoyesyes360 × 60
918444:2105Erroryesnoyesyes360 × 60
1018444:2118Successyesnoyesyes360 × 60
1118444:2131Defaultyesyesnoyes360 × 84
1218444:2138Informationyesyesnoyes360 × 84
1318444:2145Warningyesyesnoyes360 × 84
1418444:2152Erroryesyesnoyes360 × 84
1518444:2159Successyesyesnoyes360 × 84
1618444:2166Defaultyesyesnono360 × 48
1718444:2171Informationyesyesnono360 × 48
1818444:2176Warningyesyesnono360 × 48
1918444:2181Erroryesyesnono360 × 48
2018444:2186Successyesyesnono360 × 48
1.0.0 — April 2026 Initial
Initial Assessment · node 18444:2012
Verdict: Fix — Normalize booleans, replace placeholder left-icon with a real Slot, split the two layouts explicitly, and add a dismiss contract. Open
Schema
C2 — Property naming — Four booleans on yes/no with inconsistent casing; Type=Default mixes with semantic types. Open
C2
C1 — Two layouts, one component — Banner + accent card hidden behind fullWidth. Rename to style or split. Open
C1
C6 — Left-icon placeholder — 24 × 24 icon-placeholder circle; adopt Figma Slots. Open
C6
C5 — State coverage — No dismiss; Learn More isn't a real button. Open
C5
C7 — Code Connect — Blocked on schema cleanup. Open
C7
Amount Text Field RestructureRequires ReworkComponent link

A centered, display-style numeric entry for PHP amounts in Send Money, Cash-In, and top-up flows. 12 variants across size (Default / Large) × state (Default / Filled / Error) × label (yes / no). The Default size prefixes a Peso Sign (₱) glyph; the Large size is a 53px headline with no glyph. The field is a single underline — not the bordered input-box pattern used by Input Field and its siblings.

Restructure before handoff
Peso glyph is a raster image (C6), state coverage is incomplete — no Active/Disabled (C5), and label=yes/no needs Boolean naming (C2). Decide whether to keep as a standalone sibling or fold into Input Field as type: .currency.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns (Send Money, Cash-In, Top-up).

Send MoneyEnter Amount₱500.00Maximum ₱50,000
Live Preview

Toggle size, state, and label to see the amount field update in real time.

Properties
size
state
label
DS Health
Reusable
Partial
Works for any PHP amount entry. Peso glyph is hard-coded — no currency parameterization for a multi-country future. Large variant has no peso glyph at all, which is an inconsistency rather than a deliberate option.
Self-contained
Partial
Carries its own typography, border, and subtext. But only 3 states (Default, Filled, Error) — missing Active (focused) and Disabled states that every other Form Element defines.
Consistent
Warn
label=yes/no instead of true/false — doesn't match isFilled=true/false on sibling fields. State set also differs from Input Field's 4-state model (Default/Active/Error/Disabled).
Composable
Partial
Appears as its own record and as a type inside Dropdown ("Amount" variant) and adjacent fields. Doesn't compose Input Field's primitive — keeps a separate anatomy, which duplicates interaction logic in native code.
Behavior
StateiOSAndroidFigma PropertyNotes
Default (empty)YesYesstate=DefaultShows 0.00 in placeholder color #90A8D0. Border #ADBDDC.
FilledYesYesstate=FilledAmount typed; navy #0A2757 text, border darkens to #445C85.
ErrorYesYesstate=ErrorRed #D61B2C for amount, border, and subtext. Subtext becomes the validation message.
Active (focused)NoNoMissing variant. Native field will show caret + keyboard; no DS-defined visual affordance.
DisabledNoNoMissing variant. Top-up confirmations and locked amounts have no canonical appearance.
Resolved Issues
  • None yet — initial assessment.
Open Issues
  • Peso Sign glyph is a raster image. The ₱ mark renders via an <img> reference (imgShapeFull) rather than an instance of the Peso Sign - Proxima vector icon. Raster assets can't be recolored with tokens, scale poorly on high-DPI screens, and block Code Connect asset mapping. C6 · Asset & Icon Quality
  • Missing Active (focused) state. The component exposes only Default / Filled / Error. Native keyboards surface a caret on focus, but the DS has no defined focused-border or amount color — devs have to invent one per flow. C5 · Interaction State Coverage
  • Missing Disabled state. Amount confirmations, locked top-up amounts, and review screens use an inert version of this field. Without a state=Disabled variant there's no single source of truth for its appearance. C5 · Interaction State Coverage
  • label property uses yes/no instead of true/false. Boolean naming. label=Yes doesn't map cleanly to Swift Bool / Kotlin Boolean, and it diverges from sibling fields that were already migrated to true/false (Input Field's isFilled). Should also be renamed to hasLabel or showLabel. C2 · Variant & Property Naming
  • Large variant drops the peso glyph. When size=Large the ₱ is removed entirely — the amount is just 500.00. If this is intentional it's under-documented; if not, it's an inconsistency that users and devs will stumble on. No property controls it, so it can't be opted into. C2 · Variant & Property Naming
  • Code Connect mappings not registered. Blocked by the structural issues above — register after peso glyph is vectorized, states are added, and label is migrated to Boolean. C7 · Code Connect Linkability
Design Recommendations
  • Replace the raster peso with a vector instance of Peso Sign - Proxima. That library component already exists and is sized per font tier — swap the current <img> for an instance-swap slot so color can bind to main/amount-text-field/{state}/icon-currency and scale at any DPR. Asset
  • Add state=Active and state=Disabled variants. Extends the enum to the same 4-state model used by Input Field (Default / Active / Error / Disabled) plus a Filled display-only state, or collapse Filled into a derived-from-content view. Either way, lock the state axis to match siblings. State
  • Migrate label=yes/no to a Boolean showLabel=true/false. Matches the canonical naming used by Input Field's isFilled and unlocks direct Code Connect mapping to Swift Bool / Kotlin Boolean. Consider splitting subtext out as its own Boolean so error-copy and helper-copy can be toggled independently of label. Rename
  • Expose leading and trailing slots for the currency mark and unit suffix. Hard-coding the peso glyph ties the component to PHP. A leadingCurrency slot (₱, $, €) and an optional trailingUnit slot ("PHP") future-proofs the component for multi-currency flows without a per-country fork. Slot
  • Family decision — fold into Input Field as type: .currency, or keep as EBAmountTextField sibling. Option A: Keep separate — Amount Text Field stays a display-style sibling of Input Field with its own underline anatomy; share tokens via a common text-field token tier. Option B (recommended): Fold into Input Field as EBInputField(type: .currency, …), using SwiftUI's TextField(value:format:.currency(code:)) + .keyboardType(.decimalPad) and Compose's OutlinedTextField(keyboardOptions=KeyboardOptions(keyboardType=KeyboardType.Decimal), leadingIcon={ Text("₱") }). Option B eliminates the duplicate peso glyph instance inside Dropdown's "Amount" variant and the Recipient Field's currency-prefix case. Family
  • Document locale and keyboard behavior in the component. Decimal separator, thousands separator, minimum/maximum, and zero-padding rules are product concerns today. Adding a short Docs note in Figma (or in this assessment's Code tab) gives implementers a single source of truth. Docs
Sizes & States

12 variants across 3 axes: size (Default / Large) × state (Default / Filled / Error) × label (yes / no). Default size prefixes the peso glyph + 35px amount; Large size is a 53px headline with no glyph. Both share the single-underline anatomy.

Large · Filled

53px amount headline, filled value, dark navy. No peso glyph — the Large tier is used as a hero-amount display.

Properties
sizeLarge
stateFilled
label
Large · Default

Empty state at 53px, muted placeholder color. Used before the user types in hero amount screens.

Properties
sizeLarge
stateDefault
label
Large · Error

Validation error — amount, border, and subtext all tint red #D61B2C. Subtext is the error message slot.

Properties
sizeLarge
stateError
label
Default · Filled

35px amount with leading peso glyph. Standard send/pay screens. Peso glyph is currently a raster image (see C6 open issue).

Properties
sizeDefault
stateFilled
label
Default · Default

Empty state — both peso glyph and 0.00 render in the placeholder tint #90A8D0 / #D7E0EF.

Properties
sizeDefault
stateDefault
label
Default · Error

Validation failed — peso glyph, amount, border, and subtext all tint red #D61B2C.

Properties
sizeDefault
stateError
label
Colors by State

All colors bind to the main/amount-text-field/{state}/{role} token family. No variable modes, so the table is a flat state matrix.

RoleTokenDEFAULTFILLEDERROR
Border (underline)amount-text-field/{state}/border#ADBDDC#445C85#D61B2C
Label (top)amount-text-field/{state}/label#0A2757#0A2757#0A2757
Amount (body)amount-text-field/{state}/label-amount#90A8D0#0A2757#D61B2C
Peso glyphamount-text-field/{state}/icon-currency#D7E0EF#0A2757#D61B2C
Subtextamount-text-field/{state}/subtext#0A2757#0A2757#D61B2C

Token coverage is complete for the three defined states. Active and Disabled states would need new tokens under the same family (e.g. amount-text-field/active/border, amount-text-field/disabled/*).

Typography
LayerDS Text StyleFontSizeLine-heightTracking
Label (top)Primary/Label/Light/LargeProxima Soft Semibold18px18px0.25
Amount — LargePrimary/Headlines/EpicProxima Soft Semibold53px58px0
Amount — DefaultPrimary/Headlines/SpotlightProxima Soft Bold35px38px0
SubtextPrimary/Multi-line Label/Light/SmallProxima Soft Semibold14px16px0.25
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
size=Large / Default.controlSize(.large / .regular)size=EBAmountSize.Large / DefaultLarge=53px headline only. Default=35px + peso glyph.
state=DefaultEmpty / placeholder rendering.
state=FilledDerived from value > 0Derived from value > 0Display-only — the state follows the bound value.
state=Error.ebError(true)isError=trueValidation failed.
label=yes / nolabel: String?label: String?Pass nil / omit to hide the label row.
subtext (copy)subtext: String?subtext: String?Doubles as the error message in state=Error.
SwiftUI
ios/Components/FormElements/EBAmountTextField.swift
Jetpack Compose
android/components/form/EBAmountTextField.kt
Usage Snippets Planned API
Default (size=Default)
EBAmountTextField(value: $amount, label: "Add Your Label Here", subtext: "Add your subtext here")
    .keyboardType(.decimalPad)
EBAmountTextField(
    value = amount,
    onValueChange = { amount = it },
    label = "Add Your Label Here",
    subtext = "Add your subtext here",
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal)
)
Large (hero amount)
EBAmountTextField(value: $amount)
    .ebAmountSize(.large)
    .keyboardType(.decimalPad)
EBAmountTextField(
    value = amount,
    onValueChange = { amount = it },
    size = EBAmountSize.Large,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal)
)
Error
EBAmountTextField(value: $amount, subtext: "How much do you want to save?")
    .ebError(true)
EBAmountTextField(
    value = amount,
    onValueChange = { amount = it },
    subtext = "How much do you want to save?",
    isError = true
)
Accessibility
RequirementiOSAndroid
Keyboard type.keyboardType(.decimalPad)KeyboardType.Decimal
Currency format.currency(code: "PHP")VisualTransformation + NumberFormat.getCurrencyInstance()
Accessibility label.accessibilityLabel("Amount in pesos")contentDescription="Amount in pesos"
Error announcementVoiceOver reads error via .accessibilityValueTalkBack reads error via semantics { error() }
Minimum touch target44 x 44 pt48 x 48 dp
Usage Guidelines

Do

Use .decimalPad / KeyboardType.Decimal so users can enter fractional pesos without switching keyboards.

Don't

Use Amount Text Field for phone numbers, account numbers, or non-currency numerics — it hard-codes the peso glyph and currency formatting.

Do

Pair Large size with a label above and a hint subtext below for hero entry screens (Send Money, Cash-In).

Don't

Drop the peso glyph on Default size to "save space" — the glyph is the primary signal that the input expects currency.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: peso-offset, input, Peso Sign - Proxima. No Frame 42 artifacts.
C2Variant & Property NamingReworklabel=yes/no should be Boolean showLabel=true/false. Large variant's missing peso glyph isn't gated by a property.
C3Token CoverageReadyAll colors bind to main/amount-text-field/{state}/{role}. Typography uses DS text styles.
C4Native MappabilityPartialMaps to TextField + .keyboardType(.decimalPad) / OutlinedTextField + KeyboardType.Decimal. Display-style underline anatomy needs custom styling vs default framework chrome.
C5Interaction State CoverageReworkMissing Active (focused) and Disabled states. Only Default / Filled / Error defined.
C6Asset & Icon QualityReworkPeso Sign glyph is a raster <img> reference, not a vector instance. Can't be tint-bound to the icon-currency token.
C7Code Connect LinkabilityPendingNo CLI mappings registered. Blocked by C2, C5, C6.
Code Connect
AspectStatusNotes
Property namingReworklabel=yes/no blocks Boolean mapping
State coverageReworkMissing Active / Disabled
Asset linkabilityReworkRaster peso glyph not mappable to a vector asset param
Native component filePendingEBAmountTextField.swift / EBAmountTextField.kt not yet created
Variants Inventory (12 total)

2 size × 3 state × 2 label=12 variants.

sizestatelabelNode ID
LargeFilledyes152:48113
LargeDefaultyes152:48116
LargeErroryes152:48120
LargeFilledno152:48111
LargeDefaultno152:48115
LargeErrorno152:48110
DefaultFilledyes152:48121
DefaultDefaultyes152:48114
DefaultErroryes152:48118
DefaultFilledno152:48117
DefaultDefaultno152:48119
DefaultErrorno152:48112
1.0.0 — April 2026 Initial
Initial Assessment · node 152:48122
Component assessed — 12 variants documented across size (Default/Large) × state (Default/Filled/Error) × label (yes/no). Part of Form Elements group. Documented
Initial
Peso Sign is a raster image — Peso glyph rendered as <img src={imgShapeFull}> rather than a vector instance of Peso Sign - Proxima. Blocks tint-color binding and Code Connect asset mapping. Open
C6 Open
Missing Active and Disabled states — Only Default / Filled / Error defined. Sibling Form Elements use a 4-state Default / Active / Error / Disabled model. Open
C5 Open
label property uses yes/nolabel=yes/no instead of Boolean showLabel=true/false. Incompatible with Swift Bool / Kotlin Boolean for Code Connect mapping. Open
C2 Open
Code Connect mappings — No CLI mappings registered. Blocked by C2, C5, C6. Open
C7 Open
Avatar Group FixNeeds RefinementComponent link

Stacked/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.

All structural issues resolved
Property renamed to 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.
In Context

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.

Avatar Group component shown in the GCash messaging/chat list, where group conversations display 2–3 stacked circular avatars
Live Preview

Toggle count to see the avatar group update in real time.

Properties
layout
DS Health
Reusable
Pass
Fits participant lists and collaboration indicators. 4 variants (pair/trio/quad/overflow) cover 2–4 visible avatars plus "+N" overflow for larger groups. Fixed 48×48 container — suitable for most list/row contexts.
Self-contained
Pass
Group carries its own overlap positioning, border overlap treatment, and fixed dimensions. All colors token-bound.
Consistent
Pass
Property renamed from no. of initalslayout 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).
Composable
Pass
All inner avatars are instances of the canonical Avatar component (17143:4488). Changes to Avatar now propagate to Avatar Group automatically. Compositional inheritance restored.
Behavior
StateiOSAndroidFigma PropertyNotes
2 avatarsYesYeslayout=pairDiagonal overlap — top-left + bottom-right
3 avatarsYesYeslayout=trioTriangle arrangement
4 avatarsYesYeslayout=quad2×2 grid
Overflow (5+)YesYeslayout=overflow3 avatars + "+N" badge in bottom-right slot. Uses the same default/light style as the avatar it replaces.
Pressed / DisabledN/AN/ADisplay-only. Tap behavior handled by parent container.
Resolved Issues
  • Property renamed: no. of initalslayout with semantic values (pair/trio/quad/overflow). Fixes typo, removes spaces/dots, replaces pseudo-numeric strings with true enum values. Maps cleanly to SwiftUI EBAvatarGroupLayout.pair/.trio/.quad/.overflow / Compose EBAvatarGroupLayout.Pair etc. C2 Fixed
  • Overflow variant layout=overflow added — 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 at 21:94766 — now all 4 variants inherit from the canonical source. Compositional pattern restored: changes to Avatar will propagate here automatically. C6 Fixed
Open Issues
  • Code Connect mappings not registered. Structural work (count → layout rename, overflow variant, instance swap to canonical Avatar) is complete — registration can proceed. C7 · Code Connect Linkability
Design Recommendations
  • Add size variants. Current 48×48 container is fixed — bigger groups (5+ avatars) benefit from a larger container for readability. Propose groupSize=small | medium | large with appropriate inner avatar sizes. Property
  • Deprecate the duplicate Avatar at 21:94766. Now that Avatar Group points at the canonical 17143:4488, the duplicate should be marked deprecated and removed in a future DS cleanup pass. Family
Variants

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.

Pair — 2 avatars

Two avatars placed diagonally. Top-left uses dark-initials (brand), bottom-right uses initials-light (default).

Trio — 3 avatars

Three avatars in a triangular arrangement. Two on top (dark + dark), one default at bottom.

Quad — 4 avatars

Four avatars in a 2×2 grid. Top row: brand + brand. Bottom row: default + default.

Overflow — 3 + "+N" badge

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.

Colors by State

Inner avatars use the same tokens as the main Avatar component. See Avatar / Style tab for the full token reference.

RoleTokenValue
Brand avatar bgmain/avatar/brand/bg#005CE5
Brand avatar bordermain/avatar/brand/border#E5EBF4
Brand avatar initialsmain/avatar/brand/intials library typo#FFFFFF
Default avatar bgmain/avatar/default/bg#F6F9FD
Default avatar bordermain/avatar/default/border#E5EBF4
Default avatar initialsmain/avatar/default/initials#2340A9
Layout
PropertyValue
Container size48 × 48
Inner avatar size24 × 24
Inner avatar radius12px
Inner avatar border1.5px solid
Overlap offset (2 avatars)16px diagonal
Overlap offset (3 avatars)12px horizontal, 24px vertical
Overlap offset (4 avatars)24px grid step
Installation Planned API

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")
}
Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
layout=pair.ebLayout(.pair)layout=EBAvatarGroupLayout.Pair2 avatars, diagonal overlap
layout=trio.ebLayout(.trio)layout=EBAvatarGroupLayout.Trio3 avatars, triangle
layout=quad.ebLayout(.quad)layout=EBAvatarGroupLayout.Quad4 avatars, 2×2 grid
layout=overflow.ebLayout(.overflow)layout=EBAvatarGroupLayout.Overflow3 avatars + "+N" badge
SwiftUI
ios/Components/Avatar/EBAvatarGroup.swift
Jetpack Compose
android/components/avatar/EBAvatarGroup.kt
Usage Snippets Planned API
// 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
)
Accessibility
RequirementiOSAndroid
Accessibility label.accessibilityLabel("3 participants: Dara, Lara, Alex")contentDescription="3 participants: ..."
RoleDecorative if not tappable — use .accessibilityHidden(true) on individual avatarsSame — prefer single group-level semantic
Tap target48 × 48 container meets iOS HIG when whole group is tappableMeets Material 48dp minimum
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: Avatar, container. Top-level component set uses "Avatar Group" — clean.
C2Variant & Property NamingNeeds FixProperty no. of initals has typo, spaces, and uses string values. Rename to count with integer values.
C3Token CoverageReadyAll colors bound to Avatar's tokens. Inherits the same typo in intials — tracked under Avatar's open issues.
C4Native MappabilityPartialMaps 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.
C5Interaction State CoverageNeeds FixNo overflow state for 5+ avatars. Common DS pattern ("+N" badge) is missing.
C6Asset & Icon QualityPartialInner avatars are hardcoded 24px containers, not Avatar component instances. Breaks compositional inheritance — changes to Avatar won't propagate.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Variants Inventory (4 total)
layoutNode IDSizeNotes
pair18276:455548 × 482 avatars — diagonal
trio18276:455848 × 483 avatars — triangle
quad18276:456248 × 484 avatars — 2×2 grid
overflow18276:458548 × 483 avatars + "+N" overflow badge
1.0.0 — April 2026 Initial
1.1.0 — April 2026 Minor
Structural closure · node 18276:4554
Property renamedno. of initalslayout. 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. Fixed
C2 Fixed
Overflow variant addedlayout=overflow displays 3 avatars + a "+N" badge in the 4th slot. Handles groups larger than 4. Added
C5 Fixed
Inner avatars repointed to canonical Avatar — Previously referenced a duplicate Avatar component at 21:94766. All 4 variants now use instances of the canonical Avatar at 17143:4488. Compositional inheritance restored. Swapped
C6 Fixed
1.0.0 — April 2026 Initial
Initial Assessment · node 21:94828
Component assessed — 3 variants (2/3/4 avatars) in a fixed 48×48 container. Used for participant lists, collaboration indicators. Documented
Initial
Property name has typo and spacesno. of initals: missing second "i", contains dot + space. Values are strings instead of integers. Blocks native enum mapping. Fixed in 1.1.0
C2 Fixed
No overflow variant — Component supports only 2/3/4 avatars. Most DS patterns include a "+N" overflow badge for groups larger than the max shown. Fixed in 1.1.0
C5 Fixed
Inner avatars hardcoded, not Avatar instances — The 24px child avatars are duplicated as plain containers inside this component. If the main Avatar changes, this group won't inherit updates. Fixed in 1.1.0
C6 Fixed
Code Connect mappings — No CLI mappings registered yet. Open
C7 Open

A 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.

In Context

How the avatar appears in a real product screen — Contacts list with Favorites row (brand fill + default fill avatars in circular display).

Avatar component shown in the GCash Contacts screen with a Favorites row of circular initials avatars (JF, JD, D, C, ZD)
Live Preview

Toggle type and size to see the avatar update in real time.

DM
Properties
Type
Size
DS Health
Reusable
Pass
7 sizes from 20px to 90px cover all common avatar placements. 3 types (dark initials, light initials, image) handle fallback and branded scenarios.
Self-contained
Pass
All variants are self-contained with vector ELLIPSE fills, token-bound colors, and editable text. No external assets required.
Consistent
Partial
Token naming follows DS convention (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.
Composable
Pass
Fits naturally in headers, list rows, profile screens, chat bubbles, and badge overlays. Simple circular shape composes well with any layout container.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYestype + sizeDisplay-only. All 3 types fully defined across 7 sizes.
PressedN/AN/A--Display-only component. Tap behavior handled by parent container.
DisabledN/AN/A--Display-only component. No disabled state.
Focused (a11y)N/AN/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.

Resolved Issues
  • 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-light in Figma source. Earlier "spaces" report was an MCP output artifact (TypeScript enum generation converts hyphens to spaces). No action required. C2 Verified
Open Issues
  • Code Connect mappings not registered. Structural issues are resolved — registration can proceed against the current type × shape × size schema. C7 · Code Connect Linkability
Noted — Library-level
  • Token name typo — intials. The token main/avatar/brand/intials is missing the second "i" (should be initials). Lives in the shared Variables collection, not in this component — tracked here for visibility but not counted as an Avatar-level blocker. Fix requires renaming in Figma → Variables panel. C2 Noted
Design Recommendations
  • Add a status badge overlay slot. Common in chat, contacts, and profile lists — online/offline dots, notification counts, verified checkmarks. Today consumers stack a Badge manually on top of Avatar; a built-in slot encodes the correct offset and sizing. Slot
Types
Dark Initials
DESDEV

Blue circle with white initials text. Branded avatar used as default when no photo is available.

DM
Properties
Size
Properties
Typedark-initials
Size64px
Colors by Type -- Dark Initials

Display-only component. No interaction states. All colors bound to main/avatar/brand/ tokens.

RoleTokenValue
Circle bgmain/avatar/brand/bg#005CE5
Circle bordermain/avatar/brand/border#E5EBF4
Initials textmain/avatar/brand/initials#FFFFFF
Light Initials
DESDEV

Light circle with blue initials text. Neutral variant for non-branded contexts.

LM
Properties
Size
Properties
Typeinitials-light
Size64px
Colors by Type -- Light Initials

Display-only component. No interaction states. All colors bound to main/avatar/default/ tokens.

RoleTokenValue
Circle bgmain/avatar/default/bg#F6F9FD
Circle bordermain/avatar/default/border#E5EBF4
Initials textmain/avatar/default/initials#2340A9
Image
DESDEV

User profile photo in a circle clip. Falls back to placeholder when image fails to load.

Properties
Size
Properties
Typeimage
Size64px
Colors by Type -- Image

Display-only component. Placeholder colors shown when image has not loaded. All colors bound to main/avatar/placeholder/ tokens.

RoleTokenValue
Placeholder bgmain/avatar/placeholder/bg#C2CFE5
Placeholder bordermain/avatar/placeholder/border#E5EBF4
Installation Planned API

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.

Property Mapping

Every row maps a Figma component property to its native equivalent.

Figma PropertySwiftUICompose
type=dark-initials.darkInitialsAvatarType.DarkInitials
type=initials-light.lightInitialsAvatarType.LightInitials
type=image.image(url:)AvatarType.Image(url)
size=20px...90pxsize: AvatarSizesize: AvatarSize
SwiftUI
ios/Components/Avatar/EBAvatar.swift
Jetpack Compose
android/components/avatar/EBAvatar.kt
Usage Snippets Planned API
Dark Initials
// Dark initials
EBAvatar("DM", type: .darkInitials, size: .large)
// Dark initials
EBAvatar(
    initials = "DM",
    type = AvatarType.DarkInitials,
    size = AvatarSize.Large
)
Light Initials
// Light initials
EBAvatar("LM", type: .lightInitials, size: .medium)
// Light initials
EBAvatar(
    initials = "LM",
    type = AvatarType.LightInitials,
    size = AvatarSize.Medium
)
Image
// Image
EBAvatar(imageURL: profileURL, size: .large)
// Image
EBAvatar(
    imageUrl = profileUrl,
    type = AvatarType.Image,
    size = AvatarSize.Large
)
Accessibility
RequirementiOSAndroid
Alt textaccessibilityLabel("User avatar")contentDescription="User avatar"
Decorative modeisAccessibilityElement=false (in lists)importantForAccessibility=no
Image loadingAsyncImage with placeholderSubcomposeAsyncImage with placeholder
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyLayers named container, background, replace here - image. Simple hierarchy. Minor: some sizes have a child also named container.
C2Variant & Property NamingReadyVariant naming resolved (initials-light). Token name typo fixed (main/avatar/brand/initials). Size values use px suffix (minor, no impact on native mapping).
C3Token CoveragePartial8 color tokens, full typography tokens, and radius/radius-round connected. Border-width is fixed per size (by design).
C4Native MappabilityReadyMaps to custom Circle-clipped view on both platforms. No web-only patterns.
C5Interaction State CoverageN/ADisplay-only component. No interactive states needed.
C6Asset & Icon QualityReadyAll initials variants now use vector ELLIPSE layers. Image type rasters are expected (user photos).
C7Code Connect LinkabilityNeeds RefinementUsage descriptions attached. Variant naming now clean. Token typo remains. No CLI mappings.
Code Connect

Maps type and size properties to native parameters. 21 variants (3 types x 7 sizes). Display-only -- no state dimension.

Type variant dimension
FigmaSwiftUICompose
dark-initialstype: .darkInitialsAvatarType.DarkInitials
initials-lighttype: .lightInitialsAvatarType.LightInitials
imageimageURL: URLAvatarType.Image(url)
Size variant dimension
FigmaSwiftUICompose
20pxsize: .xxSmallAvatarSize.XXSmall
24pxsize: .xSmallAvatarSize.XSmall
32pxsize: .smallAvatarSize.Small
40pxsize: .mediumAvatarSize.Medium
48pxsize: .largeAvatarSize.Large
64pxsize: .xLargeAvatarSize.XLarge
90pxsize: .xxLargeAvatarSize.XXLarge
Variants Inventory 21 total

3 type × 7 size=21 variants. No interaction state axis (display-only component).

TypeSizesNotesCount
dark-initials20, 24, 32, 40, 48, 64, 90 pxBrand background, white initials7
initials-light20, 24, 32, 40, 48, 64, 90 pxLight background, dark initials7
image20, 24, 32, 40, 48, 64, 90 pxPhoto fill (raster expected)7
View full Type × Size breakdown (21 rows)
TypeSizeRaster?Node ID
dark-initials20px--17143:4489
dark-initials24px--17143:4497
dark-initials32px--17143:4505
dark-initials40px--17143:4513
dark-initials48px--17143:4523
dark-initials64px--17143:4531
dark-initials90px--17143:4539
initials-light20px--17143:4492
initials-light24px--17143:4500
initials-light32px--17143:4508
initials-light40px--17143:4517
initials-light48px--17143:4526
initials-light64px--17143:4535
initials-light90px--17143:4542
image20pxexpected17143:4495
image24pxexpected17143:4503
image32pxexpected17143:4511
image40pxexpected17143:4521
image48pxexpected17143:4529
image64pxexpected17143:4546
image90pxexpected17143:4548

Raster column: ✗=initials background exported as raster image instead of vector. "expected"=image type uses photos by design.

1.0.0 -- March 2026 Initial
Initial Assessment -- node 17143:4488
Component assessed -- 21 variants documented across type (dark-initials / initials-light / image) x size (20px / 24px / 32px / 40px / 48px / 64px / 90px). Token audit found 8 component-specific color tokens + full typography token set. Documented
Initial
Variant naming resolved -- type=initials - light renamed to type=initials-light across all 7 variants. Now matches dark-initials hyphen style. Fixed
C2 Resolved
Token name typo fixed -- main/avatar/brand/intials corrected to main/avatar/brand/initials. Token now maps correctly to native implementations. Fixed
C2 Resolved
Raster backgrounds replaced -- 5 initials variants (dark-initials 40px/64px, initials-light 40px/64px/90px) now use vector ELLIPSE layers with token-bound fills instead of raster backgrounds. Fixed
C6 Resolved
Border-radius tokenized -- All sizes now use radius/radius-round (99999) instead of hardcoded per-size values (45.213px, 24px, 16px, 12px, 10px). Fixed
C3 Resolved
Code Connect mappings -- Usage descriptions and documentation links attached per variant. No native component files or Code Connect CLI mappings registered yet. Still Open
C7 Open

A 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.

In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Live Preview

Toggle State, Level, and Type to see the badge update in real time.

Properties
State
Level
Type
DS Health
Reusable
Pass
Universal status indicator used across transaction lists, vouchers, dashboards, and notifications. 7 semantic states and 3 emphasis levels cover all common badge use cases.
Self-contained
Pass
Pure display component. Carries its own background, label color, padding, and border-radius per variant. No external dependencies or slots.
Consistent
Partial
Token naming follows DS convention (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.
Composable
Pass
Simple leaf component. Nests cleanly in list rows, card headers, table cells, and notification banners. No child slots or complex nesting.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesState + Level + TypeDisplay-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.

Resolved Issues
  • Hardcoded 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. C3 Fixed
  • State property values renamed to match token semantic names across all 60 affected variants: InfoInformation, SuccessPositive, WarningNotice, DangerNegative, DisabledMuted. Figma State values now align 1:1 with token namespace — cleaner Code Connect mapping, no translation layer needed. C2 Fixed
Open Issues
  • Code Connect mappings not registered. Structural work through v1.1 (state rename, opacity fix) is done — registration can proceed against the 68-variant schema. C7 · Code Connect Linkability
Design Recommendations
  • Add a leading icon slot. Common pattern in fintech status badges (warning triangle, check circle, info icon). Today consumers can't pair an icon with a badge without detaching. Slot
  • Consider Medium / Light levels for Primary and Brand. These states only support Heavy today — if lower-emphasis variants are needed (subtle pill, ghost badge), add them now rather than waiting for a future break. Property
  • Numeric count variant. Notification badges (inbox unread count, cart count) overlap Badge's visual space but aren't formalized here. Either document that consumers use Counter instead, or add a count variant with overflow handling ("99+"). Property
Types

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).

Default
DESDEV

Pill-shaped badge with full border-radius (99px). Standard status indicator for general use.

Properties
State
Level
Properties
TypeDefault
StatePrimary
LevelHeavy
Voucher
DESDEV

Badge with bottom-right radius only (4px). Used on voucher cards and promotional items. Fixed 18px height.

Properties
State
Level
Properties
TypeVoucher
StatePrimary
LevelHeavy
Transaction
DESDEV

Rounded rectangle badge (4px radius). Used in transaction lists and history screens. Compact padding.

Properties
State
Level
Properties
TypeTransaction
StatePrimary
LevelHeavy
Dashboard
DESDEV

Compact rounded rectangle badge (4px radius) with smaller typography (10px). Used in dashboard widgets and summary cards.

Properties
State
Level
Properties
TypeDashboard
StatePrimary
LevelHeavy
Colors by Appearance Mode

Display-only component. All colors bound to main/badge/{semantic}/{level}/ tokens. Primary and Brand states only support Heavy level.

StateLevelRoleTokenValue
PrimaryHeavybgmain/badge/primary/heavy/background#005CE5
PrimaryHeavylabelmain/badge/primary/heavy/label#FFFFFF
BrandHeavybgmain/badge/brand/heavy/background#1972F9
BrandHeavylabelmain/badge/brand/heavy/label#FFFFFF
InfoLightbgmain/badge/information/light/background#E5F1FF
InfoLightlabelmain/badge/information/light/label#005CE5
InfoMediumbgmain/badge/information/medium/background#D2E5FF
InfoMediumlabelmain/badge/information/medium/label#005CE5
InfoHeavybgmain/badge/information/heavy/background#2340A9
InfoHeavylabelmain/badge/information/heavy/label#FFFFFF
SuccessLightbgmain/badge/positive/light/background#E7F8F0
SuccessLightlabelmain/badge/positive/light/label#048570
SuccessMediumbgmain/badge/positive/medium/background#CAF2E0
SuccessMediumlabelmain/badge/positive/medium/label#048570
SuccessHeavybgmain/badge/positive/heavy/background#12AF80
SuccessHeavylabelmain/badge/positive/heavy/label#FFFFFF
WarningLightbgmain/badge/notice/light/background#FCF0CA
WarningLightlabelmain/badge/notice/light/label#966F0B
WarningMediumbgmain/badge/notice/medium/background#F7D96E
WarningMediumlabelmain/badge/notice/medium/label#966F0B
WarningHeavybgmain/badge/notice/heavy/background#CA970C
WarningHeavylabelmain/badge/notice/heavy/label#FFFFFF
DangerLightbgmain/badge/negative/light/background#F8E6E6
DangerLightlabelmain/badge/negative/light/label#B50707
DangerMediumbgmain/badge/negative/medium/background#F4C7C9
DangerMediumlabelmain/badge/negative/medium/label#8D0710
DangerHeavybgmain/badge/negative/heavy/background#D61B2C
DangerHeavylabelmain/badge/negative/heavy/label#FFFFFF
DisabledLightbgmain/badge/muted/light/background#C2C5CA
DisabledLightlabelmain/badge/muted/light/label#FFFFFF
DisabledMediumbgmain/badge/muted/medium/background#9A9FA7
DisabledMediumlabelmain/badge/muted/medium/label#FFFFFF
DisabledHeavybgmain/badge/muted/heavy/background#717883
DisabledHeavylabelmain/badge/muted/heavy/label#FFFFFF
Layout by Type
PropertyDefaultVoucherTransactionDashboard
Heightauto18px (fixed)autoauto
Padding H8px8px4px4px
Padding V2px (top) / 4px (bottom)2px (top) / 4px (bottom)1px (top) / 3px (bottom)1px
Corner radius99px (pill)0/0/4px/0 (BR only)4px4px
Typography
TypeText StyleFontSizeTrackingLine-height
Default / Voucher / TransactionPrimary/Label/FineHeyMeow Rnd Bold12px0.5px12px
DashboardPrimary/Label/TinyHeyMeow Rnd Bold10px0.25px10px
Installation Planned API

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.

Property Mapping

Every row maps a Figma component property to its native equivalent.

Figma PropertySwiftUIComposeNotes
State=Primarystate: .primaryBadgeState.PrimaryHeavy level only
State=Brandstate: .brandBadgeState.BrandHeavy level only
State=Infostate: .infoBadgeState.InfoToken: information
State=Successstate: .successBadgeState.SuccessToken: positive
State=Warningstate: .warningBadgeState.WarningToken: notice
State=Dangerstate: .dangerBadgeState.DangerToken: negative
State=Disabledstate: .disabledBadgeState.DisabledToken: muted
Level=Heavylevel: .heavyBadgeLevel.HeavySolid fill, white label
Level=Mediumlevel: .mediumBadgeLevel.MediumMid-tone fill, dark label
Level=Lightlevel: .lightBadgeLevel.LightSubtle fill, dark label
Type=Defaulttype: .defaultBadgeType.DefaultPill shape (99px radius)
Type=Vouchertype: .voucherBadgeType.VoucherBottom-right radius only
Type=Transactiontype: .transactionBadgeType.TransactionRounded rect (4px)
Type=Dashboardtype: .dashboardBadgeType.DashboardCompact, smaller font
SwiftUI
ios/Components/Badge/EBBadge.swift
Jetpack Compose
android/components/badge/EBBadge.kt
Usage Snippets Planned API
Default (Pill)
// 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
)
Voucher
EBBadge("50% OFF", state: .danger, level: .heavy, type: .voucher)
EBBadge(
    text = "50% OFF",
    state = BadgeState.Danger,
    level = BadgeLevel.Heavy,
    type = BadgeType.Voucher
)
Transaction
EBBadge("Failed", state: .danger, level: .heavy, type: .transaction)
EBBadge(
    text = "Failed",
    state = BadgeState.Danger,
    level = BadgeLevel.Heavy,
    type = BadgeType.Transaction
)
Dashboard
EBBadge("Active", state: .success, level: .light, type: .dashboard)
EBBadge(
    text = "Active",
    state = BadgeState.Success,
    level = BadgeLevel.Light,
    type = BadgeType.Dashboard
)
Accessibility
RequirementiOSAndroid
Accessibility labelaccessibilityLabel("Status: Completed")contentDescription="Status: Completed"
Decorative modeisAccessibilityElement=false (when status is conveyed elsewhere)importantForAccessibility=no
Color contrastHeavy levels meet WCAG AA (4.5:1+)Heavy levels meet WCAG AA (4.5:1+)
Non-color indicatorBadge text conveys meaning alongside colorBadge text conveys meaning alongside color
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySimple single-layer structure: container with text child. Semantic naming.
C2Variant & Property NamingReadyClean enum properties: State, Level, Type. Minor: State names don't match token semantic names.
C3Token CoverageReadyAll colors bound to main/badge/ tokens. Note: 2 variants have hardcoded opacity: 0.90.
C4Native MappabilityReadyMaps to custom EBBadge on both platforms. Simple text + background shape.
C5Interaction State CoverageN/ADisplay-only component. No interactive states needed.
C6Asset & Icon QualityN/ANo icons or assets. Text-only component.
C7Code Connect LinkabilityNeeds RefinementNo CLI mappings registered yet. Property naming is clean and ready for mapping.
Code Connect
AspectStatusNotes
Property namingReadyClean enum properties: State (7), Level (3), Type (4). Ready for Code Connect mapping.
Token coverageReadyAll colors token-bound. Minor opacity inconsistency on 2 variants.
State coverageN/ADisplay-only. No interaction states.
Native component filePendingEBBadge.swift / EBBadge.kt not yet created
Variants Inventory (68 total)

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.

StateLevelTypesNotes
PrimaryHeavyDefault, Voucher, Transaction, Dashboard4 variants
BrandHeavyDefault, Voucher, Transaction, Dashboard4 variants
InfoLight / Medium / HeavyDefault, Voucher, Transaction, Dashboard12 variants
SuccessLight / Medium / HeavyDefault, Voucher, Transaction, Dashboard12 variants
WarningLight / Medium / HeavyDefault, Voucher, Transaction, Dashboard12 variants
DangerLight / Medium / HeavyDefault, Voucher, Transaction, Dashboard12 variants
DisabledLight / Medium / HeavyDefault, Voucher, Transaction, Dashboard12 variants
1.1.0 -- April 2026 Minor
Structural closure -- node 18482:28972
Opacity normalized -- Hardcoded 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. Fixed
C3 Fixed
State values renamed to match token semantics -- 60 variants renamed: Info ->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. Fixed
C2 Fixed
1.0.0 -- April 2026 Initial
Initial Assessment -- node 18482:28972
Component assessed -- 68 variants documented across State (Primary/Brand/Info/Success/Warning/Danger/Disabled) x Level (Heavy/Medium/Light) x Type (Default/Voucher/Transaction/Dashboard). Token audit confirms all colors bound to main/badge/{semantic}/{level}/ tokens. Documented
Initial
Hardcoded opacity on 2 variants -- Danger/Heavy and Disabled/Heavy Transaction variants have opacity: 0.90 on container instead of using token-driven values. Inconsistent with other variants. Fixed in 1.1.0
C3 Fixed
State-to-token name mismatch -- Figma property names (Info, Success, Warning, Danger, Disabled) don't match token semantic names (information, positive, notice, negative, muted). Minor friction for automated Code Connect mapping. Fixed in 1.1.0
C2 Fixed
Code Connect mappings -- No CLI mappings registered yet. Property naming is clean and ready for mapping. Open
C7 Open
Banner RestructureReworkComponent link

A neutral-background promotional banner — leading image or icon alongside a text stack (optional preamble, heading, optional description) and an optional button link. 20 Figma variants across Property (Within A Container / Full Width) × position (left / right) × with link × with button × with preamble × with icon. Shares ~95% of its schema with Carousel - Item — the only meaningful difference is the carousel's peek/snap behaviour, which belongs on the container, not the item.

Restructure — collapse 5 boolean-ish axes into a clean API, add asset/background slots, consolidate with Carousel - Item
Property names with spaces (with link, with button, with preamble, with icon) don't survive codegen. with link + with button describe mutually exclusive CTAs and should be one action enum. with icon is too narrow — a leading asset slot accepting Icon / Avatar / Illustration / Image is more reusable. Property=Within A Container | Full Width is a padding/layout concern owned by the parent. Background image and chevron should be vector slots. Finally, Banner and Carousel - Item share enough DNA to be one component with carousel behaviour on the container.
In Context

Banner is used in-flow as a promotional callout — typically between sections on a Home or Dashboard screen. "Within A Container" leaves horizontal padding on either side so the banner sits as a card; "Full Width" bleeds edge-to-edge. The image or icon sits on the opposite side of the text per the position axis.

Live Preview

Edit the text or flip the property toggles to see how the banner reflows. The image is a flat placeholder in this preview; in Figma it's an instance of a separate "Banner Asset Placeholder" component.

Content
preamble
heading
description
action label
Properties
Property
position
with preamble
with icon
action
DS Health
Reusable
Partial
Banner reads cleanly across promo, info, and up-sell contexts, but the image asset is an instance of a sibling component (not a declared slot) and the icon variant bakes in a grey placeholder — both force product teams to detach or fork to drop in real artwork.
Self-contained
Warn
Card chrome, text tokens, and colors resolve to main/banner/color/*. But the chevron on the button link is a raster shape_full PNG, and the icon placeholder is a drawn circle — neither is self-contained as a vector instance.
Consistent
Fail
Five boolean-ish axes with space-separated names (with link, with button, with preamble, with icon) — no other DS component uses spaces in property names. Property=Within A Container | Full Width is a padding concern named like a semantic mode. with link + with button are mutually exclusive but modeled as independent booleans.
Composable
Partial
Nests cleanly into Home/Dashboard scrollers and maps to iOS HStack / Compose Row. Duplicates ~95% of Carousel - Item's schema — the two should be one component.
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYesProperty × position × with link × with button × with preamble × with iconStatic banner; whole card is the tap target when an action is present.
PressedMissingMissingNot builtTappable banner lacks pressed feedback — needs a subtle scale-down or overlay tint.
FocusedMissingMissingNot builtKeyboard / D-pad focus ring needed when used in an a11y-first flow.
Within A ContainerContainer paddingContainer paddingProperty=Within A Container12 px outer padding + 8 px corner radius around the banner card. Owned by the parent layout on native — not a component variant.
Full WidthEdge-to-edgeEdge-to-edgeProperty=Full WidthNo outer padding, no corner radius. Also owned by the parent layout.
Open Issues
  • Property names use spaces.with link, with button, with preamble, with icon aren't valid identifiers in any native codegen target. Should be camelCase: hasLink, hasAction, hasPreamble, hasLeadingAsset. C2 · Variant & Property Naming
  • Property is a meta-name, not a semantic one. Its values (Within A Container / Full Width) describe outer padding and corner radius — a layout concern the parent should own. Rename to padding if kept, or drop the axis entirely and let the consumer control width + padding. C2 · Variant & Property Naming
  • with link + with button encode mutually exclusive CTAs as independent booleans. Both are text + chevron link styles — the distinction is cosmetic. This schema admits the impossible state with link=yes + with button=yes (excluded by authoring convention, not by the schema). Collapse into one action enum. C2 · Variant & Property Naming
  • Sparse cartesian variant space. 5 boolean-ish axes × 2 container modes would yield 64 combinations; only 20 ship. Author manually pruned invalid combos — that logic should live in the schema (enums, mutually exclusive props), not in variant authoring discipline. C1 · Layer Structure & Naming
  • No first-class image slot. The image is an instance of a separate "Banner Asset Placeholder" component — product teams swap via instance-override rather than a declared Figma Slot. Should be a named asset slot accepting an Image, Illustration, or Gradient. C4 · Native Mappability
  • with icon=yes renders a drawn grey circle. The icon slot is a flat #C2C6CF circle, not a swappable Icon / Avatar / Illustration instance. Can't carry a token-bound color or glyph. C6 · Asset & Icon Quality
  • Chevron is a raster shape_full PNG. The "learn more" arrow ships as an <img> asset. Doesn't scale cleanly and can't take a token tint. C6 · Asset & Icon Quality
  • No pressed / focused / disabled states. The whole banner is tappable but only Default is modeled. Native platforms need pressed (tap feedback) and focused (keyboard/D-pad) at minimum. C5 · Interaction State Coverage
  • Container padding modeled as a component variant.Within A Container adds 12 px outer padding and wraps the inner card in a rounded container; Full Width drops both. On native this is a layout-level decision (parent gives Banner its width + padding), not a Figma variant. C4 · Native Mappability
  • Code Connect mappings not registered. Blocked until properties are renamed, axes are collapsed, and asset/background slots are adopted. C7 · Code Connect Linkability
Design Recommendations
  • Rename space-separated booleans to camelCase.with linkhasLink, with buttonhasAction, with preamblehasPreamble, with iconhasLeadingAsset. Matches the DS-wide naming fix applied to Carousel Item and the Form family. Rename
  • Collapse with link + with button into one action enum.action: .none | .link("Label") | .button("Label"). Mutually exclusive CTAs shouldn't be modeled as independent booleans — the schema should make with link=yes + with button=yes unrepresentable. Property
  • Replace with icon boolean with a leading asset slot. Accept an Icon, Avatar, Illustration, or Image instance. Native: leadingAsset: @ViewBuilder (SwiftUI) / leadingAsset: @Composable () -> Unit (Compose). Eliminates the rigid icon-only placeholder. Slot
  • Add a background / image slot. Replace the "Banner Asset Placeholder" instance with a first-class Figma Slot that accepts an Image, Illustration, or Gradient. Native: background: AnyView / background: @Composable () -> Unit. Slot
  • Rename Property → drop it or make it padding.Within A Container vs Full Width is a padding + radius concern owned by the parent layout on native. Either remove the axis entirely (the banner fills whatever width its parent hands it) or rename to padding: .container | .full. Property
  • Rename positionimagePosition. More specific and self-documenting. Keep as .left | .right enum. Rename
  • Consolidate with Carousel - Item. Both components have preamble / heading / description / button / image-with-position slots. The only difference is peek/snap behaviour — which belongs on the carousel container (scroll snap + scale/opacity transforms), not a sibling component. Target: one EBBanner used standalone or inside EBCarousel. Family
  • Vectorize the chevron. Swap the raster shape_full for a vector Icon instance — crisp at any scale, token-bound tint. Asset
  • Add pressed + focused states. Pressed: 0.98 scale or 6–8% overlay on tap. Focused: 2 px focus ring at border/focus. Banners are always tappable and need both feedback signals. State
  • Announce as a single actionable element. VoiceOver and TalkBack should read preamble + heading + description + action label as one announcement with a button/link role. A11y
Within A Container · position=left · with preamble · with button node 756:82655

The most content-rich variant — preamble + heading + description + button link, with the image on the right and the content column left-aligned. Wraps in a rounded card with 12 px outer padding.

Layout
Outer frame360 × 155 (hug)
Outer padding12 (space/space-12)
Inner card padding16
Corner radius8 (radius/radius-3)
Content column width216 (pl=120 reserved for image)
Content gap2 between lines · 16 before button (space/space-2, space/space-16)
Image asset area360 × 152 absolute (centered, behind content)
Chevron size24 × 24
Colors by State
ROLETOKENDEFAULT
Card bgmain/banner/color/bg#FFFFFF
Preamblemain/banner/color/label-preamble @ 60%#072592
Headingmain/banner/color/label-title#072592
Descriptionmain/banner/color/description#6780A9
Action labelmain/banner/color/label-link#005CE5
Chevron tintmain/banner/color/icon#005CE5
Typography
PreamblePrimary/Label/Fine
· Font / Size / LHProxima Soft Bold · 12 / 12
· Tracking+0.5 (tracking-wider)
HeadingPrimary/Headlines/Block
· Font / Size / LHProxima Soft Bold · 18 / 23
· Tracking+0.25 (tracking-wide)
DescriptionSecondary/Bold/Caption
· Font / Size / LHBarkAda Semibold · 12 / 18
Action labelSecondary/Heavy/Base
· Font / Size / LHBarkAda Bold · 14 / 20
Composed sub-components
ImageInstance of "Banner Asset Placeholder"
Image (proposed)Named asset slot
ChevronRaster shape_full today
Chevron (proposed)Vector Icon instance
Icon (with icon variant)Drawn grey circle today
Icon (proposed)Leading Icon / Avatar slot
Full Width · position=right · with button node 756:82667

Edge-to-edge variant — no outer padding, no corner radius. The banner's own 16 px padding sits directly against the screen edges. Used when the banner is the hero element of the section.

Layout
Width × Height360 × 119 (hug)
Outer padding0 (full-width)
Inner padding16
Corner radius0
Content column width184
Image asset area360 × 152 absolute (centered)
Proposed rename
PropertyProperty=Full Width → padding=none
Or droplet parent control width + padding
RationaleContainer padding is a layout concern, not a component identity axis
Within A Container · with icon · no action node 756:82657

Icon-led variant — replaces the image with a drawn grey circle placeholder. No action CTA. Used when the banner is informational rather than promotional.

Layout
Outer frame360 × 176 (hug)
Icon placeholder19.692 × 19.692 circle (#C2C6CF)
Content column240 (pl=119)
Gap4 between icon and heading
Proposed rename
Propertywith icon → leadingAsset slot
RationaleAccept Icon, Avatar, Illustration, or Image
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBBanner
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
Property: Within A Container | Full Widthpadding: container | none(or drop)Parent layoutParent layout
position: left | rightimagePosition: left | rightimagePosition: .leftimagePosition=EBBannerImagePosition.Left
with preamblepreamble?: Stringpreamble: String?preamble: String?
with link + with buttonaction: .none | .link | .buttonaction: EBBannerAction?action: EBBannerAction?
with iconleadingAsset slotleadingAsset: () -> AnyViewleadingAsset: @Composable (() -> Unit)?
(Banner Asset Placeholder instance)background: Image | Illustration slotbackground: AnyViewbackground: @Composable () -> Unit
(raster chevron)vector IconBuilt into .link/.button actionBuilt into .link/.button action
(not modeled)onTap: () -> VoidonTap: (() -> Void)?onClick: (() -> Unit)?
Suggested file paths
  • ios/Components/Banner/EBBanner.swift
  • ios/Components/Banner/EBBannerAction.swift.none | .link | .button
  • android/components/banner/EBBanner.kt
  • android/components/banner/EBBannerAction.kt
Usage
// Promo banner — image on the right, button link action
EBBanner(
    preamble: "PROMO",
    heading: "New user bonus",
    description: "₱50 cashback on your first transfer.",
    action: .button("Claim"),
    imagePosition: .right,
    background: { Image("promo-hero").resizable() }
) { onClaim() }

// Info banner — leading icon, no action
EBBanner(
    heading: "Verify your account",
    description: "Tap here to complete your KYC.",
    action: .none,
    imagePosition: .left,
    leadingAsset: { Icon(.infoCircle) }
)
// Promo banner — image on the right, button link action
EBBanner(
    preamble = "PROMO",
    heading = "New user bonus",
    description = "₱50 cashback on your first transfer.",
    action = EBBannerAction.Button("Claim") { onClaim() },
    imagePosition = EBBannerImagePosition.Right,
    background = { Image(painterResource(R.drawable.promo_hero), contentDescription = null) }
)

// Info banner — leading icon, no action
EBBanner(
    heading = "Verify your account",
    description = "Tap here to complete your KYC.",
    imagePosition = EBBannerImagePosition.Left,
    leadingAsset = { EBIcon(EBIcons.InfoCircle) }
)
Accessibility
RequirementiOSAndroid
Whole-card tap targetWrap in Button with combined accessibilityLabel (preamble + heading + description + action).Modifier.clickable().semantics(mergeDescendants=true).
Role announcement.accessibilityAddTraits(.isButton) when action is set.Role.Button inside semantics.
Decorative image.accessibilityHidden(true) on the background image view.contentDescription=null on the background Image.
Focus ringDefault SwiftUI focus ring (tvOS + iPadOS keyboard nav).D-pad focus: 2 dp outline at border/focus.
Min touch target360 × (93-176) ≫ 44 pt ✓360 × (93-176) ≫ 48 dp ✓
Usage Guidelines
Do
  • Use Banner for in-flow promo / info callouts — between content sections.
  • Pick imagePosition based on reading flow — LTR locales usually prefer .left image + right-aligned text, or vice versa.
  • Keep the heading to 1-2 lines — 18 pt Bold headline, narrow content column.
  • Use preamble for category context (PROMO, NEW, TIP).
  • Let the parent layout decide outer padding — don't rely on Property=Within A Container.
Don't
  • Don't use both a link and a button — action is single-CTA by design.
  • Don't stack Banner inside a carousel — use Carousel - Item (or, post-consolidation, EBBanner inside EBCarousel).
  • Don't bake text into the image — background is decorative only.
  • Don't use the drawn-circle icon placeholder in production — always pass a real leadingAsset.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRequires ReworkSparse cartesian — 5 boolean-ish axes × 2 container modes yields 64 combos; 20 ship. Container-padding axis conflates layout with identity.
C2Variant & Property NamingRequires ReworkProperty names use spaces (with link, with button, with preamble, with icon). Property is a meta-name. with link + with button encode mutually exclusive CTAs as two booleans.
C3Token CoverageReadyAll colors bind to main/banner/color/*; radii to radius/radius-3; spacing to space/space-*; bg to main/banner/color/bg.
C4Native MappabilityRequires ReworkImage is an instance (not a slot); container-padding axis doesn't map to native; icon-only asset axis is rigid.
C5Interaction State CoverageRequires ReworkOnly Default — no pressed, focused, or disabled for a tappable banner.
C6Asset & Icon QualityRequires ReworkRaster shape_full chevron. Drawn grey circle icon placeholder. Image asset is a separate component instance.
C7Code Connect LinkabilityNot MappedBlocked on property renames, axis collapse, and slot adoption.
Variants Inventory (20 total)

Property (2) × position (2) × with link × with button × with preamble × with icon — if all boolean combinations shipped, 2 × 2 × 2 × 2 × 2 × 2=64 variants. The author pruned invalid combos (with link=yes + with button=yes, with icon=yes + with button=yes, etc.) and shipped 20 variants — 10 per Property mode, mirrored across position=left|right.

Content shapePropertyCountExample nodes (left · right)
heading + desc + buttonWithin A Container2756:82659 · 756:82653
heading + desc + buttonFull Width2756:82669 · 756:82667
heading + desc + icon (no action)Within A Container2756:82657 · 756:82658
heading + desc + icon (no action)Full Width2756:82668 · 756:82672
preamble + heading + desc + buttonWithin A Container2756:82655 · 756:82656
preamble + heading + desc + buttonFull Width2756:82664 · 756:82662
heading + desc + linkWithin A Container2756:82654 · 756:82671
heading + desc + linkFull Width2756:82663 · 756:82670
heading + desc (no action)Within A Container2756:82665 · 756:82666
heading + desc (no action)Full Width2756:82661 · 756:82660
View full variant breakdown (20 rows)
NodePropertypositionwith linkwith buttonwith preamblewith iconDimensions
756:82653Within A Containerrightnoyesnono360 × 143
756:82659Within A Containerleftnoyesnono360 × 143
756:82667Full Widthrightnoyesnono360 × 119
756:82669Full Widthleftnoyesnono360 × 119
756:82658Within A Containerrightnononoyes360 × 176
756:82657Within A Containerleftnononoyes360 × 176
756:82672Full Widthrightnononoyes360 × 152
756:82668Full Widthleftnononoyes360 × 152
756:82656Within A Containerrightnoyesyesno360 × 155
756:82655Within A Containerleftnoyesyesno360 × 155
756:82662Full Widthrightnoyesyesno360 × 131
756:82664Full Widthleftnoyesyesno360 × 131
756:82671Within A Containerrightyesnonono360 × 141
756:82654Within A Containerleftyesnonono360 × 141
756:82670Full Widthrightyesnonono360 × 117
756:82663Full Widthleftyesnonono360 × 117
756:82666Within A Containerrightnononono360 × 117
756:82665Within A Containerleftnononono360 × 117
756:82660Full Widthrightnononono360 × 93
756:82661Full Widthleftnononono360 × 93
1.0.0 — April 2026 Initial
Initial Assessment · node 756:82673
Verdict: Restructure — Rename space-separated booleans, collapse link/button into one action enum, add leading asset + background slots, vectorize chevron, add pressed state, consolidate with Carousel - Item. Open
Family
C1 — Sparse cartesian axes — 5 boolean-ish axes × 2 container modes=64 combos; 20 ship. Invalid combos pruned by convention, not by schema. Open
C1
C2 — Property names with spaceswith link, with button, with preamble, with icon, and meta-named Property. Mutually exclusive CTAs modeled as independent booleans. Open
C2
C4 — Image slot + container-padding axis — Image is a sibling-component instance, not a Figma Slot. Property=Within A Container | Full Width conflates layout with identity. Open
C4
C5 — Missing interaction states — No pressed, focused, or disabled for a tappable banner. Open
C5
C6 — Raster chevron + drawn icon placeholder — Vectorize chevron; swap drawn circle for a leading asset slot. Open
C6
C7 — Code Connect — Blocked on renames, axis collapse, and slot adoption. Open
C7
Bottom Sheet RestructureReworkComponent link

The bottom-anchored sheet surface used to host list pickers, confirmations, forms, filters, and onboarding pages. Currently registered in Figma as Bottom Drawer (2 variants via Alignment=Left Align | Center Align). The actual in-product usage spans dozens of content shapes — ID pickers, transfer confirmations, tips lists, welcome cards, switch-account confirmations — but the DS component only models the top header block plus two hard-baked CTAs; content is 4 decorative placeholder rectangles. This is really a sheet header masquerading as a full sheet.

Restructure — rebuild around a content slot and delegate sheet mechanics to the platform
Three structural problems stack: (1) scope is wrong — the component is the sheet header, not the sheet; (2) the content region is 4 decorative boxes instead of a Figma Slot, so every real usage detaches or adds a new product-local variant; and (3) it overlaps with Modal (18507:71705) and Overlay (47:329691), all three independently declaring what a floating-surface-over-scrim looks like. Proposal: rebuild as EBBottomSheet — a thin wrapper around SwiftUI .sheet / Compose ModalBottomSheet — with explicit dragHandle, header, content, and footer slots, and consume the already-assessed Overlay for the scrim.
In Context

Bottom Sheet anchors to the bottom edge over a dimmed background. In the sticker-sheet context file (12522:109042), instances are used across a wide range of content shapes: ID pickers, confirmation dialogs, transfer summaries, tips lists, welcome cards, and switch-account prompts — each with different inner composition, all wrapped in the same surface.

Live Preview

Toggle between Alignment values and the proposed future controls (detent, drag handle, scrim). The content options below the current Figma axis are previews of the proposed restructure — none exist in Figma today.

Properties (current)
alignment
preamble
description
cta
Proposed (not in Figma yet)
detent
drag handle
scrim
content
DS Health
Reusable
Fail
The component ships only the header block — every real product usage (list picker, confirmation, form, tips list) has to either detach or duplicate the surface. Content region is 4 fixed placeholder rectangles, not a slot.
Self-contained
Warn
Owns its surface bg, header colours, and CTA buttons via token bindings, but redraws the modal-surface concern already owned by the separately-maintained Overlay component. Scrim is not part of the component — consumers must add Overlay by hand every time.
Consistent
Warn
Token namespace is main/bottom-header/color/*, but the component is named Bottom Drawer — neither name matches common usage "Bottom Sheet". The Alignment axis silently changes the shape of the component (Center drops Close X, adds a headerSlot). Two axis values behave like two separate components.
Composable
Fail
Content cannot be swapped without detaching. CTAs are hard-baked Button instances (1 primary + 1 tertiary) — count and pairing are fixed. No way to compose DS Action List, Form fields, or Filter chips inside the sheet as a first-class operation.
Behavior
BehavioriOSAndroidFigma SpecNotes
Present / dismissYesYesNot annotatediOS: .sheet(isPresented:). Android: ModalBottomSheet(onDismissRequest:). Slide-up entrance implied by pattern, not documented on the component.
Drag handle (grabber)YesYesMissingNo handle node in Figma. iOS renders via .presentationDragIndicator(.visible). Material 3 renders via ModalBottomSheet(dragHandle={ BottomSheetDefaults.DragHandle() }).
Detent snapping (medium / large)YesYesMissingSheet height in Figma is driven by content height only — no half / full axis. Natively handled via .presentationDetents([.medium, .large]) / SheetValue.PartiallyExpanded.
Swipe-down-to-dismissYesYesNot annotatedPlatform-native gesture — should be configurable via a dismissible boolean on the wrapper.
Scrim / tap-outside dismissYesYesNot composedScrim lives in the separate Overlay component today (47:329691). Sheet should consume it, not redraw.
Close button (X)YesYesAsymmetricOnly present on Left Align. Raster PNG. Should become a trailing slot in a title-bar region, available to both alignments.
Header slot (e.g. stepper)YesYesCenter Align onlyCenter Align silently adds a headerSlot used for progress bars / steppers. Left Align has no equivalent. Either surface it on both or model as its own region.
Content scroll lockImplicitImplicitNot documentedBackground scroll locked while sheet is presented; sheet's own content scrolls independently when detent < content height.
Empty / loading / error (content)YesYesNot modeledContent slot owner's responsibility; sheet itself has no intrinsic empty/loading/error state.
Open Issues
  • Component scope is the header, not the sheet. "Bottom Drawer" only models the rounded top surface plus a header and hard-baked CTA row. The actual sheet primitives — drag handle, detents, scrim, snap behaviour, swipe-down-to-dismiss — are absent. Every product usage in the context file (12522:109042) has to re-compose the sheet by hand. C1 · Layer Structure & Naming
  • Content region is 4 decorative placeholder rectangles, not a Slot. Inside the body, UI Slot, SLOT 2, SLOT 3, SLOT 4 are pink-dashed #FFECF8 rectangles toggled by booleans showSlot1..4. They are not Figma Slots — designers can't instance-swap in Action List rows, form fields, or Filter chips without detaching. C1 · Layer Structure & Naming
  • Alignment axis hides a second component. Left Align and Center Align are not just text-alignment differences: Left Align has an icon placeholder + title block + Close X on the right; Center Align has a separate headerSlot (used for progress bars / steppers) + title block and no Close X. These are two distinct layouts collapsed into one enum. C2 · Variant & Property Naming
  • No drag handle primitive. Nothing in the Figma tree renders a drag handle ("grabber"). iOS and Android expect this as an explicit visual affordance the user grabs to resize. Either add a handle node bound to a token, or document that rendering is delegated to the platform primitive. C5 · Interaction State Coverage
  • No detent axis. There's no medium / large / fitContent axis on the component. Sheet height is whatever the decorative slots sum to. Native APIs require a discrete detent set; the Figma model doesn't reflect this. C4 · Native Mappability
  • Scope overlap with Modal and Overlay. Bottom Sheet, Modal (18507:71705), and Overlay (47:329691) all independently model "surface above a scrim". None of them compose each other. The scrim should live in Overlay (already assessed); Modal and Bottom Sheet should consume it. C7 · Code Connect Linkability
  • Close X is raster and asymmetric. The close icon is a Figma CDN PNG asset (shape_full) rendered only on Left Align — Center Align has no dismiss affordance at all. Both alignments should offer the same affordance, and it should be a vector Icon instance bound to main/bottom-header/color/icon-close. C6 · Asset & Icon Quality
  • CTAs are hard-baked, not composable. A single primary button + a single tertiary button are instance-swapped inside the component with booleans primaryAction / secondaryAction. Consumers who need one button, two horizontal buttons, three stacked options, or a link-only footer have to detach. CTA should be a footer slot receiving any button composition. C4 · Native Mappability
  • Icon placeholder is a raw grey circle, not a Slot. Left Align's leading icon is a hardcoded #C2C6CF circle inside an icon-placeholder frame. Same anti-pattern as Modal's icon slot. Should be a Figma Slot backed by the Icon component. C1 · Layer Structure & Naming
  • Token namespace and component name disagree. The component is named "Bottom Drawer" while its tokens live in main/bottom-header/color/*. DS literature (Material, HIG) and this report use "Bottom Sheet". Pick one name and propagate: rename the component, rename the token collection, or both. C2 · Variant & Property Naming
  • No Code Connect mapping. Blocked until the restructure lands — mapping the current schema would hardcode the wrong architecture. C7 · Code Connect Linkability
Design Recommendations
  • Restructure around a clean shell with four named slots. Target shape: EBBottomSheet(isPresented, detents, dragHandle, header, content, footer). header=optional title bar region (title, preamble, leading icon, trailing icon / close). content=the one and only body Slot — accepts any DS composition (Action List, form fields, filter chips, tips list). footer=button group pinned to the bottom. Every current product usage becomes a composition: list picker=BottomSheet + Action List; confirmation=BottomSheet + description + button group; form=BottomSheet + Labeled Fields + button group; tips list=BottomSheet + numbered list; welcome card=BottomSheet + illustration + button group. Property
  • Promote the body to a real Figma Slot. Replace the 4 pink-dashed placeholder rectangles with a single named content Slot (Figma's Slot feature). Default to an empty 24-padded frame; let consumers instance-swap in Action List rows, form fields, or any other DS primitive without detaching. Slot
  • Consolidate the Bottom Sheet / Modal / Overlay family. Canonical hierarchy: Overlay (scrim primitive, already shipped) → consumed by both Modal (centered dialog) and Bottom Sheet (bottom-anchored sheet). The three ship distinct anchor positions but share the scrim. Do not collapse Modal and Bottom Sheet into one — native platforms treat them as separate APIs (.sheet vs .alert / Dialog vs ModalBottomSheet). Family
  • Replace the Alignment enum with a proper header schema. Split the silent shape-shift into explicit properties: titleAlignment=left | center (just text-align), leadingSlot (icon / avatar / empty), trailingSlot (close X / icon button / empty), aboveTitleSlot (stepper / progress bar / empty). Both alignments now share the same structural shape, just different text-align and slot content. Property
  • Add an explicit detent axis. Introduce Detent=medium | large | fitContent as a Figma variant — even if visually similar, this makes the Code Connect mapping 1:1 with .presentationDetents([.medium, .large]) / SheetValue.PartiallyExpanded. Designers can then show in mocks which detent a sheet resolves to. Property
  • Add a drag-handle primitive. Vector rect, 32×4, radius 4, bound to a new token main/bottom-header/color/drag-handle (suggest #C2C6CF). Ship as its own tiny component so Modal-style sheets can omit it and Bottom Sheet can include it. Default visible for Bottom Sheet. Asset
  • Footer action group should be a slot, not baked buttons. Replace the primaryAction + secondaryAction booleans with a footer slot that accepts any button composition — 0, 1, 2 horizontal, 2 vertical, link-only, icon+label. Same fix that Modal needs, and both should share a new EBButtonGroup primitive if the team wants to keep the DS tight. Slot
  • Replace the raster close icon with a vector instance. Swap the Figma CDN PNG close for the DS vector Close icon, and bind colour to main/bottom-header/color/icon-close (#6780A9). Available on both alignments via the trailing slot. Asset
  • Convert the leading icon-placeholder into an Icon slot. Same pattern as Modal: add a Figma Slot for the leading icon backed by the Icon component so designers can swap without detaching. Default to a neutral status icon or nothing. Slot
  • Rename the component and its token namespace. Pick one: either rename the component to Bottom Sheet and rename the token collection from main/bottom-header/color/* to main/bottom-sheet/color/*, or keep "Drawer" and align tokens to main/bottom-drawer/*. Current disagreement between component name, token name, and common DS vocabulary costs designers every time they search. Recommended: rename to Bottom Sheet to match Material / HIG / this assessment. Rename
  • Annotate the present / dismiss / gesture contract. Document on the component: slide-up entrance, fade-out-with-scrim exit, swipe-down-to-dismiss, tap-outside-dismiss, ESC/back button behaviour, focus trap, restore-focus-on-close. Developers currently have to infer these from adjacent patterns. Docs
  • Add a dismissible/modal switch. Some flows (transfer confirmation, destructive action) need a non-swipe-dismiss sheet. Surface this as dismissible: bool on the component, mapping to iOS .interactiveDismissDisabled(!dismissible) and Compose sheetState.confirmValueChange. State
Left Align node 12522:12860

Header aligned to the leading edge. Ships with an optional leading icon placeholder, preamble + title stack, a trailing close X, then the decorative body slots and 2 hard-baked CTAs.

Properties
NameBottom Drawer
alignmentLeft Align
iconPlaceholderboolean (default true)
preambleboolean (default true)
headerstring "Title here of the header..."
descriptionboolean (default true)
showSlot1..4booleans — decorative only
primaryActionboolean (default true)
secondaryActionboolean (default true)
Close XHardcoded — raster PNG
Colors by State
ROLETOKENVALUE
Surface bgmain/bottom-header/color/bg#FFFFFF
Preamblemain/bottom-header/color/preamble#90A8D0
Title / Headermain/bottom-header/color/header#0A2757
Descriptionmain/bottom-header/color/description#445C85
Close iconmain/bottom-header/color/icon-close#6780A9
Icon placeholder(hardcoded)#C2C6CF
Primary CTA bgmain/button/primary/brand/enabled/bg#005CE5
Primary CTA labelmain/button/primary/brand/enabled/label#FFFFFF
Tertiary CTA labelmain/button/tertiary/brand/enabled/label#005CE5
Surface, preamble, header, description, and close-icon colours bind to main/bottom-header/color/*. Icon-placeholder grey is hardcoded — should be replaced with an Icon Slot bound to a semantic token.
Layout
Width360
Height (default)324
Corner radius (top)8
Corner radius (bottom)0
Header paddingpt 24 · pb 8 · pl 24 · pr 48
Header gap (icon ↔ title)8
Preamble ↔ title gap6
Content padding24 sides · 32 bottom
Description pb12
CTA group paddingpx 24 · pb 36
CTA gap12
Primary button radius99 (pill)
Close icon size24 · top 24 · right 24
Typography
Preamble stylePrimary/Label/Small
Preamble fontProxima Soft · Bold · 14 / 14
Preamble tracking0.25
Title stylePrimary/Headlines/Section
Title fontProxima Soft · Bold · 22 / 26
Title tracking0
Description styleSecondary/Default/Base
Description fontBarkAda · Medium · 14 / 20
CTA label stylePrimary/Label/Large
CTA fontProxima Soft · Bold · 18 / 18
Header alignmentleft
// Left-aligned header · two CTAs stacked
.sheet(isPresented: $showSheet) {
    EBBottomSheet(
        titleAlignment: .leading,
        leading: { EBAvatar(image) },
        trailing: { EBIconButton(.close) { showSheet = false } },
        preamble: "Preamble here",
        title: "Title here of the header",
        description: "This area is designated for descriptions."
    ) {
        // content slot — any DS composition
        EBActionList { ... }
    } footer: {
        EBButton("Label") { ... }
        EBTextButton("Label") { ... }
    }
    .presentationDetents([.medium, .large])
    .presentationDragIndicator(.visible)
}
// Left-aligned header · two CTAs stacked
if (showSheet) {
    EBBottomSheet(
        titleAlignment = Alignment.Start,
        leading = { EBAvatar(image) },
        trailing = { EBIconButton(Icons.Close) { showSheet = false } },
        preamble = "Preamble here",
        title = "Title here of the header",
        description = "This area is designated for descriptions.",
        detents = listOf(Detent.Medium, Detent.Large),
        onDismissRequest = { showSheet = false },
        footer = {
            EBButton(onClick = {}) { Text("Label") }
            EBTextButton(onClick = {}) { Text("Label") }
        }
    ) {
        EBActionList { ... }
    }
}
Center Align node 12817:43834

Header centered. Silently drops the leading icon + trailing close X and adds an above-title headerSlot used for progress bars / steppers. Same body placeholders and hard-baked CTAs as Left Align.

Properties
NameBottom Drawer
alignmentCenter Align
headerSlotboolean (default true) — above-title
preambleboolean (default true)
headerstring "Title here of the header..."
descriptionboolean (default true)
Leading iconNot present in Center Align
Close XNot present in Center Align
primaryActionboolean (default true)
secondaryActionboolean (default true)
Colors by State
ROLETOKENVALUE
Surface bgmain/bottom-header/color/bg#FFFFFF
Preamblemain/bottom-header/color/preamble#90A8D0
Title / Headermain/bottom-header/color/header#0A2757
Descriptionmain/bottom-header/color/description#445C85
Primary CTA bgmain/button/primary/brand/enabled/bg#005CE5
Primary CTA labelmain/button/primary/brand/enabled/label#FFFFFF
Tertiary CTA labelmain/button/tertiary/brand/enabled/label#005CE5
Center Align shares the same main/bottom-header/color/* bindings as Left Align. No icon-close token surfaced because the close X is absent here — this is the asymmetry called out in Open Issues.
Layout
Width360
Height (default)330
Corner radius (top)8
Header paddingpt 24 · pb 8 · px 24
Header gap (slot ↔ title)16
Preamble ↔ title gap6
Header slot height~16 (progress bar / stepper)
Content padding24 sides · 32 bottom
Description pb12
CTA group paddingpx 24 · pb 36
CTA gap12
Primary button radius99 (pill)
Typography
Preamble stylePrimary/Label/Small
Preamble fontProxima Soft · Bold · 14 / 14
Title stylePrimary/Headlines/Section
Title fontProxima Soft · Bold · 22 / 26
Description styleSecondary/Default/Base
Description fontBarkAda · Medium · 14 / 20
CTA label stylePrimary/Label/Large
CTA fontProxima Soft · Bold · 18 / 18
Header alignmentcenter
// Centered header · above-title progress bar · two CTAs
.sheet(isPresented: $showSheet) {
    EBBottomSheet(
        titleAlignment: .center,
        aboveTitle: { EBProgressBar(step: 2, of: 5) },
        preamble: "Preamble here",
        title: "Title here of the header",
        description: "This area is designated for descriptions."
    ) {
        // content slot
        EBForm { ... }
    } footer: {
        EBButton("Next") { ... }
        EBTextButton("Cancel") { ... }
    }
    .presentationDetents([.large])
    .presentationDragIndicator(.visible)
}
// Centered header · above-title progress bar · two CTAs
if (showSheet) {
    EBBottomSheet(
        titleAlignment = Alignment.Center,
        aboveTitle = { EBProgressBar(step = 2, total = 5) },
        preamble = "Preamble here",
        title = "Title here of the header",
        description = "This area is designated for descriptions.",
        detents = listOf(Detent.Large),
        onDismissRequest = { showSheet = false },
        footer = {
            EBButton(onClick = {}) { Text("Next") }
            EBTextButton(onClick = {}) { Text("Cancel") }
        }
    ) {
        EBForm { ... }
    }
}
Installation Planned API
// Swift Package Manager — requires iOS 16+ for .presentationDetents
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
// build.gradle.kts — uses Material 3 ModalBottomSheet
implementation("com.gcash.designsystem:eb-components:1.0.0")
implementation("androidx.compose.material3:material3:1.2.0")

import com.gcash.designsystem.components.EBBottomSheet
Property Mapping Proposed — post-restructure

The current Figma schema (alignment + 9 booleans) is not fit for 1:1 mapping. The table below maps the proposed post-restructure schema to native APIs.

Proposed Figma propertySwiftUICompose
isPresented.sheet(isPresented: $binding)if (showSheet) ModalBottomSheet(onDismissRequest:)
detents.presentationDetents([.medium, .large])sheetState=rememberModalBottomSheetState(…) + Detent enum
dragHandle=visible|hidden.presentationDragIndicator(.visible / .hidden)dragHandle={ BottomSheetDefaults.DragHandle() } or null
titleAlignment=leading|centertitleAlignment: .leading / .centertitleAlignment=Alignment.Start / Center
leading slotleading: { EBAvatar(…) } (ViewBuilder)leading: @Composable () -> Unit
trailing slot (e.g. close)trailing: { EBIconButton(.close) { dismiss() } }trailing: @Composable () -> Unit
aboveTitle slot (progress / stepper)aboveTitle: { EBProgressBar(…) }aboveTitle: @Composable () -> Unit
preamblepreamble: String?preamble: String?=null
titletitle: Stringtitle: String
descriptiondescription: String?description: String?=null
content slot@ViewBuilder content: () -> Content (trailing closure)content: @Composable ColumnScope.() -> Unit
footer slotfooter: () -> Footerfooter: @Composable RowScope.() -> Unit
dismissible.interactiveDismissDisabled(!dismissible)sheetState.confirmValueChange={ dismissible }
(legacy) alignment=Left Align→ split into titleAlignment + leading + trailing→ split into titleAlignment + leading + trailing
(legacy) showSlot1..4 booleans→ removed, replaced by content slot→ removed, replaced by content slot
(legacy) primaryAction / secondaryAction→ removed, replaced by footer slot→ removed, replaced by footer slot
Suggested file paths
  • ios/Components/BottomSheet/EBBottomSheet.swift
  • ios/Components/BottomSheet/EBBottomSheetModifier.swift (thin wrapper around .sheet)
  • ios/Components/BottomSheet/EBBottomSheetLegacy.swift (iOS 15 fallback via UIViewControllerRepresentable)
  • android/components/bottomsheet/EBBottomSheet.kt
  • android/components/bottomsheet/EBBottomSheetDefaults.kt
Usage Proposed API
// 1 · List picker — BottomSheet + Action List rows in content slot
.sheet(isPresented: $showPicker) {
    EBBottomSheet(
        title: "Provide a valid ID to unlock GCredit",
        trailing: { EBIconButton(.close) { showPicker = false } }
    ) {
        EBActionList {
            ForEach(ids) { id in
                EBActionRow(id.name) { select(id) }
            }
        }
    } footer: {
        EBButton("Next") { advance() }
        EBTextButton("Do it later") { dismiss() }
    }
    .presentationDetents([.large])
}

// 2 · Confirmation dialog — description + button group
.sheet(isPresented: $showConfirm) {
    EBBottomSheet(
        title: "Are you sure?",
        description: "By proceeding, I agree to share my mobile number with GCash.",
        leading: { EBAvatar(recipient.photo) }
    ) { EmptyView() } footer: {
        EBButton("Proceed") { proceed() }
    }
    .presentationDetents([.medium])
}

// 3 · Form — BottomSheet + Labeled Fields in content slot
.sheet(isPresented: $showForm) {
    EBBottomSheet(
        titleAlignment: .center,
        aboveTitle: { EBProgressBar(step: 2, of: 5) },
        title: "Welcome to GInsure!"
    ) {
        EBLabeledField("Full name", text: $name)
        EBLabeledField("Mobile number", text: $mobile)
    } footer: {
        EBButton("Next") { save() }
        EBTextButton("Cancel") { dismiss() }
    }
    .presentationDetents([.large])
}

// 4 · Non-dismissible — destructive confirmation
.sheet(isPresented: $showDelete) {
    EBBottomSheet(
        title: "You're about to switch accounts",
        dismissible: false
    ) { EmptyView() } footer: {
        EBButton("Proceed", role: .destructive) { proceed() }
        EBTextButton("Cancel") { showDelete = false }
    }
    .presentationDetents([.medium])
    .interactiveDismissDisabled(true)
}
// 1 · List picker — BottomSheet + Action List rows in content slot
if (showPicker) {
    EBBottomSheet(
        title = "Provide a valid ID to unlock GCredit",
        trailing = { EBIconButton(Icons.Close) { showPicker = false } },
        detents = listOf(Detent.Large),
        onDismissRequest = { showPicker = false },
        footer = {
            EBButton(onClick = ::advance) { Text("Next") }
            EBTextButton(onClick = ::dismiss) { Text("Do it later") }
        }
    ) {
        EBActionList {
            ids.forEach { id -> EBActionRow(id.name, onClick = { select(id) }) }
        }
    }
}

// 2 · Confirmation dialog — description + button group
EBBottomSheet(
    title = "Are you sure?",
    description = "By proceeding, I agree to share my mobile number with GCash.",
    leading = { EBAvatar(recipient.photo) },
    detents = listOf(Detent.Medium),
    onDismissRequest = {},
    footer = { EBButton(onClick = ::proceed) { Text("Proceed") } }
) { // no content }

// 3 · Non-dismissible — destructive confirmation
EBBottomSheet(
    title = "You're about to switch accounts",
    dismissible = false,
    detents = listOf(Detent.Medium),
    onDismissRequest = {},
    footer = {
        EBButton(onClick = ::proceed, colors = EBButtonDefaults.destructiveColors()) { Text("Proceed") }
        EBTextButton(onClick = { showDelete = false }) { Text("Cancel") }
    }
) { }
Accessibility
RequirementiOSAndroid
Modal trait.sheet applies the modal trait automatically — VoiceOver traps focus inside the sheet.ModalBottomSheet treats content as modal by default — TalkBack swipe is contained.
Focus managementFocus moves to the sheet on present; restores to trigger on dismiss. If a first-field focus is desired, use .focused($firstField).Focus enters sheet content on show; restored to trigger on dismiss. Request initial focus via LaunchedEffect + focusRequester.
Title as headingMark the title Text with .accessibilityAddTraits(.isHeader) so VoiceOver reads it first.Use Modifier.semantics { heading() } on the title; set paneTitle on the sheet surface.
Drag handle announcementiOS's built-in grabber is announced as "Adjustable". Custom handles need .accessibilityLabel("Resize sheet") and .accessibilityAdjustableAction.Material 3 default handle exposes resize action. Custom handles need Modifier.semantics { contentDescription="Resize sheet" }.
Dismiss gestureSwipe-down + ESC + tap-outside all route through isPresented. For non-dismissible, use .interactiveDismissDisabled(true).Back gesture + tap-outside via onDismissRequest. Non-dismissible: sheetState.confirmValueChange={ false }.
Close button (if trailing slot)Wrap 24×24 icon in a ≥44×44pt tappable area. Label: .accessibilityLabel("Close").Wrap 24×24 icon in a ≥48×48dp tappable area. contentDescription=stringResource(R.string.close).
Destructive CTAUse role: .destructive on the footer button.Use EBButtonDefaults.destructiveColors() and explicit contentDescription.
Reduce motionRespect UIAccessibility.isReduceMotionEnabled — skip slide-up / use cross-fade.Respect Settings.Global.ANIMATOR_DURATION_SCALE — shorten animation when accessibility demands it.
Usage Guidelines
Do
  • Use Bottom Sheet for content picks, list selections, filter panels, confirmations with supporting context, and multi-step forms that benefit from an anchored surface.
  • Always pair the sheet with Overlay for the scrim — delegate to the platform if possible (SwiftUI's .sheet provides it; Compose's ModalBottomSheet provides it).
  • Show the drag handle when the sheet supports multiple detents. Hide it when the sheet is a single, fixed-height surface.
  • Put heavy primary action in the footer slot; keep the content slot scrollable for long content.
  • Use titleAlignment: .center with the aboveTitle slot (progress bar / stepper) for onboarding and multi-step flows.
  • Make the sheet dismissible: false only when an explicit user choice is required (destructive actions, legal confirmations).
Don't
  • Don't use Bottom Sheet for quick toasts or brief notifications — use Alert or a snackbar pattern.
  • Don't hardcode content into the sheet variant — use the content slot and compose DS primitives.
  • Don't use Bottom Sheet for full-screen navigation; use a route / screen instead.
  • Don't stack bottom sheets — resolve the current sheet before presenting another.
  • Don't draw your own scrim inside the sheet content; the platform or Overlay primitive owns it.
  • Don't fork a new "Bottom Sheet" variant when a content shape changes — the shell stays the same, content in the slot varies.
  • Don't omit the close affordance when dismissible: false. Every sheet needs at least one way out (footer CTA or trailing close).
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRequires ReworkScope is the header, not the sheet. Content region is 4 decorative placeholder rectangles instead of a Figma Slot. Icon-placeholder is a raw circle. Component name ("Bottom Drawer") disagrees with token namespace ("bottom-header") and DS convention ("Bottom Sheet").
C2Variant & Property NamingRequires ReworkAlignment axis collapses two structurally different layouts (Left has leading icon + close X; Center has above-title headerSlot + no close X). No detent axis. 9 booleans (showSlot1..4, primaryAction, secondaryAction, preamble, description, iconPlaceholder) that should be slots.
C3Token CoverageNeeds RefinementSurface, preamble, header, description, close icon all bound to main/bottom-header/color/*. Icon-placeholder grey (#C2C6CF) is hardcoded. Drag-handle token doesn't exist yet (component has no handle).
C4Native MappabilityRequires ReworkDoes not map to .sheet / ModalBottomSheet as-is. No detent axis, no drag handle, no dismissible contract, hard-baked CTAs. After restructure → clean 1:1 mapping.
C5Interaction State CoverageRequires ReworkOnly default state. No drag states (resting / dragging / snapping), no present / dismiss transition annotation, no empty / loading / error state guidance for the content slot.
C6Asset & Icon QualityNeeds RefinementClose X is a remote raster PNG (shape_full from Figma CDN) rather than a vector Icon instance. Icon-placeholder is a raw #C2C6CF circle, not a vector icon slot.
C7Code Connect LinkabilityNot MappedBlocked on restructure. Scope overlap with Modal and Overlay must be resolved; mapping the current schema would hardcode the wrong architecture.
Code Connect readiness
AspectStatusNotes
Component boundaryRequires ReworkNeeds to consume Overlay and stop redeclaring the surface. Needs to expose a real content slot before mapping.
Property namesRequires ReworkReplace alignment + 9 booleans with titleAlignment + named slots (leading, trailing, aboveTitle, content, footer) + detents.
Token bindingsNeeds RefinementMost colours bound to main/bottom-header/color/*. Hardcoded icon-placeholder grey needs a token. Drag-handle token (new) needed.
Slot architectureRequires ReworkNo Figma Slots in use today. Promote body, footer, leading, trailing, and above-title to first-class Slots.
State coverageRequires ReworkAdd detent axis and dismissible boolean. Drag states are platform-native — document delegation.
Platform API alignmentRequires ReworkTarget is iOS 16+ .sheet + Material 3 ModalBottomSheet. Fallback for iOS 15 via UIViewControllerRepresentable/UISheetPresentationController.
Variants Inventory (2 total)

Single axis — Alignment=Left Align | Center Align. The header shape-shifts across these two values (see Open Issues).

#AlignmentNodeDimensionsHeader slots presentNotes
1Left Align12522:12860360 × 324iconPlaceholder (leading) · preamble · title · Close X (trailing, raster)Title + preamble left-aligned next to optional leading icon. Close X fixed top-right at (24, 24).
2Center Align12817:43834360 × 330headerSlot (above-title, e.g. progress bar) · preamble · titleNo leading icon, no close X. Adds an headerSlot used for progress bars / steppers. Title + preamble centered.

Gaps: No drag handle, no detent axis, no trailing close on Center Align, no leading icon on Center Align, no above-title slot on Left Align. The "two variants" hide at least four separate shape differences.

1.0.0 — April 2026 Initial
Initial Assessment · node 12817:43833
DS Health — 2 variants across 1 axis (alignment). Reusable and Composable both Fail: content is decorative placeholders, CTAs are hard-baked, no Slot architecture. Documented
Baseline
C1 — Component scope — Registered as "Bottom Drawer" but only models the sheet header + CTA area. Actual sheet primitives (drag handle, detents, scrim) absent. Open
C1
C1 — Content region — 4 decorative placeholder rectangles (UI Slot, SLOT 2..4) toggled by booleans instead of a Figma Slot. Open
C1
C2 — Alignment axis — Left vs Center are not just text-alignment; Center adds an above-title headerSlot and drops Close X. Two component shapes collapsed into one enum. Open
C2
C2 — Naming disagreement — Component named "Bottom Drawer", tokens in main/bottom-header/color/*, DS convention is "Bottom Sheet". Recommend rename to Bottom Sheet. Open
C2
C4 — Native mappability — No detent axis, no drag handle, hard-baked CTAs. Does not map to .sheet / ModalBottomSheet until restructure. Open
C4
C5 — Interaction states — No drag states, no empty / loading / error guidance for the content slot, no present / dismiss transition annotation. Open
C5
C6 — Raster close icon — Close X is a Figma CDN PNG (shape_full). Should be a vector Icon instance bound to main/bottom-header/color/icon-close. Open
C6
C7 — Code Connect — Blocked on restructure. Scope overlap with Modal (18507:71705) and Overlay (47:329691) must be resolved first. Open
C7
Family note — Recommended hierarchy: Overlay (scrim, shipped) → consumed by Modal (centered) + Bottom Sheet (bottom-anchored). Do not collapse Modal and Bottom Sheet; native APIs are distinct. Family
Family
Typography note — Description uses BarkAda (secondary font) at Secondary/Default/Base. Covered by the standing custom-font action item. Info
Info

Used 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.

In Context

How the button appears in a real product screen — primary and secondary actions in a bottom sheet.

Button component shown in a GCash Physical Card bottom sheet with primary and secondary buttons
Live Preview

Toggle properties and appearance modes to see the button update in real time.

Properties
Style
State
Size
Icon Placement
Mode
Appearance
DS Health
Reusable
Pass
Three styles (Filled/Outline/Text) with four appearance modes cover primary, secondary, tertiary, surface, and destructive action patterns across all contexts.
Self-contained
Pass
All styles, states, and appearance colors are self-contained via variable bindings. Leading Container and Trailing Container SLOT nodes in every variant.
Consistent
Pass
Clean 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.
Composable
Pass
Style=Filled → Button, Style=Outline → OutlinedButton, Style=Text → TextButton. SLOT nodes support icon+label compositions. Each size has its own text style — clean native mapping.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesState=DefaultAll four appearance modes fully defined.
PressedYesYesState=PressedDarker fill/border using pressed tokens.
DisabledYesYesState=DisabledMuted color tokens applied across all appearances.
DestructiveYesYesAppearance mode: DestructiveRed tokens via variable mode. Applies to all 3 styles (Filled/Outline/Text).
Focused (a11y)N/AN/AMobile-only component. Focus rings rendered natively by iOS (UIKit/SwiftUI) and Android (Material a11y). No Figma state required.
LoadingYesYesNative modifierHandled as an interaction modifier in native code — .ebLoading(true) (SwiftUI) / isLoading=true (Compose). Removed as a Figma state in v4.0.
Icon Only (a11y)YesYesIcon Placement=Icon OnlySquare target matches size height. Requires accessibilityLabel / contentDescription since no visible text.
Resolved Issues
  • Layer renamed from .base/button/smallcontainer on compact disabled container (C1)
  • Icon slots (Leading Container, Trailing Container) added to all variants as Figma SLOT nodes (C2)
  • isError replaced — Destructive is now an appearance variable mode, not a variant property (C2)
  • v2: Outlined and Text Link moved from appearance to Style variant 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: Button variable collection created with 4 appearance modes (Default/Destructive/White/Subtle) — 12 color variables bound to all 60 variants (C3)
  • v3: Old Button Size and button/variant collections removed (C3)
  • v3.1: Loading state added — 12 new State=Loading variants with dot indicators replacing label, disabled appearance colors (C5)
  • v4.0: Icon Placement promoted to component property — replaces leadingIcon/trailingIcon booleans with a single 4-value enum (None/Leading/Trailing/Icon Only). Adds Icon Only square 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-container wrapper layer removed — outermost component now holds fill/radius/auto-layout directly. Layer depth reduced from 4 to 3 (component → container → label/icon). Inner container retained 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 to appearance/stroke/color + new appearance/label/on-surface/color, all 60 Text variants bound to appearance/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/color variable 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 (was Primary/Label/Light/* family). (C3)
Open Issues
  • Code Connect mappings not registered. All structural blockers resolved through v4.1 — registration can now proceed against the current API (Style × Appearance × Size + Icon Placement). C7 · Code Connect Linkability
Design Recommendations
  • Document full-width (stretch) behavior. Add an isFullWidth boolean property for bottom-sheet CTAs and standalone action areas. Today this is achieved via constraints on each screen; a first-class property makes the intent explicit and removes per-screen guesswork. Property
Styles
Filled
DESDEV

Solid background with contrasting label. Primary action style. Colors change via Appearance variable mode.

Properties
State
Size
Icon Placement
Mode
Appearance
Properties
AppearanceDefault
StateDefault
SizeLarge
Colors by Appearance Mode

Token names resolve to different hex values per mode. All 4 modes share the same 4 variables from the Button collection.

ModeRoleEnabledPressedDisabled
Defaultbg#005CE5#2340A9#9BC5FD
label#FFFFFF#FFFFFF#FFFFFF
Destructivebg#D81E1E#B01818#F5A3A3
label#FFFFFF#FFFFFF#FFFFFF
Whitebg#FFFFFF#EEF2F9#F5F7FA
label#005CE5#005CE5#005CE5
Subtlebg#E5F1FF#D2E5FF#EEF5FF
label#005CE5#005CE5#005CE5
Tokens (v4.1 Mode-driven):appearance/container/fillappearance/container/fill-pressedappearance/container/fill-disabledappearance/label/colorappearance/label/color-pressedappearance/label/color-disabled
Outline
DESDEV

Transparent background with border and accent-colored label. Secondary action style.

Properties
State
Size
Icon Placement
Mode
Appearance
Properties
AppearanceDefault
StateDefault
SizeLarge
Colors by Appearance Mode

Outline uses border + label tokens — no background fill. All 4 modes share the same 3 variables from the Button collection.

ModeRoleEnabledPressedDisabled
Defaultborder#005CE5#2340A9#9BC5FD
label#005CE5#2340A9#9BC5FD
Destructiveborder#D81E1E#B01818#F5A3A3
label#D81E1E#B01818#F5A3A3
Whiteborder#005CE5#2340A9#9BC5FD
label#005CE5#2340A9#9BC5FD
Subtleborder#005CE5#2340A9#9BC5FD
label#005CE5#2340A9#9BC5FD
Tokens (v4.1 Mode-driven):appearance/stroke/colorappearance/stroke/color-pressedappearance/stroke/color-disabledappearance/label/on-surface/colorappearance/label/on-surface/color-pressedappearance/label/on-surface/color-disabled
Text
DESDEV

No background or border. Label only. Tertiary action style.

Properties
State
Size
Icon Placement
Mode
Appearance
Properties
AppearanceDefault
StateDefault
SizeLarge
Colors by Appearance Mode

Text style uses label-only tokens — no background or border. All 4 modes share the same 3 variables from the Button collection.

ModeRoleEnabledPressedDisabled
Defaultlabel#005CE5#2340A9#9BC5FD
Destructivelabel#D81E1E#B01818#F5A3A3
Whitelabel#005CE5#2340A9#9BC5FD
Subtlelabel#005CE5#2340A9#9BC5FD
Tokens (v4.1 Mode-driven):appearance/label/on-surface/colorappearance/label/on-surface/color-pressedappearance/label/on-surface/color-disabled
Installation Planned API

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: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.

Property Mapping

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 PropertySwiftUICompose
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…XSmallcontrolSize: .large / .regular / .small / .compact / .minisize=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=LeadingLabel("…", systemImage: "…")leadingIcon={ Icon(…) }
Icon Placement=TrailingLabel + trailing ImagetrailingIcon={ Icon(…) }
Icon Placement=Icon OnlyEBButton(icon: Image(…), accessibilityLabel: "…")EBButton(contentDescription="…") { Icon(…) }
SwiftUI
ios/Components/Button/EBButton.swift
Jetpack Compose
android/components/button/EBButton.kt
Usage Snippets Planned API
Filled — Primary action
// 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")
}
Outline — Secondary action
// 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") }
}
Text — Tertiary action
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")
}
Accessibility
RequirementiOSAndroid
Min touch target44 × 44pt48 × 48dp
Focus ringHandled by UIKit/SwiftUIHandled by Material ripple
Icon-only buttons.accessibilityLabel("Send")contentDescription="Send"
Destructive rolerole: .destructive — announced by VoiceOverUse semantics { role=Role.Button }
Loading state.accessibilityLabel("Loading") + disable tapsemantics { stateDescription="Loading" } + disable click
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyAll layers use semantic names. container, #label, leadingIcon, trailingIcon consistent across all 60 variants.
C2Variant & Property NamingReadyv3 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.
C3Token CoverageReadyAll color values connected to semantic tokens. Layout/sizing driven by button/size variable collection (height, padding-h, padding-v, font-size).
C4Native MappabilityReadyMaps to Button, OutlinedButton, TextButton. Destructive maps to role: .destructive / contentColor=errorColor.
C5Interaction State CoverageReadyDefault, 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.
C6Asset & Icon QualityReadyIcon slots are Figma SLOT nodes accepting vector icon instances. Boolean properties control visibility.
C7Code Connect LinkabilityNeeds RefinementNo CLI mappings registered yet. Property structure is clean and ready for mapping.
Code Connect

Maps v3.2 variant dimensions (Style × Size × State) and variable modes (Appearance) to native parameters. 60 variants × 4 appearance modes=240 visual states.

Style variant dimension
FigmaSwiftUICompose
Filled.ebAppearance(.filled)EBButton {}
Outline.ebAppearance(.outlined)EBOutlinedButton {}
Text.ebAppearance(.textLink)EBTextButton {}
Size variant dimension
FigmaSwiftUICompose
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
State variant dimension
FigmaSwiftUICompose
Default(default)(default)
Pressed(system)(system)
Disabled.disabled(true)enabled=false
(Loading — runtime).ebLoading(true)isLoading=true
Appearance variable mode
FigmaSwiftUICompose
Default(omit — default)(omit — default)
Destructive.ebColorScheme(.destructive)colors=…destructiveColors()
White.ebColorScheme(.white)colors=…whiteColors()
Subtle.ebColorScheme(.subtle)colors=…subtleColors()
Icon Placement component property (v4.0)
FigmaSwiftUICompose
None(text only — default)(text only — default)
LeadingleadingIcon: Image(…)leadingIcon={ Icon(…) }
TrailingtrailingIcon: Image(…)trailingIcon={ Icon(…) }
Icon OnlyEBButton(icon:, accessibilityLabel:)EBButton(contentDescription=…) { Icon(…) }
Variants Inventory (180 total)

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.

StyleSizesStatesIcon PlacementsCount
FilledLarge, Medium, Small, Compact, XSmallDefault, Pressed, DisabledNone, Leading, Trailing, Icon Only60
OutlineLarge, Medium, Small, Compact, XSmallDefault, Pressed, DisabledNone, Leading, Trailing, Icon Only60
TextLarge, Medium, Small, Compact, XSmallDefault, Pressed, DisabledNone, Leading, Trailing, Icon Only60
View full Style × Size breakdown (15 rows)
StyleSizeHeightStates × Icon PlacementsCount
FilledLarge50px3 × 412
FilledMedium48px3 × 412
FilledSmall36px3 × 412
FilledCompact28px3 × 412
FilledXSmall24px3 × 412
OutlineLarge50px3 × 412
OutlineMedium48px3 × 412
OutlineSmall36px3 × 412
OutlineCompact28px3 × 412
OutlineXSmall24px3 × 412
TextLarge50px3 × 412
TextMedium48px3 × 412
TextSmall36px3 × 412
TextCompact28px3 × 412
TextXSmall24px3 × 412
4.1.0 — April 2026 Minor
Mode-driven tokens applied + structure flatten + height refinement · node 17104:184842
Mode-driven appearance tokens applied to all 180 variants — Filled fills bound to 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. Applied
C3 Improved
New appearance/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. Added
C3 Improved
button-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. Restructured
C1 Improved
Large height reduced 56 → 50px — Per design review approval. Matches the visual rhythm of other CTAs in the system. Padding adjusted to maintain proportions. Refined
C3 Refined
Text styles renamedPrimary/Label/Large (was Primary/Label/Light/Base), Primary/Label/Base, Primary/Label/Small, Primary/Label/Fine. Cleaner semantic naming, removes the redundant "Light" prefix. Renamed
C2 Improved
Figma component description added — Documents the Appearance Mode → SwiftUI/Compose API mapping directly in the Figma component description. Surfaces the Mode layer in dev handoff (Dev Mode panel). Will be superseded by Code Connect when C7 is implemented. Documented
C7 Partial
4.0.0 — April 2026 Major
Icon Placement restructure + Appearance Mode documentation · node 17104:184842
Icon Placement promoted to component property — Previously two boolean toggles (leadingIcon, 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). Restructured
C2 Improved
Appearance Mode documented in Figma component description — Appearance (Default/Destructive/White/Subtle) remains a Variable Mode for token reuse but is now explicitly documented in the Figma component description with SwiftUI/Compose API mapping. Addresses the Mode-invisibility handoff gap. Will be superseded by Code Connect when C7 is implemented. Documented
C7 Partial
State property reduced to 3 valuesState now Default/Pressed/Disabled. Loading is handled as an interaction modifier in native code rather than a Figma variant. Simplified
C5 Refined
3.2.0 — March 2026 Minor
Changes Applied via Figma MCP · node 17104:184842
Compact size added — New Size=Compact (28px height) between Small and XSmall. 12 new variants. Total: 60 variants (3 Styles × 5 Sizes × 4 States). Added
C2 Improved
Height tokens bound — All sizes now use space tokens for height: Large=space/space-56, Medium=space/space-48, Small=space/space-36, Compact=space/space-28. XSmall height still derived from padding. Refined
C3 Improved
3.1.0 — March 2026 Minor
Loading State Added via Figma MCP · node 17104:184842
Loading state added as 4th state dimension — 12 new State=Loading variants (3 Styles × 4 Sizes). Dot indicators (● ● ●) replace label text. Uses disabled appearance colors. Tap is disabled during loading. Added
C5 Resolved
Variant count increased from 36 → 48 — 4 states (Default/Pressed/Disabled/Loading) × 4 sizes × 3 styles. 192 visual states across 4 appearance modes. Updated
+12 Variants
2.0.0 — March 2026 Major
Component Restructure via Figma MCP · node 17104:184842
isError 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. Fixed
C2 Resolved
White and Subtle appearances added — 6 new Brand-only variants (3 States each). White for inverse/dark-surface contexts; Subtle for neutral-tinted surface contexts. Added
+6 Variants
Size dimension removed from variant matrix — Compact variants deleted. Size is now driven by the button/size variable collection with 4 modes: Large (52px), Medium (36px), Small (28px), XSmall (24px). Reduces variant count from 36 → 24 while expanding size coverage. Restructured
36 → 24 Variants
button/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. Added
C3 Enhanced
Icon slots upgraded to SLOT nodes with Boolean propertiesleadingIcon and trailingIcon promoted from hidden frames to Figma SLOT nodes. Boolean component properties added for designer toggle control. Upgraded
C6 Enhanced
1.3.0 — March 2026 Re-assessment
Re-assessment · node 17104:184842
isError re-classified as C2 issueisError 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.0
C2 Re-opened
Focus ring removed from C5 scope — Component is mobile-only. Focus rings rendered natively by iOS (UIKit/SwiftUI) and Android (Material a11y). No Figma state required. Clarified
C5 Scope Revised
1.2.0 — March 2026 Minor
Changes Applied via Figma MCP · node 17104:184842
Icon slots added: leadingIcon + trailingIcon — Added to all 30 variants. Hidden by default. Upgraded to SLOT nodes with Boolean properties in v2.0.0. Fixed
C2 Resolved
1.1.0 — March 2026 Minor
Changes Applied via Figma MCP · node 17104:184842
Layer renamed: .base/button/smallcontainer — Resolves C1. Fixed
C1 Resolved
1.0.0 — March 2026 Initial
Initial Assessment · node 17104:184842
Component assessed — 30 variants documented. Documented
Initial
Token audit complete — 24 color + 9 layout tokens confirmed. Verified
C3 Pass
Focus ring and loading state missing — Loading resolved in v3.1.0. Focus ring is N/A for mobile (rendered natively by iOS/Android). Resolved
C5 Pass
Code Connect mappings — No CLI mappings registered. Still Open
C7 Open
Checkbox Component link

A 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.

In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Live Preview

Toggle size and state to see the checkbox update in real time.

Properties
isSelected
State
Size
DS Health
Reusable
Partial
All 5 interaction states and indeterminate defined across 3 sizes (C5 resolved). Checkbox is icon-only by design — CheckboxItem compound component provides label + description pairing.
Self-contained
Pass
Checkmark is a separable 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.
Consistent
Pass
Token naming follows DS convention (main/checkbox/color/...). isSelected uses true/false/indeterminate. All property values follow boolean and enum standards (C2 resolved).
Composable
Partial
Nests in form layouts, list rows, and select-all patterns. Indeterminate state defined. CheckboxItem compound component wraps Checkbox + Label + Description for accessible form groups.
Behavior
StateiOSAndroidFigma PropertyNotes
UncheckedYesYesisSelected=falseBorder-only container. 3 sizes.
CheckedYesYesisSelected=trueBlue fill + separable icon-check layer.
IndeterminateYesYesisSelected=indeterminateBlue fill + icon-indeterminate dash.
DisabledYesYesState=Disabled40% opacity. Checked: #9BC5FD fill.
PressedYesYesState=PressedUnchecked: #EBF2FF bg. Checked: #0F57C8.
FocusedYesYesState=FocusedBlue #1972F9 border stroke.
ErrorYesYesState=ErrorRed border / red #D81E1E fill.
Resolved Issues
  • isSelected=Yes/No renamed to isSelected=true/false in Figma — now maps correctly to Swift Bool and Kotlin BooleanC2 Fixed
  • Checkmark rebuilt as a separable icon-check child 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=indeterminate per size with icon-indeterminate dash layer. 6 → 33 total variants C5 Fixed
Open Issues
  • Code Connect mappings not registered. All structural blockers resolved — registration can now proceed against the 33-variant isSelected × State × Size schema. C7 · Code Connect Linkability
Design Recommendations
  • CheckboxItem compound component created. Composes Checkbox + Label (HeyMeow Rnd Bold) + Description (BarkAda Medium). 4 variants: isSelected (true/false) × Size (Small 14px label / Medium 18px label). Node: 17734:161220. Created
States

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.

Unchecked
DESDEV

Empty container with border stroke. Represents a deselected option.

Properties
isSelectedfalse
State
Size
Checked
DESDEV

Filled container with white checkmark. Represents a selected option. Checkmark is rendered via a separable icon-check child layer.

Properties
isSelectedtrue
State
Size
Colors by State

All interaction states now have defined colors. Token paths follow main/checkbox/color/{state}/{role} convention.

StateRoleTokenDEFAULTPRESSEDDISABLEDERROR
UncheckedBorderunselected/border#D7E0EF#1972F9#D7E0EF#D81E1E
UncheckedContainer bgunselected/bg#EBF2FF
CheckedContainer bgselected/bg#1972F9#0F57C8#9BC5FD#D81E1E
CheckedCheckmarkselected/icon-check#FFFFFF#FFFFFF#FFFFFF#FFFFFF
IndeterminateContainer bgindeterminate/bg#1972F9
IndeterminateDash iconindeterminate/icon#FFFFFF
FocusedBorderfocused/border#1972F9 (all isSelected values)
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
isSelectedisOn: Binding<Bool>checked: Booleantrue/false
isSelected=indeterminatetoggleIndeterminateTriStateCheckboxPartial selection
Size.controlSize()size=EBCheckboxSize.*Small 16px, Medium 20px, Large 24px
State=Disabled.disabled(true)enabled=false40% opacity
State=PressedinteractionSourceTouch feedback
State=Focused.focused()interactionSourceKeyboard/switch nav
State=Error.ebError(true)isError=trueForm validation
SwiftUI
ios/Components/Checkbox/EBCheckbox.swift
Jetpack Compose
android/components/checkbox/EBCheckbox.kt
Usage Snippets Planned API
Unchecked
// 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
// 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
// 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
// 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
// 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
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 x 44 pt48 x 48 dp
Accessibility label.accessibilityLabel("Accept terms")semantics { contentDescription="Accept terms" }
Checked state announcementVoiceOver reads "checked" / "unchecked" automatically via ToggleTalkBack reads state automatically via Checkbox semantics
IndeterminatetoggleIndeterminate 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.

Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyRoot frame named container. Simple, semantic hierarchy. No generic layer names.
C2Variant & Property NamingReadyisSelected now uses true/false — corrected in Figma. Maps directly to Swift Bool / Kotlin Boolean. indeterminate property is a C5 concern (missing state variant).
C3Token CoverageReadyAll 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.
C4Native MappabilityReadyMaps to Toggle(.checkbox) / Checkbox. Indeterminate maps to TriStateCheckbox. Label pairing via CheckboxItem compound component.
C5Interaction State CoverageReadyAll 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.
C6Asset & Icon QualityReadyCheckmark rebuilt as a separable icon-check child vector layer inside each checked container. Can be tinted via selected/icon-check token and swapped natively.
C7Code Connect LinkabilityNeeds RefinementAll structural blockers resolved (C2, C5, C6). Ready for CLI mapping registration. No mappings registered yet.
Code Connect
AspectStatusNotes
Property namingReadyisSelected=true/false — maps directly to Swift Bool and Kotlin Boolean
Icon/asset qualityReadyicon-check is now a named, separable child layer — can be mapped to a native icon slot via Code Connect
State coverageReadyAll interaction states defined — Default, Pressed, Focused, Disabled, Error, plus Indeterminate
Usage descriptionsReadyAll 33 variants have usage descriptions attached in Figma
Native component filePendingEBCheckbox.swift / EBCheckbox.kt not yet created
Variants Inventory (33 total)

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.

isSelectedStatesSizesCount
falseDefault, Pressed, Focused, Disabled, ErrorSmall, Medium, Large15
trueDefault, Pressed, Focused, Disabled, ErrorSmall, Medium, Large15
indeterminateDefault onlySmall, Medium, Large3
View full State × isSelected × Size breakdown (33 rows)
isSelectedStateSizeNode ID
falseDefaultSmall17143:2465
falsePressedSmall17733:968
falseFocusedSmall17733:971
falseDisabledSmall17733:974
falseErrorSmall17733:977
trueDefaultSmall17143:2468
truePressedSmall17733:980
trueFocusedSmall17733:984
trueDisabledSmall17733:988
trueErrorSmall17733:992
falseDefaultMedium17143:2471
falsePressedMedium17733:996
falseFocusedMedium17733:998
falseDisabledMedium17733:1000
falseErrorMedium17733:1002
trueDefaultMedium17143:2473
truePressedMedium17733:1004
trueFocusedMedium17733:1008
trueDisabledMedium17733:1012
trueErrorMedium17733:1016
falseDefaultLarge17143:2476
falsePressedLarge17733:1020
falseFocusedLarge17733:1022
falseDisabledLarge17733:1024
falseErrorLarge17733:1026
trueDefaultLarge17143:2478
truePressedLarge17733:1028
trueFocusedLarge17733:1032
trueDisabledLarge17733:1036
trueErrorLarge17733:1040
indeterminateDefaultSmall17733:1044
indeterminateDefaultMedium17733:1048
indeterminateDefaultLarge17733:1052
1.5.0 — March 2026 Minor
Compound Component + Cleanup · node 17734:161220
CheckboxItem compound component created — 4 variants: isSelected (true/false) × Size (Small/Medium). Each contains a real Checkbox instance + Label (HeyMeow Rnd Bold, 14px/18px) + Description (BarkAda Medium, 12px). Wraps atomic Checkbox with label pairing for accessible form use. Created
New Component
Variant property order reordered — Naming changed from isSelected=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
Cleanup
1.4.0 — March 2026 Minor
C5 Fix · node 17143:2464
All interaction states added — Added 27 new variants covering State (Pressed/Focused/Disabled/Error) × isSelected (true/false) × Size (Small/Medium/Large). Variants 6 → 33. Colors: Pressed uses light blue fill + blue border (unchecked) / #0F57C8 (checked); Focused uses blue border; Disabled uses 40% opacity; Error uses red border / #D81E1E fill. Fixed
C5 Resolved
Indeterminate state added — Added isSelected=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). Fixed
C5 Indeterminate
1.3.0 — March 2026 Minor
C6 Fix · node 17143:2464
Checkmark rebuilt as separable vector layer — Deleted the flattened boolean-op containers from all 3 isSelected=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). Fixed
C6 Resolved
1.2.0 — March 2026 Minor
C2 Fix · node 17143:2464
Boolean property renamed in Figma — All 6 variants renamed from isSelected=Yes/No to isSelected=true/false. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. C2 criterion resolved. Fixed
C2 Resolved
1.1.0 — March 2026 Minor
Assessment Rebuild · node 17143:2464
Full 4-tab assessment built — Overview, Style, Code, and Changelog tabs. Interactive live preview with size and state controls. Spec cards for Unchecked and Checked appearances. Full criteria scorecard and Code Connect readiness table. Updated
Documentation
Property name corrected — Existing assessment incorrectly referenced isChecked. Figma metadata confirms the property is isSelected. All documentation updated. Fixed
C2 Note
1.0.0 — March 2026 Initial
Initial Assessment · node 17143:2464
Component assessed — 6 variants documented across isSelected (Yes/No) x Size (Small 16px / Medium 20px / Large 24px). Token audit found 7 variables defined. Documented
Initial
Flattened checkmark icon — Checked containers had no child layers. The white checkmark was a boolean operation baked into the container frame. Cannot be extracted, tinted, or swapped as a component instance. Hard C6 blocker. Fixed in v1.3.0
C6 Resolved
Missing interaction states — Only checked/unchecked defined. No disabled, pressed, focused, indeterminate, or error state variants. Checkboxes require all of these for production form use. Fixed in v1.4.0
C5 Resolved
Boolean property uses Yes/NoisSelected=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Fixed in v1.2.0
C2 Resolved
Code Connect mappings — Usage descriptions attached per variant. Was blocked by C2 and C6 (both resolved). Pending C5 (missing states) before complete CLI mapping. No CLI mappings registered yet. Open
C7 Open
Chip RestructureNeeds RefinementFilter frame Dropdown frame

A small pill-shaped component used for filters, tags, selected values, and pill-styled dropdown triggers. Currently split across two Figma components — Filter (6 variants) and Filter with Dropdown (2 variants) — that share the same pill anatomy and should be consolidated into a single Chip component with semantic slot props.

Recommend rename + consolidation
Rename "Filter" → Chip (industry term — Material, Polaris, Carbon all use it). Merge Filter + Filter with Dropdown into one component with style (filled/light/outline), leading (none/avatar/icon), and trailing (none/close/chevron) slot props. Fixes the paradigm mismatch, collapses 8 variants into cleaner prop combinations, and aligns with native chip APIs.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. Chips appear in filter rows below title bars, as applied-filter readouts on list screens, and as pill-styled dropdown triggers for sort/filter controls.

TitleFilterSort byTag
Live Preview

Toggle style, leading, and trailing to see every combination.

Properties
style
leading
trailing
DS Health
Reusable
Pass
Fits filter rows, applied-filter readouts, tag lists, and pill-styled sort/filter triggers. Same shape works across all three use cases — that's the argument for consolidation.
Self-contained
Pass
Carries pill radius, height, padding, typography, and color tokens. All 8 variants share the same base anatomy.
Consistent
Warn
Split across two components — "Filter" uses type (primary/light/outline) + with icon (yes/no), "Filter with Dropdown" uses type (default / "with active time"). Different property schemas, yes/no booleans, and inconsistent value naming. C2
Composable
Warn
Leading slot is a hardcoded 24px gray circle (icon-placeholder) not a real Avatar instance or swappable Icon. Close icon is a vector. Breaks compositional inheritance — changes to Avatar won't propagate. C6
Behavior
StateiOSAndroidFigma PropertyNotes
Active / SelectedYesYesFilter · type=primaryFilled blue background, white label. Used when a filter is applied.
Inactive (light)YesYesFilter · type=lightLight gray pill, gray label. Used for unapplied filters or tag readouts.
Inactive (outline)YesYesFilter · type=outlineWhite pill, gray border, gray label. Alternative inactive style.
Dropdown triggerYesYesFilter w/ Dropdown · type=defaultLight style with chevron. Used for sort/filter pickers.
Dropdown with valueYesYesFilter w/ Dropdown · type=with active timeDropdown trigger displaying the selected value in blue (label-link).
Pressed / Disabled / ErrorN/AN/ANot defined in Figma. Engineers must improvise. C5
Open Issues
  • Two components share the same pill anatomy.Filter (6 variants) and Filter with Dropdown (2 variants) should consolidate into a single Chip with style, leading, and trailing slot props. C2 · Variant & Property Naming
  • Boolean values use yes/no instead of true/false. The with icon property is incompatible with Swift Bool / Kotlin Boolean for Code Connect. C2 · Variant & Property Naming
  • Enum value "with active time" contains spaces and unclear naming. Should be withValue / hasSelectedValue, or collapsed into an optional selectedValue: String prop. C2 · Variant & Property Naming
  • Leading slot is a hardcoded placeholder circle. The 24px gray icon-placeholder blocks instance swap — should be a swappable Avatar or Icon. C6 · Asset & Icon Quality
  • No pressed / selected / disabled / error states. State coverage missing — chips are interactive (tap-to-apply, tap-to-remove) and need full state documentation. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the two-component consolidation and prop rename land. C7 · Code Connect Linkability
Design Recommendations
  • Rename "Filter" → "Chip" and merge with "Filter with Dropdown" into one component. Matches industry terminology (Material FilterChip/InputChip, Polaris Tag, Carbon Tag) and lets a single component cover filters, tags, and dropdown triggers. Property
  • Replace type + with icon + (dropdown) type with three semantic slot props:
    style=filled / light / outline
    leading=none / avatar / icon
    trailing=none / close / chevron
    Plus an optional selectedValue string for the "Sort by X" pattern. Property
  • Replace the 24px placeholder circle with a real slot — instance-swap an Avatar (for person filters) or Icon (for category filters). Follows the same pattern Avatar Group uses for its inner Avatar composition. Slot
  • Add pressed / disabled states so applied-filter chips have a documented pressed affordance (important because they're tappable-to-remove) and disabled filters have a defined appearance. State
Styles

Three base styles shared across filter and dropdown use cases. Height is 32px across all variants; padding and gap vary with the leading/trailing slots.

Filled — active filter

Brand blue fill with white label. Represents an active/applied filter. Shown with leading avatar + trailing close.

Light — inactive / default

Light gray fill with gray label. Used for inactive filters, tags, or as the base style for dropdown triggers.

Outline — inactive / subtle

White fill with 2px gray border and gray label. Alternative inactive style for surfaces where the light gray pill wouldn't contrast enough.

Dropdown — light + trailing chevron

Light style with a trailing chevron. Used as a pill-styled dropdown trigger. The "with active time" variant shows a selected value in blue (label-link) — collapsed under the proposed selectedValue optional prop.

Colors by Style

Three style modes, each with bg, label, and icon tokens. Dropdown adds label-link and chevron.

StyleRoleTokenValue
Filledbgmain/filter/color/primary/bg#005CE5
labelmain/filter/color/primary/label#FFFFFF
iconmain/filter/color/primary/icon#F6F9FDB8
Lightbgmain/filter/color/secondary/bg#EEF2F9
labelmain/filter/color/secondary/label#6780A9
iconmain/filter/color/secondary/icon#7E96BE
selected valuemain/filter/color/secondary/label-link#005CE5
chevronmain/filter/color/secondary/chevron#005CE5
Outlinebordermain/filter/color/tertiary/border#D7E0EF
labelmain/filter/color/tertiary/label#6780A9
iconmain/filter/color/tertiary/icon#7E96BE
Layout
PropertyTokenValue
Height32px
Corner radiusradius/radius-pill99px
Padding (with leading)4L / 14R
Padding (no leading)14 horizontal
Padding (dropdown)space/space-16 · space/space-1216L / 12R
Outline border width2px
Leading avatar size24 × 24 (should be Avatar instance)
Close icon size16 × 16
Chevron size24 × 24
Gap: icon → labelspace/space-44px
Gap: label → closespace/space-88px
Gap: label → selected value (dropdown)space/space-88px
Typography
PropertyValue
DS text stylePrimary/Label/Base
FontHeyMeow Rnd
Weight700 (Bold)
Size16px
Line height16px
Tracking+0.25
Installation Planned API

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:chip:1.0.0")
}
Property Mapping (proposed — after rename + consolidation)
Proposed Figma PropertySwiftUI ParamCompose ParamNotes
style.ebStyle(.filled / .light / .outline)style=EBChipStyle.Filled / Light / OutlineReplaces Filter's type property
leadingleading: EBChipLeading?leading: @Composable (() -> Unit)?.none / .avatar(...) / .icon(...)
trailingtrailing: EBChipTrailing?trailing: EBChipTrailing?.none / .close / .chevron
selectedValueselectedValue: String?selectedValue: String?Optional selected-value text (for dropdown "Sort by X" pattern)
labeltitle: Stringlabel: StringMain label text
onTap / onCloseaction / onRemoveonClick / onRemoveTwo callbacks for applied-filter chips (tap body vs. tap close)
SwiftUI
ios/Components/Chip/EBChip.swift
Jetpack Compose
android/components/chip/EBChip.kt
Usage Snippets Planned API
// Applied filter — brand fill, avatar + close
EBChip("Filter Name",
    leading: .avatar(EBAvatar(initials: "DM")),
    trailing: .close,
    onRemove: { /* remove filter */ })
.ebStyle(.filled)

// Inactive filter — light, label only
EBChip("Category")
    .ebStyle(.light)

// Dropdown trigger with selected value
EBChip("Sort by",
    selectedValue: "Conservative first",
    trailing: .chevron,
    action: { /* open dropdown */ })
.ebStyle(.light)
// Applied filter — brand fill, avatar + close
EBChip(
    label = "Filter Name",
    style = EBChipStyle.Filled,
    leading = { EBAvatar(initials = "DM") },
    trailing = EBChipTrailing.Close,
    onRemove = { /* remove filter */ }
)

// Inactive filter — light, label only
EBChip(
    label = "Category",
    style = EBChipStyle.Light
)

// Dropdown trigger with selected value
EBChip(
    label = "Sort by",
    selectedValue = "Conservative first",
    style = EBChipStyle.Light,
    trailing = EBChipTrailing.Chevron,
    onClick = { /* open dropdown */ }
)
Accessibility
RequirementiOSAndroid
Tap target32px height is below HIG 44pt — wrap in a 44pt-tall hit area32dp height is below Material 48dp — expand touch target via Modifier.minimumInteractiveComponentSize()
Role.accessibilityAddTraits(.isButton)Use semantics { role=Role.Button }
Close button label.accessibilityLabel("Remove filter: \(label)")contentDescription="Remove filter: $label"
Dropdown indicator.accessibilityHint("Double-tap to change")Announce chevron via role + state
Selected state.accessibilityAddTraits(.isSelected) for style=filledselected=true in semantics for active filters
Usage Guidelines

Do

Use style=filled for applied/active filters, style=light for inactive filters and dropdown triggers, style=outline when the surface beneath is already light gray.

Don't

Mix filled and light chips in the same filter row without intent — it signals "one filter is selected," so using both styles for unrelated reasons misleads the user.

Do

Pair the close affordance with applied filters so users can remove them with a single tap.

Don't

Add a close icon to dropdown-trigger chips — the chevron already indicates the tap behavior; a close icon implies removal instead of picking.

Do

Keep the chip label to one or two words. Use selectedValue to show the picked option separately.

Don't

Stretch chips to fill width — they're meant to fit their content. If a control needs full width, use a Button or Select Field instead.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: container, Placeholder, Close, Chevron Down.
C2Variant & Property NamingNeeds FixSplit across two components with mismatched property schemas. with icon uses yes/no. Dropdown uses type="with active time". Rename to Chip and use style / leading / trailing.
C3Token CoverageReadyAll colors, radii, spacing, and typography bound to tokens.
C4Native MappabilityReadyMaps to custom pill on iOS and FilterChip/InputChip on Android. Single EBChip composable works once consolidated.
C5Interaction State CoverageNeeds FixNo pressed / disabled / error states. Selected state is implied by style=filled but not defined as a separate variant.
C6Asset & Icon QualityNeeds FixLeading slot is a hardcoded 24px gray circle (icon-placeholder) — should be a swappable Avatar/Icon instance.
C7Code Connect LinkabilityPendingBlocked by C2 consolidation. Clean prop names land once Chip is renamed and merged.
Variants Inventory (8 total across 2 Figma components)
SourceStyleLeadingTrailingNode ID
FilterFilled (primary)AvatarClose18336:22244
FilterFilled (primary)18336:22253
FilterLightAvatarClose18336:22257
FilterLight18336:22266
FilterOutlineAvatarClose18336:22270
FilterOutline18336:22279
Filter w/ DropdownLightChevron18336:22292
Filter w/ DropdownLight (w/ selected value)Chevron18336:22284

After rename + consolidation, these 8 variants collapse into 3 style × 3 leading × 3 trailing=27 prop combinations on one Chip component (most unused; actual DS palette is ~8 common combinations).

1.0.0 — April 2026 Initial
Initial Assessment · nodes 18336:22243 + 18336:22283
Assessed as Chip — Two Figma components ("Filter" with 6 variants, "Filter with Dropdown" with 2 variants) share the same pill anatomy. Recommended rename + consolidation into a single Chip component with semantic slot props. Documented
Initial
Two-component split, mismatched schemas — Filter uses type + with icon. Dropdown uses type="with active time". Booleans are yes/no. Should consolidate to style / leading / trailing + optional selectedValue. Open
C2 Open
No pressed/disabled/error states — Engineers must improvise these affordances. Open
C5 Open
Leading slot is hardcoded placeholder — 24px gray circle instead of a swappable Avatar/Icon instance. Open
C6 Open
Code Connect mappings — Blocked by C2 rename + consolidation. Clean prop names land once Chip is merged. Open
C7 Open
Counter FixRefineComponent link

A small numeric pill used as trailing content in list rows, tab items, and section headers. Two formats: single integer for counts (notifications, unread, pending items) and N / M slash for progress-against-capacity (slots used, steps completed). Fixed 24 px tall, hugs digit width. 4 variants across state (empty / filled) × with limit (yes / no).

Fix — parameterize values and clean boolean naming
Both formats (single-integer + slash) belong. The fixes are: rename with limithasLimit with true/false; parameterize count: Int and limit: Int?; add 99+ overflow handling. Variant count stays at 4.
In Context

Counter appears inline with text to show counts — section headers for unread notifications, tab item badges for pending items, limit/slot usage displays.

Live Preview

Flip with limit to switch between the two formats. Type count (and limit when applicable) to see the pill hug digit width. Try 247 with maxDisplay=99 to see the overflow collapse to 99+. state auto-derives from count: 0 is muted empty, anything else is brand-blue filled.

Content
count
limit
maxDisplay
Properties
with limit
state
DS Health
Reusable
Pass
Generic count primitive — used across Section Header, Tab Item, and standalone notification contexts.
Self-contained
Pass
Owns its typography, color tokens, and radius. Nothing external required to render.
Consistent
Partial
with limit uses yes/no strings instead of true/false. Count/limit values are hardcoded text — not usable for real counts without detaching.
Composable
Pass
Hugs content width, drops into any inline layout (Section Header, Tab Item) without manual sizing.
Behavior
StateiOSAndroidFigma SpecNotes
EmptyYesYesstate=emptyCount is 0. Muted label, same bg. Used to indicate "nothing pending".
FilledYesYesstate=filledCount is greater than 0. Brand-blue label, same bg. Used when there's activity to surface.
With limitYesYeswith limit=yesRenders "N / M" (e.g. "3 / 10") — for slot/limit displays like "beneficiaries used".
Without limitYesYeswith limit=noRenders a single integer. Used for unread counts, inbox badges.
Pressed / DisabledN/AN/ACounter is display-only — no interactive states.
Overflow (99+)MissingMissingNot modeledReal counts can exceed 99 (unread messages, notifications). Need overflow display ("99+") — not built today.
Open Issues
  • with limit uses yes/no strings. Should be hasLimit: true/false for direct Swift Bool / Kotlin Boolean mapping. C2 · Variant & Property Naming
  • Count and limit values are hardcoded text. "0 / 10" and "10 / 10" are baked into each variant — consumers must detach to show any other value. Expose count: Int and limit: Int? as parameters so the component renders its own formatted string. C2 · Variant & Property Naming
  • No overflow handling for large counts. Inbox counts routinely exceed 99 (unread messages, notifications). The single-integer format needs "99+" display; the slash format needs equivalent overflow when count exceeds the limit. Not modeled today. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Trivial once parameterization and boolean rename land. C7 · Code Connect Linkability
Design Recommendations
  • Rename with limit to hasLimit. Change values from yes/no strings to true/false. Aligns with the boolean naming convention used across the DS. Rename
  • Expose count and limit as parameters.count: Int (required) + limit: Int? (optional — activates slash format when set). Drop the text overrides; the component formats the string itself. Eliminates the "detach to change the number" anti-pattern. Property
  • Derive state from count. Empty when count==0, filled otherwise. Removes one property the consumer has to set manually. Allow explicit override for edge cases. Property
  • Add overflow handling for both formats. Single-integer: count > maxDisplay renders "99+". Slash format: count > limit should clamp display or render "limit+" to prevent visual overflow. Pattern used in Material / Apple badges. State
  • Document the two use cases. Single-integer=how many of X are there (notifications, unread, pending). Slash format=progress against capacity (slots used, steps completed). Clarify in the spec so teams pick the right format. Docs
  • Document Counter ↔ Badge relationship. Counter=numeric (count, progress). Badge=status/tag label (Success, Premium). Teams reach for the wrong one without this guidance. Docs
Empty — with limit node 18482:71322

Slash format showing zero progress against a limit ("0 / 10"). Muted label on neutral bg. Used when no slots are filled yet.

Properties
stateempty
with limityes
Example text0 / 10
Colors by State
ROLETOKENVALUE
Surfaceempty/bg#EEF2F9
Labelempty/label#C2CFE5
Layout
Height24
Padding0 × 8 (hug width)
Corner radius99 (pill)
Example width53 (for "0 / 10")
Typography
StylePrimary/Label/Small
FontHeyMeow Rnd Bold
Size / line-height14 / 14
Letter-spacing+0.25
Alignmentcenter
Filled — with limit node 18482:71324

Slash format with a filled count ("10 / 10"). Brand-blue label on neutral bg. Used when capacity is at or approaching the limit.

Properties
statefilled
with limityes
Example text10 / 10
Colors
ROLETOKENVALUE
Surfacefilled/bg#EEF2F9
Labelfilled/label#072592
Single integer nodes 18482:71326, 18482:71328

Standalone count — notifications, unread messages, pending items. Hugs tightly around the digit (24 × 24 for single digit, grows for 2+ digits). Empty state shown muted; filled state shown in brand-blue. Pairs with overflow handling ("99+") once count is parameterized.

Properties
with limitno
stateempty | filled
Example text0
Layout
Min-width24 (circle for single digit)
Max-widthhug (grows with digit count)
Padding0 × 8
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBCounter
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
(hardcoded text "0 / 10", "10 / 10")count: Intcount: Intcount: Int
with limit: yes | nolimit: Int? (nil=single-integer format; set=slash format)limit: Int?limit: Int?
state: empty | filledderived from count (0=empty, >0=filled)auto, with overrideauto, with override
(not modeled)maxDisplay: Int=99maxDisplay: Int=99maxDisplay: Int=99
Suggested file paths
  • ios/Components/Counter/EBCounter.swift
  • android/components/counter/EBCounter.kt
Usage
// Single integer — notification count
EBCounter(count: unreadCount)

// Overflow — renders "99+" when count > 99
EBCounter(count: 247)

// Slash format — progress against capacity
EBCounter(count: beneficiaries.count, limit: 10)  // "3 / 10"

// Trailing counter inside a list row
EBListRow(
    leading: EBAvatar(initials: "JD"),
    label: "Messages",
    trailing: .counter(unreadCount)
)
// Single integer — notification count
EBCounter(count = unreadCount)

// Overflow — renders "99+" when count > 99
EBCounter(count = 247)

// Slash format — progress against capacity
EBCounter(count = beneficiaries.size, limit = 10)  // "3 / 10"

// Trailing counter inside a list row
EBListRow(
    leading = { EBAvatar(initials = "JD") },
    label = "Messages",
    trailing = { EBCounter(count = unreadCount) }
)
Accessibility
RequirementiOSAndroid
Context-aware labelSet .accessibilityLabel("5 unread messages") — screen readers should hear what the number means, not just the digits.Set contentDescription="5 unread messages".
Zero stateWhen count==0, default behavior (hideWhenZero: true) removes the pill from the accessibility tree entirely. Best practice — nothing to announce.Same — hidden at zero by default.
OverflowAnnounce the actual count, not "99+" — e.g. "247 unread". The "99+" is a visual truncation, not the truth.Same — screen reader gets the real number.
ContrastFilled: #072592 on #EEF2F9=11.8:1 ✓. Empty: #C2CFE5 on #EEF2F9=1.4:1 — fails AA. Empty is decorative (shown only when the user opts out of hideWhenZero); don't use for counts that must be read.Same ratios apply.
Usage Guidelines
Do
  • Use for numeric counts — unread messages, pending items, slots used.
  • Pair with a screen-reader-friendly label ("3 of 10 beneficiaries", not "3 / 10").
  • Hide the counter when count==0 if the zero state is the norm (prevents muted pills cluttering the UI).
  • Prefer Counter + Section Header / Tab Item compositions over standalone use.
Don't
  • Don't use Counter for status labels ("New", "Draft") — use Badge.
  • Don't put non-numeric text inside — this isn't a generic chip.
  • Don't attach onClick / onPress — Counter is a display primitive. Wrap the parent in an interactive container if needed.
  • Don't hardcode the count/limit text by detaching — use the (proposed) count / limit props.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyClean one-layer structure (container + label).
C2Variant & Property NamingReworkwith limithasLimit (bool). Parameterize count/limit.
C3Token CoverageReadySurface + label bound to main/counter/color/*.
C4Native MappabilityReadyMaps to a tiny EBCounter view/composable — Text inside a Capsule.
C5Interaction State CoverageNeeds RefinementDisplay-only — no interactive states needed. But overflow ("99+") for large counts is missing.
C6Asset & Icon QualityN/ANo assets.
C7Code Connect LinkabilityNot MappedTrivial once parameterization + boolean rename land.
Variants Inventory (4 total)

state (2) × with limit (2)=4 variants. Both formats are kept — they solve different problems: single-integer for counts, slash for progress.

#Nodestatewith limitFormatExampleDimensions
118482:71322emptyyesslash0 / 1053 × 24
218482:71324filledyesslash10 / 1059 × 24
318482:71326emptynosingle integer025 × 24
418482:71328fillednosingle integer024 × 24
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:71321
Verdict: Fix — Keep both formats (single integer + slash). Rename with limithasLimit, parameterize count + limit, add 99+ overflow. Variant count stays at 4. Open
Schema
C2 — Boolean namingwith limit: yes/nohasLimit: true/false. Direct Swift Bool / Kotlin Boolean mapping. Open
C2
C2 — Parameterize values — Expose count: Int + limit: Int?; drop hardcoded text. Derive state from count. Open
C2
C5 — Overflow — Add maxDisplay (default 99); counts beyond render "99+" in single-integer format, and clamp in slash format. Open
C5
C7 — Code Connect — Trivial once parameterization + rename land. Open
C7
Date Picker - Group ConsolidateRequires ReworkComponent link

The popover/panel surface that appears when the Date Picker trigger is tapped. 3 variants on a single Type axis — Date (a 6-row × 7-col calendar grid of day cells), Year (a 3-col scrollable grid of year cells), Month (a 3-col grid of month cells). All variants share a 360×296 frame, header with Prev/Next chevrons, white bg, 1px border, 12px-blur drop shadow. Sibling to the Date Picker trigger.

Consolidate into native pickers — don't redraw
Both iOS (DatePicker(.graphical)) and Material 3 (DatePicker/DatePickerDialog) render the calendar surface with full locale, keyboard, and a11y support built in. The DS should ship a tokenized wrapper, not a from-scratch Figma redraw. Current component has raster chevrons (C6), no cell state coverage (C5), day-of-week layer names (C1), misleading Type axis (C2), a fake drawn scrollbar (C4), and asymmetric Month navigation (C4).
In Context

The group appears immediately below the Date Picker trigger when it enters State=Active. The three Type variants swap when the user taps the header (Date → Year → Month).

SuMTWThFSa
Live Preview

Switch Type to compare the Date grid, Year grid, and Month grid. All three share the same 360×296 card frame and header structure.

Properties
Type
DS Health
Reusable
Warn
The surface is shared across 3 variants but cannot be reused outside the Date Picker family — no slot, no header customization, no way to change grid dimensions. Re-implementing the same card pattern for any other calendar-like grid (e.g. event picker) means rebuilding it from scratch.
Self-contained
Warn
Carries bg, border, radius, shadow, and header tokens — but chevron glyphs are raster images referenced by URL, not vector icon instances. The Year variant also overlays a fake drawn Scrollbar rectangle instead of using a scrollable container.
Consistent
Warn
Type=Date | Year | Month is misleading — "Date" means day-grid view. Native convention is mode: day | month | year. Month variant has only a Next chevron (no Prev), asymmetric from Date and Year. Day cells use day-of-week layer names (Sunday...Saturday) as semantic roles.
Composable
Fail
Not composable into a native picker. Both SwiftUI DatePicker and Compose DatePickerDialog render their own calendar surface internally — this Figma component has no 1:1 native primitive. It also is currently nested inline inside the trigger (see Date Picker), not exposed as a standalone overlay.
Behavior
StateiOSAndroidFigma PropertyNotes
Day gridYesYesType=Date6 rows × 7 cols. Weekday header (Su/M/T/W/Th/F/Sa). Today shown with 1.5px blue ring. Prev/next month days dimmed.
Year gridYesYesType=Year3-col grid with overflow-clip and a drawn "Scrollbar" decoration. Selected year shown with 1px blue ring + blue label.
Month gridYesYesType=Month3-col grid with all 12 months. Missing Prev chevron — only Next is drawn. Selected month shown with 1px blue ring.
Cell: Today / SelectedYesYesDate Picker - Item state1.5px blue ring on day cells; 1px blue ring on year/month cells. Native pickers fill the cell with tint instead of ringing it.
Cell: Pressed / Hover / FocusNoNoMissing. Native pickers provide these automatically; a DS wrapper only needs to tint them.
In-range / Range start / Range endNoNoNot defined. Both platforms support date-range selection; the DS has no tokens or visuals for it.
Disabled cellNoNoNo business-rule disabled state (e.g. minDate/maxDate). Only the "prev/next month" greyed variant exists.
Keyboard navigation / focus ringNoNoNo focus styling. Native pickers handle keyboard+TalkBack/VoiceOver by default; the DS needs to preserve that.
Open Issues
  • Day cells are named by weekday, not by index or role. The day rows contain layers literally named Sunday, Monday... Saturday instead of day-cell[0]...day-cell[6] or a single repeated instance. Weekday is runtime data, not a layer identity. This leaks through to Dev Mode handoff as nonsense layer names. C1 · Layer Structure & Naming
  • Weekday header rows are instance-swapped Date Picker - Item, not a dedicated weekday cell. The header row uses the same Date Picker - Item component as the day grid but with a .base/date-item inner naming. Handoff can't distinguish "day label" from "weekday label" from property alone. C1 · Layer Structure & Naming
  • Type=Date | Year | Month is misleading. "Date" means the day grid, not a date value. Native convention and consumer mental model is mode: day | month | year. Rename the axis so code-connect params read mode=.day instead of type="Date". C2 · Variant & Property Naming
  • No 1:1 native primitive for the whole calendar surface. SwiftUI DatePicker(selection:).datePickerStyle(.graphical) and Material 3 DatePicker(state:) both render the full calendar internally. Keeping this as a drawn Figma component means the developer either ignores it (using native) or redraws it from scratch (defeating the purpose of a DS). C4 · Native Mappability
  • Year variant has a drawn fake Scrollbar rectangle. Node 18414:6277 is a Scrollbar layer with a 4px × 80px blue-tinted pill absolutely positioned over the grid. Scroll state can't be represented in static geometry — native pickers render the scroll indicator automatically. Remove, and use a scrollable container instead. C4 · Native Mappability
  • Month variant is missing the Prev chevron. Node 18431:2826's header has only a Chevron Right — the left slot is an empty 24×24 box (Chevron Left removed). Date and Year variants both have bidirectional navigation; Month is asymmetric. C4 · Native Mappability
  • Cell interaction states not defined on the grid. Only Default, Today (day), Selected (year/month), and Prev/Next (day-only, disabled-looking) exist. Missing: Pressed, Hover/Focus, In-range, Range-start, Range-end, Business-rule disabled (outside minDate/maxDate). C5 · Interaction State Coverage
  • Chevrons are raster shape_full assets. Both Prev and Next chevrons in every variant reference https://www.figma.com/api/mcp/asset/... image URLs rather than a vector icon instance. Won't scale, won't inherit token colors, can't be swapped. Reuse the DS chevron icon component. C6 · Asset & Icon Quality
  • No dedicated picker surface token scope. The panel bg and border reuse main/date-picker/month-header/color/bg / ...border as a stand-in — but the month-header is a sub-region, not the surface. Without a distinct main/date-picker/group/* scope, changing one visually mutates the other. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked by C1/C2/C4/C5/C6 and by the recommendation to wrap native pickers instead of mapping this drawn surface directly. C7 · Code Connect Linkability
Design Recommendations
  • Composition — Wrap the native date pickers, don't redraw them. iOS: DatePicker(selection:, displayedComponents:).datePickerStyle(.graphical) renders the calendar surface with full locale, leap-year, keyboard, VoiceOver, and Dynamic Type support. Material 3: DatePicker(state:) + DatePickerDialog does the same. The DS should ship EBDatePickerPanel as a token-styled overlay of the native picker — not a from-scratch Figma surface. Documentation lives as a reference spec here; production code calls the native API. Composition
  • Family — Unify Date Picker - Item and Month/Year Picker - Item into one Picker Cell. Day, month, and year cells inside this Group are all selectable cells with the same state semantics. Replace the two sibling components with one Picker Cell with kind: day | month | year and state: default | today | selected | in-range | disabled. Collapses 10 variants across 2 components into one component with two clean axes. Family
  • Property — Rename Type to mode and use native terminology. Change Type=Date | Year | Month to mode=day | month | year. Matches SwiftUI displayedComponents and Compose DisplayMode. Eliminates the ambiguity of "Date" meaning "day grid". Rename
  • Rename day cells by index/role, not by weekday. Replace the Sunday...Saturday layer names with a single repeated day-cell instance indexed by position, or with semantic roles (header-cell, day-cell, spacer-cell). Weekday is data, not layer identity. Rename
  • Replace chevron raster glyphs with vector icon instances. Swap the two shape_full image references per variant (×3=6 assets) for the DS chevron icon component. Color-bind to main/date-picker/month-header/color/icon so hover/disabled states propagate. Asset
  • Add Prev chevron to the Month variant. Navigate between year clusters (2015-2024, 2025-2034...) so Month behaves symmetrically with Date and Year. State
  • Remove the drawn Scrollbar on the Year variant. Delete node 18414:6277 and its child rectangle. Mark the Grid frame as scrollable in the component spec (overflow-clip already applied) and document that native pickers render the scroll indicator automatically. Property
  • Add cell state coverage via Picker Cell axes. Expose Pressed, Hover/Focus, In-range, Range-start, Range-end, and business-rule Disabled on the unified Picker Cell component. Link to Figma tokens in a dedicated main/date-picker/cell/* scope. State
  • Introduce a main/date-picker/group/* token scope for the surface. Separate the panel-level bg/border/shadow tokens from the month-header tokens so the two can be themed independently. Token
  • Document as a reference spec, not a production component. Given the native-pickers-own-this direction, this Figma component should be annotated as a visual reference for designers reviewing what the native picker will look like in GCash theme — not as a component developers are expected to rebuild. Add a description banner in Figma and mark it _reference. Docs
Variants

3 variants on a single Type axis. All share a 360×296 card frame, identical header layout, and the same bg/border/shadow tokens. Only the grid body changes.

Type=Date

Day grid. Header shows "Month / Year" with Prev/Next chevrons. Weekday row (Su/M/T/W/Th/F/Sa) followed by 6 rows of 7 day cells. Today shown with 1.5px blue ring; prev/next-month days dimmed.

Properties
TypeDate
Type=Year

Year grid. Header shows "Year". 3-col grid with overflow-clip and a drawn Scrollbar decoration at the right. Selected year shown with 1px blue ring and blue label.

Properties
TypeYear
Type=Month

Month grid. Header shows "Year" with only a Next chevron (Prev is missing — asymmetric with Date and Year). 3-col × 4-row grid of 12 months. Selected month shown with 1px blue ring.

Properties
TypeMonth
Colors by State

Panel-level colors are shared across all three variants. Cell-level colors belong to Date Picker - Item and Month/Year Picker - Item — shown here for cross-reference.

RoleTokenDEFAULTSELECTED / TODAYDISABLED
Panel bgmain/date-picker/month-header/color/bg#FFFFFF
Panel bordermain/date-picker/month-header/color/border#E5EBF4 (1px)
Panel shadowelevation/app/shadow0 6px 12px -8 rgba(2,14,34,.16)
Header labelmain/date-picker/month-header/color/label#0A2757
Header chevronmain/date-picker/month-header/color/icon#005CE5
Weekday bgmain/date-picker/week-header/color/bg#FFFFFF
Weekday labelmain/date-picker/week-header/color/label#0A2757
Day cell bgmain/date-picker/day/color/unselected/bg#FFFFFF#FFFFFF (ring)#FFFFFF
Day cell labelmain/date-picker/day/color/unselected/label#0A2757#005CE5#C2CFE5
Today ring (day)border/color-border-primary#005CE5 (1.5px)
Selected ring (month/year)border/color-border-primary#005CE5 (1px)
Prev/next-month day labeltext/color-text-disabled#C2CFE5
Scrollbar overlay (Year)bg/color-bg-inverse (10% opacity)#0A2757 @ 10%
Layout
PropertyValue
Panel width360px
Panel height296px (fixed on Year/Month; Hug on Date)
Panel padding16px all sides (space/space-16)
Panel corner radius0 top-left/top-right, 8px bottom-left/bottom-right
Row gap8px (space/space-8)
Header height24px (chevron size sets it)
Chevron size24 × 24
Day cell size32 × 32 (pill radius 30px)
Day cell padding10px top, 12px bottom, 6px horizontal
Month/Year cellflex-1 × 32, 8px radius (radius/radius-3)
Month/Year cell padding10px top, 8px bottom, 12px horizontal
Month/Year grid gap16px horizontal
Scrollbar overlay (Year)4 × 80, 99px pill, 10% opacity
Typography
LayerText StyleFontSizeTrackingLine-height
Header label (Month/Year, Year, Year)Primary/Label/LargeProxima Soft Bold18px0.25px18px
Weekday labelPrimary/Label/SmallProxima Soft Bold14px0.25px14px
Day cell labelPrimary/Label/Light/SmallProxima Soft Semibold14px0.25px14px
Month / Year cell labelPrimary/Label/Light/SmallProxima Soft Semibold14px0.25px14px
Native Handling Native Owns It

The calendar panel is rendered natively on both platforms — SwiftUI DatePicker with .graphical style, and Material 3 DatePicker / DatePickerDialog. A thin EBDatePickerPanel wrapper can tokenize colors to match the DS without redrawing the surface.

iOS — SwiftUI

DatePicker(
    "Date of Birth",
    selection: $birthDate,
    displayedComponents: .date
)
.datePickerStyle(.graphical)
.tint(Color.ebBrand)
// SwiftUI renders the calendar, year picker, month picker,
// keyboard navigation, and VoiceOver support.

Android — Jetpack Compose (Material 3)

val state = rememberDatePickerState()
DatePicker(
    state = state,
    colors = DatePickerDefaults.colors(
        selectedDayContainerColor = EBColors.brand,
        todayDateBorderColor    = EBColors.brand
    )
)
// Material 3 renders the full calendar + day/month/year pickers.

If the DS wants a custom overlay (e.g. custom shadow or brand radius), EBDatePickerPanel can wrap the native component. The Figma spec here is the visual target for the tokenized wrapper — not a layout to rebuild from scratch.

Property Mapping
Figma PropertySwiftUI EquivalentCompose EquivalentNotes
Type=DatedisplayedComponents: .date (graphical style, default view)DisplayMode.PickerDay grid. Native primary view for DatePicker.
Type=Year(tap header label in .graphical style)DisplayMode.Input / year header tapYear list surfaced via native interaction, not a separate view.
Type=Month(tap month header in .graphical style)(year-cluster header tap)Month grid surfaced via native interaction. Currently missing Prev chevron in Figma.
Header label "Month / Year"header automatically renderedheadline slotNative pickers render this from selection. No explicit prop.
Prev / Next chevronsautomatic on .graphicalautomatic on DatePickerHandled by the native primitive. Raster glyphs in Figma should be removed (C6).
Day cellcell styling via tint / accentColorcolors.dayContentColor, selectedDayContainerColorNative draws the cell; DS supplies the tint + today ring color.
Year / Month cellstyled via tintcolors.yearContentColor, selectedYearContainerColorUnified into Picker Cell in the family recommendation.
Scrollbar overlay (Year)Native indicator appears during scroll. Figma decoration should be removed.
SwiftUI (tokenized wrapper)
ios/Components/DatePicker/EBDatePickerPanel.swift
Jetpack Compose (tokenized wrapper)
android/components/datepicker/EBDatePickerPanel.kt
Accessibility
RequirementiOSAndroid
Calendar role / traitsAutomatic via DatePicker (.isDatePicker trait)Automatic via Material 3 DatePicker (Role.DatePicker)
Keyboard navigationArrow keys move between day cells on iPad / hw keyboardD-Pad + hw keyboard navigation by default
Focus ringSystem focus ring on focused cellSystem focus indicator on focused cell
Screen reader labelVoiceOver announces day-of-week, date, month, and Selected / TodayTalkBack announces full date + state
Dynamic Type / font scalingAutomaticAutomatic
Locale / first-day-of-weekCalendar.current.firstWeekdayLocale.getDefault().firstDayOfWeek
minDate / maxDatein: Date...Date range parameterselectableDates / yearRange
Usage Guidelines

Do

Render the panel through SwiftUI's DatePicker(.graphical) or Compose's Material 3 DatePicker. Apply DS tokens via .tint / colors=....

Don't

Don't hand-draw the calendar grid, chevrons, or month/year pickers. You'll reimplement leap-year logic, locale handling, VoiceOver/TalkBack, and keyboard support that the native pickers provide for free.

Do

Use the Figma spec as a visual target for the tokenized wrapper — colors, corner radius, shadow, and ring thickness.

Don't

Don't treat the Figma layout as a 1:1 blueprint — the native picker's internal spacing and cell sizes may not match exactly, and that's fine.

Do

Pass an explicit in: Date... range to enforce minDate/maxDate and rely on the platform's disabled styling.

Don't

Don't style disabled cells manually in consumer code — let the native picker dim them consistently with OS conventions.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds FixDay cells named by weekday (Sunday, Monday...). Weekday header rows re-use Date Picker - Item rather than a dedicated weekday cell. Scrollbar drawn as a geometry layer.
C2Variant & Property NamingNeeds FixType=Date | Year | Month is misleading ("Date" means day-grid). Rename to mode=day | month | year.
C3Token CoveragePartialMost colors, spacing, radius, and shadow values are token-bound. Missing a dedicated main/date-picker/group/* scope — panel reuses month-header tokens as a proxy.
C4Native MappabilityNot Applicable / ReworkBoth platforms own the full calendar surface. Scrollbar drawn as geometry, Month missing Prev chevron — neither expressible natively. Recommend wrapping native pickers.
C5Interaction State CoverageNeeds FixOnly Today (day) and Selected (year/month) exist. Missing Pressed, Hover/Focus, In-range, Range-start, Range-end, Business-rule Disabled.
C6Asset & Icon QualityNeeds FixChevrons are raster shape_full image references across all 3 variants. Replace with vector icon instances.
C7Code Connect LinkabilityNot MappedBlocked by C1/C2/C4/C5/C6 and by the native-wrapper direction. Map only once the Picker Cell family is unified and the surface is confirmed as a wrapper, not a redraw.
Code Connect
AspectStatusNotes
Property namingNeeds FixRename Type to mode with values day | month | year to match native conventions.
Cell primitive unificationPendingDay / Month / Year cells should become one Picker Cell with kind + state axes before mapping.
Native component filePendingEBDatePickerPanel wrapper to be created around SwiftUI DatePicker(.graphical) / Compose DatePicker.
Raster chevronsBlockerReplace with vector icon instances before mapping — raster URLs are not representable in native code.
RecommendationConsolidateDon't ship as a drawn component. Wrap native pickers and tokenize colors.
Variants Inventory (3 total)

Single axis. 3 variants on Type. All share the 360×296 card frame.

TypeDimensionsHeaderGridNode ID
Date360 × 296Prev · "Month / Year" · Next7 × 7 (weekdays + 6 day rows)12879:49310
Year360 × 296Prev · "Year" · Next3 × 7 visible + Scrollbar overlay18431:2825
Month360 × 296(empty) · "Year" · Next3 × 4 (12 months)18431:2826
1.0.0 — April 2026 Initial
Initial Assessment · node 18431:2822
Component assessed — 3 variants (Date, Year, Month) at 360×296. Shared header, bg, border, shadow. All panel-level colors and spacing bound to tokens. Documented
Initial
Day cells named by weekday — Layers named Sunday, Monday...Saturday instead of index/role. Weekday is data, not layer identity. Open
C1 Open
Type axis misleading — "Date" means day-grid view. Rename to mode=day | month | year to match SwiftUI / Material 3 terminology. Open
C2 Open
Year variant: drawn Scrollbar decoration — Node 18414:6277 is a 4×80 pill absolutely positioned over the Year grid. Native pickers render the scroll indicator automatically; this decoration misrepresents scroll state. Open
C4 Open
Month variant: Prev chevron missing — Header has only the Next chevron. Asymmetric with Date and Year. Open
C4 Open
No native primitive for the drawn surface — SwiftUI DatePicker(.graphical) and Material 3 DatePicker own the full calendar. Recommend wrapping and tokenizing, not redrawing. Open
C4 Open
Cell states missing — No Pressed, Hover/Focus, In-range, Range-start, Range-end, or business-rule Disabled coverage. Only Today (day) and Selected (year/month) exist. Open
C5 Open
Chevrons are raster glyphs — Both Prev and Next in every variant reference shape_full image URLs. Replace with vector icon instances. Open
C6 Open
No dedicated panel token scope — Panel bg/border reuse main/date-picker/month-header/*. Add a main/date-picker/group/* scope. Open
C6 Open
Code Connect not registered — Blocked by the native-wrapper direction and by the pending Picker Cell unification. Open
C7 Open
Date Picker - Item ConsolidateRequires ReworkComponent link

The 32×32 day cell rendered inside the Date Picker - Group Type=Date grid. 7 variants across two axes: Type=Default | Today | Selected | Range (Middle) | Prev/Next × State=Enabled | Disabled. Only Default and Today ship with both states; Selected, Range (Middle), and Prev/Next exist only as Enabled. Sibling to Month and Year Picker - Item (100×32 cell) — both are candidates to collapse into a single unified Picker Cell primitive.

Consolidate into a unified Picker Cell
This cell and Month and Year Picker - Item are the same selectable-cell primitive at different pixel sizes (32×32 vs 100×32) with identical state semantics. Collapse both into a single Picker Cell with kind: day | month | year + state: default | today | selected | range-middle | prev-next | disabled. Also note: at 32×32 the cell is below WCAG's 44×44 minimum touch target (A11y).
In Context

The cell is an instance inside Date Picker - Group (Type=Date). 42 cells are rendered across 6 rows × 7 columns. A sibling weekday-header row also instance-swaps this component, which is one of the reasons C1 flags the layer-naming convention.

SuMTWThFSa
Live Preview

Switch Type and State to compare the 7 published variants. Range (Middle), Selected, and Prev/Next are only defined as Enabled.

Properties
Type
State
DS Health
Reusable
Warn
Used only inside Date Picker - Group (Date view). Because it is coupled to the 32×32 day grid it cannot be reused for month or year cells — which is exactly why a sibling Month and Year Picker - Item exists at 100×32 doing nearly the same job.
Self-contained
Partial
Text, bg, ring, and radius are all token-bound via main/date-picker/day/*. However, Range (Middle) uses absolutely-positioned sibling rectangles (Range highlight start, Range highlight end) to spill the range strip into adjacent cells — this is row-level geometry leaking into a cell-level component.
Consistent
Warn
Type mixes display roles (Default, Today, Prev/Next) with selection state (Selected, Range Middle) on one axis. Disabled exists only on Default and Today — Selected, Range (Middle), and Prev/Next have no Disabled variant. Value Range (Middle) uses parentheses/space in a variant name.
Composable
Fail
Doesn't compose into a native picker — both SwiftUI DatePicker(.graphical) and Material 3 DatePicker render their own cells. Even if kept as a visual reference, it duplicates Month and Year Picker - Item at a different size instead of being one Picker Cell with a kind axis.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesType=Default, State=EnabledPlain day number on white. No ring, no fill. #0A2757 label.
TodayYesYesType=Today, State=Enabled1.5px blue ring, blue label. Native equivalent: todayDateBorderColor on Material 3.
SelectedYesYesType=Selected, State=EnabledSolid blue fill (#005CE5), white bold label. Only exists as Enabled — no Disabled form.
Range (Middle)YesYesType=Range (Middle), State=EnabledWeakest-info bg (#E5F1FF), bold blue label. Ships with extraLeft/extraRight booleans that bleed the strip into adjacent cells.
Prev/NextYesYesType=Prev/Next, State=EnabledGreyed label (#C2CFE5) for days spilling over from the adjacent month. Not a true Disabled — the day is still conceptually pickable, just visually dimmed.
Disabled (Default/Today)YesYesType={Default|Today}, State=DisabledLabel drops to #9BC5FD (Today) or #C2CFE5 (Default). Disabled missing on Selected, Range, and Prev/Next.
Pressed / Focused / HoverNoNoNot defined on any Type. Native pickers supply these automatically.
Today + SelectedNoNoNot defined. Unclear which presentation wins when today is also the selected date.
Touch target32×3232×32Below WCAG minimum 44×44. Native pickers enforce their own hit areas, but any custom wrapper would need to extend the tap area beyond the visual cell.
Open Issues
  • Sibling duplication with Month and Year Picker - Item. The two components are the same selectable-cell primitive at different pixel sizes (32×32 vs 100×32) with overlapping state semantics (Default, Today, Selected). Maintaining them as siblings doubles the variant surface and forces the Group panel to decide which cell to instance-swap based on the current view. C1 · Layer Structure & Naming
  • Range continuity is drawn per-cell, not at the row. The Range (Middle) variant includes two absolutely-positioned siblings (Range highlight start, Range highlight end) that spill 28–34% beyond the cell bounds to visually connect with neighbours. Range continuity is row-level geometry; modelling it on the cell produces per-cell decoration with leak. C1 · Layer Structure & Naming
  • Type axis mixes display role with selection state. Default, Today, and Prev/Next describe what the cell is; Selected and Range (Middle) describe what the user has done. These belong on two axes (role and selection), not one. The current shape produces invalid combinations (e.g. you can't express "Today that is also Selected"). C2 · Variant & Property Naming
  • Variant value Range (Middle) uses punctuation and whitespace. Rename to range-middle (or, after the axis-split, selection=range-middle). Current value generates type="Range (Middle)" in code-connect output, which is awkward to match on. C2 · Variant & Property Naming
  • Cell has no 1:1 native primitive. Both SwiftUI DatePicker(.graphical) and Material 3 DatePicker render their own day cells and don't accept a custom cell view. This Figma component is therefore a reference spec, not a mappable component — should be marked as such or merged into the Picker Cell family. C4 · Native Mappability
  • Disabled coverage incomplete. The State axis nominally supports Disabled, but Disabled variants exist only on Default and Today. Selected, Range (Middle), and Prev/Next have no Disabled form — unreachable if the user picks a selected date then the parent toggles disabled. C5 · Interaction State Coverage
  • No Pressed, Hover, or Focused variants. Tap feedback (iOS ripple / Android state layer) and keyboard focus ring are not defined. Native pickers supply these automatically, but any custom overlay or wrapper has no tokens to apply. C5 · Interaction State Coverage
  • Today + Selected collision is unresolved. There is no variant for the common case of today being the currently-selected date. The design team should decide which presentation wins (ring + fill, fill-only, or a hybrid) and publish a variant. C5 · Interaction State Coverage
  • Selection emphasis pattern not shared with Month/Year cells. Selected on this cell is a solid fill; Selected on Month and Year Picker - Item is a 1px ring. The two selection treatments drift — should be one token-driven "selection emphasis" applied consistently per kind. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked by the native-pickers-own-it direction (C4) and by the pending Picker Cell family unification (C1). Map only once the unified component exists and the wrapper surface is confirmed. C7 · Code Connect Linkability
Design Recommendations
  • Family — Consolidate Date Picker - Item + Month and Year Picker - Item into ONE Picker Cell. Both are selectable cells with identical state semantics (Default / Today / Selected / Disabled); only pixel size (32×32 vs 100×32) and typography differ. Proposed schema: kind=day | month | year + state=default | today | selected | range-middle | prev-next | disabled. Collapses 10 sibling variants across two components into one component with two clean axes. A single native PickerCell composable renders the correct typography per kind. Family
  • Property — Split Type into role + selection (after consolidation). Within the unified Picker Cell, separate display role (default | today | prev-next) from selection state (none | selected | range-start | range-middle | range-end). Lets designers express "Today that is also Selected" and fixes the current invalid combinations. Property
  • Rename Range (Middle) to range-middle. Remove parentheses and whitespace from the variant value. Cleaner code-connect output and matches the kebab-case used by other DS enums. Rename
  • State — Add Pressed, Focused, and Today+Selected variants. Extend the state axis with Pressed and Focused (needed for any custom wrapper rendering tap / keyboard affordances), and publish a decision variant for the common "today is also selected" case. State
  • State — Complete Disabled coverage across all Types. The State axis currently only exists on Default and Today. Publish Disabled variants for Selected, Range (Middle), and Prev/Next so the axis is rectangular — no missing cells in the variant matrix. State
  • Composition — Move range-strip continuity from the cell to the row. Remove the absolutely-positioned Range highlight start / Range highlight end siblings from the cell. Render the range strip as a row-level decoration behind the cells (a full-width rectangle between range-start and range-end). Keeps the cell component self-contained. Composition
  • Token — Share a selection-emphasis token across Picker Cell kinds. Create main/picker-cell/selection/* tokens that resolve to either "fill" or "ring" based on kind, so day/month/year selection treatments stay intentionally consistent rather than drifting. Token
  • A11y — Flag the 32×32 touch target. Below WCAG 2.5.5 minimum 44×44. If a custom wrapper is ever built, extend the hit area beyond the visual cell (add transparent padding). Document this on the component so consumers don't accidentally ship the tight target outside the native picker context. A11y
  • Docs — Mark as reference, not a production component. Given that both platforms render their own day cells inside the native DatePicker, this cell is a visual reference for the token-styled wrapper, not a component developers rebuild. Add a description banner and a _reference prefix once the Picker Cell family unification lands. Docs
Variants

7 published variants on Type × State. All share a 32×32 frame, 30px pill radius, and Primary/Label/Light/Small typography. Selected and Range (Middle) switch to Primary/Label/Small (bold).

Default · Enabled

The base day cell. Plain label on white, no ring or fill.

Properties
TypeDefault
StateEnabled
Today · Enabled

Today marker. 1.5px blue ring, blue label.

Properties
TypeToday
StateEnabled
Selected · Enabled

Currently-selected date. Solid blue fill, white bold label. No Disabled form.

Properties
TypeSelected
StateEnabled
Range (Middle) · Enabled

A cell inside a selected date range. bg/color-bg-info-weakest fill, bold blue label. Ships with extraLeft/extraRight booleans that spill the strip into adjacent cells.

Properties
TypeRange (Middle)
StateEnabled
Prev/Next · Enabled

A day from the adjacent month spilling into the current month's grid. Dimmed label, no ring, no fill.

Properties
TypePrev/Next
StateEnabled
Default · Disabled

Disabled day. Label dims to text/color-text-disabled (#C2CFE5).

Properties
TypeDefault
StateDisabled
Today · Disabled

Today on a disabled day. Ring and label both use border/color-border-primary-disabled / text/color-text-primary-disabled (#9BC5FD).

Properties
TypeToday
StateDisabled
Colors by Type

All cell colors are bound to tokens. Selected uses the main/date-picker/day/color/selected/* scope; Default/Today borrow main/date-picker/day/color/unselected/* plus primary text/border tokens for the ring and label. Range (Middle) reuses the shared bg/color-bg-info-weakest rather than a picker-scoped token.

TypeRoleTokenENABLEDDISABLED
Defaultbgmain/date-picker/day/color/unselected/bg#FFFFFF#FFFFFF
Defaultlabelmain/date-picker/day/color/unselected/label#0A2757#C2CFE5 (text/color-text-disabled)
Todaybgmain/date-picker/day/color/unselected/bg#FFFFFF#FFFFFF
Todayringborder/color-border-primary#005CE5 (1.5px)#9BC5FD (border/color-border-primary-disabled)
Todaylabeltext/color-text-primary#005CE5#9BC5FD (text/color-text-primary-disabled)
Selectedbgmain/date-picker/day/color/selected/bg#005CE5
Selectedlabelmain/date-picker/day/color/selected/label#FFFFFF
Range (Middle)bgbg/color-bg-info-weakest#E5F1FF
Range (Middle)labeltext/color-text-primary#005CE5 (bold)
Prev/Nextbgmain/date-picker/day/color/unselected/bg#FFFFFF
Prev/Nextlabeltext/color-text-disabled#C2CFE5
Layout
PropertyValue
Cell size32 × 32
Corner radius30px (pill)
Padding10px top, 12px bottom, 6px horizontal
Label width20px (fixed, centred)
Today ring1.5px solid
Range highlight strip32px tall, bleeds ~28–34% beyond cell edges via extraLeft / extraRight
Gap (inside grid)0 (cells are edge-to-edge in the row)
Typography
VariantText StyleFontWeightSizeLine-heightTracking
Default / Today / Prev/NextPrimary/Label/Light/SmallProxima SoftSemibold (600)14px14px0.25px
Selected / Range (Middle)Primary/Label/SmallProxima SoftBold (700)14px14px0.25px
Native Handling Native Owns It

The day cell is drawn by the native picker. SwiftUI DatePicker(.graphical) and Material 3 DatePicker both render their own day cells and don't expose a custom-cell slot. Token-tint the native picker to match the DS appearance — don't ship a custom EBDatePickerItem composable.

iOS — SwiftUI

DatePicker(
    "Date of Birth",
    selection: $birthDate,
    displayedComponents: .date
)
.datePickerStyle(.graphical)
.tint(Color.ebBrand)           // Selected day fill
.accentColor(Color.ebBrand)     // Today ring
// SwiftUI renders each cell; Today and Selected come from
// Calendar.current + the bound selection. No custom cell view.

Android — Jetpack Compose (Material 3)

val state = rememberDatePickerState()
DatePicker(
    state = state,
    colors = DatePickerDefaults.colors(
        selectedDayContainerColor = EBColors.brand,          // Selected
        selectedDayContentColor   = EBColors.onBrand,
        todayDateBorderColor      = EBColors.brand,          // Today ring
        dayInSelectionRangeContainerColor = EBColors.infoWeakest // Range (Middle)
    )
)
// Material 3 renders each day cell; prev/next-month cells are dimmed
// automatically via dayContentColor variants.

If the product absolutely needs a custom cell (e.g. an event-dot indicator), build a non-native calendar grid and compose PickerCell inside it — but at that point you lose the native a11y and locale handling, which is a significant trade-off.

Property Mapping
Figma PropertySwiftUI EquivalentCompose EquivalentNotes
Type=Default(default rendering)dayContentColorBase day cell. Uses primary text token.
Type=Today.accentColor / automatic Today ringtodayDateBorderColorNative pickers detect Today from Calendar.current; you only supply the ring color.
Type=Selected.tint (via selection binding)selectedDayContainerColor / selectedDayContentColorSolid fill. Not a custom cell — selection is derived from the bound Date.
Type=Range (Middle)(no direct API — requires custom calendar)dayInSelectionRangeContainerColorMaterial 3 supports range via DateRangePicker. SwiftUI has no built-in range picker — needs a custom component.
Type=Prev/Next(automatic dimming)(automatic dimming via dayContentColor)Adjacent-month days are dimmed by the native picker automatically. No prop needed.
State=Disabledin: Date... range parameterselectableDatesEnforced via the allowable date range — the picker dims cells outside the range automatically.
extraLeft / extraRightFigma-only. Range continuity is handled by the native picker or by row-level geometry in a custom component, not per-cell.
SwiftUI (if ever materialized, else reference only)
ios/Components/DatePicker/EBPickerCell.swift · kind: day
Jetpack Compose (if ever materialized, else reference only)
android/components/datepicker/EBPickerCell.kt · kind: Day
Accessibility
RequirementiOSAndroid
Touch target (44 × 44 min)Figma cell is 32 × 32 — native picker extends hit areaFigma cell is 32 × 32 — native picker extends hit area
Screen reader labelVoiceOver: "Friday, 5 March, Today" (from DatePicker)TalkBack: "5 March 2026, Today" (from Material 3)
Selected announcement"Selected" trait added automatically"Selected" state added automatically
Focus ringSystem focus ring on iPad / hw keyboardSystem focus indicator on D-Pad / hw keyboard
Disabled announcement"Dimmed" trait when outside in: range"Disabled" state when outside selectableDates
Dynamic Type / font scalingAutomaticAutomatic
Usage Guidelines

Do

Treat this cell as a visual reference for how the native picker should look in GCash theme — colors, ring thickness, radius, and label weight.

Don't

Don't ship a standalone EBDatePickerItem composable. Native pickers render their own cells; a custom cell composable has no slot to plug into.

Do

If you genuinely need a custom cell (e.g. event dots, legend markers), merge with Month and Year Picker - Item into a unified PickerCell(kind:, state:) and compose it inside a custom calendar grid.

Don't

Don't maintain Date Picker - Item and Month and Year Picker - Item as siblings — they're the same primitive at different sizes.

Do

Rely on the native picker for locale-aware firstDayOfWeek, leap-year handling, VoiceOver / TalkBack, and minDate/maxDate enforcement.

Don't

Don't draw disabled days manually. Pass an in: range (SwiftUI) or selectableDates (Compose) and let the platform style them.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds FixSibling-duplication with Month and Year Picker - Item. Range continuity modelled with absolute-positioned siblings that spill beyond cell bounds.
C2Variant & Property NamingNeeds FixType mixes display role with selection state on one axis. Value Range (Middle) uses punctuation/whitespace.
C3Token CoveragePassAll colors, spacing, radius, and typography are token-bound (main/date-picker/day/* + primary text/border tokens).
C4Native MappabilityNot Applicable / ReworkNative pickers own the day cell and don't accept a custom cell view. Reference-only unless merged into a custom PickerCell.
C5Interaction State CoverageNeeds FixDisabled missing on Selected, Range (Middle), Prev/Next. No Pressed, Hover, Focused. Today + Selected collision unresolved.
C6Asset & Icon QualityPartialNo raster assets — cell is pure geometry + text. But selection emphasis (fill vs ring) drifts from Month and Year Picker - Item; should be one token-driven pattern.
C7Code Connect LinkabilityNot MappedBlocked by C4 (native owns it) and by the pending Picker Cell family unification.
Code Connect
AspectStatusNotes
Property namingNeeds FixSplit Type into role + selection; rename Range (Middle) to range-middle.
Family unificationPendingMerge with Month and Year Picker - Item into PickerCell with kind: day | month | year.
Native component filePendingNo standalone composable — native DatePicker renders cells. Only materialize EBPickerCell if a custom calendar grid is ever built.
Range continuityBlockerMove Range highlight start/end geometry from the cell up to the row before mapping.
RecommendationConsolidateMerge into PickerCell, mark as reference spec for the native picker's day cell.
Variants Inventory (7 total)

5 Type × 2 State would produce 10 variants, but only 7 are published — Selected, Range (Middle), and Prev/Next exist only with State=Enabled.

TypeStateDimensionsEmphasisNode ID
DefaultEnabled32 × 32none12874:42181
DefaultDisabled32 × 32dim label13948:3888
TodayEnabled32 × 321.5px blue ring13944:5633
TodayDisabled32 × 321.5px primary-disabled ring13948:3891
SelectedEnabled32 × 32solid blue fill, white label12874:42183
Range (Middle)Enabled32 × 32weakest-info fill, bold blue label, extraLeft/extraRight booleans13944:5637
Prev/NextEnabled32 × 32dim label only13944:5653

Missing combinations: Selected · Disabled, Range (Middle) · Disabled, Prev/Next · Disabled. Also missing across all Types: Pressed, Focused, Hover, Today + Selected.

1.0.0 — April 2026 Initial
Initial Assessment · node 12874:42180
Component assessed — 7 variants across Type × State. 32×32 pill cell with token-bound colors, spacing, radius, and typography. Documented
Initial
Sibling duplication with Month and Year Picker - Item — Same selectable-cell primitive at different sizes. Proposal: collapse both into Picker Cell with kind: day | month | year. Open
C1 Open
Range continuity modelled per-cellRange highlight start and Range highlight end are absolute-positioned siblings that spill beyond the cell. Should be row-level geometry. Open
C1 Open
Type mixes role and selection — Default/Today/Prev-Next are display roles; Selected/Range (Middle) are selection states. Split into role + selection. Open
C2 Open
Variant value Range (Middle) needs cleanup — Rename to range-middle for clean code-connect output. Open
C2 Open
No native primitive for a day cell — Native DatePicker renders its own cells on both platforms. Reference-only unless a custom calendar grid is built. Open
C4 Open
Disabled missing on Selected/Range/Prev-Next — State axis is not rectangular. Also missing Pressed, Hover, Focused, and Today + Selected. Open
C5 Open
Selection emphasis drifts across sibling cells — Day cell Selected is a fill; month/year cell Selected is a ring. Align on one token-driven pattern per kind. Open
C6 Open
Code Connect not registered — Blocked by the native-pickers-own-it direction and by the pending Picker Cell family unification. Open
C7 Open
Date Picker RestructureRequires ReworkComponent link

A field-shaped trigger that opens an inline calendar panel. 5 usable variants across State (Default/Active) × isFilled (true/false) × isDisabled (Yes/No) — of a 2×2×2=8 matrix, only 5 combinations are defined because Disabled collapses both State values into a single visual. Lead component of the Date Picker family (Group, Item, Month/Year Picker - Item).

Restructure required before handoff
Component is structurally an Input Field with a calendar glyph — the whole calendar panel should be a separate popover, not nested in the trigger. State × isDisabled axes produce invalid combinations (C2). Calendar icon is a raster (C6). No Error/Pressed states (C5). Blocks native DatePicker mapping.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Date of BirthMarch 2026
Live Preview

Toggle state, fill, and disabled to see the trigger update. Active state shows the inline calendar panel.

Properties
State
isFilled
isDisabled
DS Health
Reusable
Partial
Trigger is generic enough for any date field context. Re-use is limited because the calendar panel is bundled inline — consumers can't show the trigger without the whole calendar, and can't show the calendar without the trigger.
Self-contained
Warn
Carries its own tokens for trigger states, but no Error state despite sibling Select Field having one. Calendar glyph is a raster image reference, not a DS icon instance.
Consistent
Warn
isDisabled uses Yes/No (should be true/false). Axis design is ambiguous — Disabled is conceptually a state, not orthogonal to State, yielding 5 usable of 8 combinations. Diverges from Input Field / Select Field pattern which use a single State enum including Disabled.
Composable
Warn
Calendar popover (Date Picker - Group) is composed inline, not as a separate overlay with its own positioning. Cannot be swapped for a sheet, bottom sheet, or native dialog. Blocks 1:1 mapping to SwiftUI DatePicker / Compose DatePickerDialog.
Behavior
StateiOSAndroidFigma PropertyNotes
Default (empty)YesYesState=Default, isFilled=falseGray #D7E0EF border. Placeholder "Value" #90A8D0.
Default (filled)YesYesState=Default, isFilled=trueGray border, selected date shown in #0A2757.
Active (opening — empty)YesYesState=Active, isFilled=falseBlue #005CE5 2px border. Inline calendar panel attached below.
Active (filled)YesYesState=Active, isFilled=trueBlue border, filled value, calendar visible.
DisabledYesYesisDisabled=YesGray #EEF2F9 bg. Value #90A8D0. No border. Calendar glyph dims.
ErrorNoNoNot defined. Sibling Select/Input Field both support Error — must be added for validation flows (e.g. "Enter a valid birth date").
PressedNoNoNot defined. Touch feedback expected on both platforms.
Open Issues
  • Layer naming inconsistent with sibling fields. Trigger frame is named Select Field and its inner container / text-container are generic — but the component itself is Date Picker. The Date Picker - Group popover has layer names like row, Monday...Saturday (day-of-week labels as layer names rather than semantic roles). Normalize to kebab-case semantic names. C1 · Layer Structure & Naming
  • isDisabled uses Yes/No, isFilled uses False/True. Should be lowercase true/false for direct Swift Bool / Kotlin Boolean mapping. Inconsistent casing between the two booleans also breaks Code Connect naming uniformity. C2 · Variant & Property Naming
  • Axis design produces invalid combinations.State × isFilled × isDisabled is a 2×2×2=8 matrix but only 5 combinations exist because Disabled collapses State. Siblings (Input Field, Select Field) use a single State enum including Disabled — Date Picker should follow that pattern. C2 · Variant & Property Naming
  • Calendar popover is composed inline, not as an overlay. Date Picker - Group is nested inside the trigger's stack and positioned absolutely — it cannot be swapped for a sheet or presented by the native date dialog. SwiftUI DatePicker and Compose DatePickerDialog both expect the trigger and the picker to be separable. C4 · Native Mappability
  • No Error state. Input Field, Select Field, and Dropdown all support Error; Date Picker does not. Birth-date and expiry flows routinely need inline validation ("Must be 18+", "Date can't be in the past"). C5 · Interaction State Coverage
  • No Pressed state. Standard tap feedback (iOS highlight, Android ripple) is missing. Consumers improvise it per-screen. C5 · Interaction State Coverage
  • Calendar glyph is a raster shape_full image. The trigger icon pulls from an MCP asset URL rather than a vector icon instance. Won't scale cleanly, won't inherit state tokens, can't be swapped. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked by C1/C2/C4/C5/C6 above. C7 · Code Connect Linkability
Design Recommendations
  • Family — Unify the two cell primitives.Date Picker - Item (day cells) and Month and Year Picker - Item (month/year cells) are both selectable cells with identical state semantics (Default/Today/Selected/Disabled). Only size + typography differ. Propose one Picker Cell component with axes kind: day | month | year + state: default | today | selected | disabled. Reduces 10 variants across 2 components to 1 component with 2 clean axes. Family
  • Composition — Wrap the native date pickers instead of redrawing them. iOS: DatePicker(selection:, displayedComponents:) with .datePickerStyle(.graphical|.compact|.wheel|.field). Android: Material 3 DatePicker + DatePickerDialog. The DS EBDatePicker should be a thin wrapper that tokenizes the trigger and cells to match brand — not a from-scratch calendar redraw in Figma. Keeps accessibility, localization, and leap-year logic native. Composition
  • Field-trigger consistency — Unify with Input Field as type: .date. Structurally this is an Input Field with a date value display + calendar glyph. Matches SwiftUI TextField(value:, format: .date) / Compose OutlinedTextField(readOnly=true) + DatePickerDialog. Consolidates a fragmented field family and inherits Input Field's Error state, label slot, and helper text. Family
  • Collapse State × isFilled × isDisabled into a single state enum. Target schema: state: default | active | error | disabled + isFilled: true | false. Matches Input Field (8 variants from 4×2). Removes the 3 invalid combinations in the current matrix and aligns with the rest of the Form Elements family. Property
  • Separate the calendar panel from the trigger. Move Date Picker - Group to a true overlay component (top-anchored, dismissible, shadow-elevated) rather than an inline child of the trigger. Lets the trigger be used alone (e.g. inline text input mode) and lets the panel be presented by either a dropdown or a sheet depending on context. Composition
  • Add Error and Pressed states. Error: red border + subtext (mirror Input Field tokens). Pressed: slight bg tint for tap feedback. Required for form accessibility and parity with the rest of Form Elements. State
  • Replace the calendar raster with a vector icon instance. Use the existing DS calendar icon (if present) or add one to the icon library. Color-bind to selected-field/color/{state}/icon so it dims with Disabled automatically. Asset
  • Rename booleans to lowercase true/false.isDisabled=Yes/No to isDisabled=true/false; isFilled=False/True to isFilled=false/true. Consistent with the naming convention adopted by Input Field. Rename
States

5 usable variants from a 2×2×2 State × isFilled × isDisabled matrix. Disabled collapses both State values into one visual. All share the 360×68px trigger container; Active expands to show the inline calendar panel.

Default — Empty

Idle trigger with gray border and placeholder text. Calendar glyph visible on the right.

Properties
StateDefault
isFilledfalse
isDisabledNo
Default — Filled

Trigger showing a selected date. Gray border, filled text color #0A2757.

Properties
StateDefault
isFilledtrue
isDisabledNo
Active — Empty

Trigger focused with 2px blue border. Inline calendar panel attached below showing month header, weekday row, and date grid.

Properties
StateActive
isFilledfalse
isDisabledNo
Active — Filled

Trigger focused with 2px blue border and filled value. Calendar visible with the selected day highlighted in the grid.

Properties
StateActive
isFilledtrue
isDisabledNo
Disabled

Non-interactive. Gray #EEF2F9 bg, no border, value dims to #90A8D0. Calendar glyph dims. Only isFilled=true is defined in Disabled.

Properties
StateDefault
isFilledtrue
isDisabledYes
Colors by State

Trigger colors reuse the selected-field token family (shared with Dropdown and Select Field). Calendar panel uses dedicated date-picker tokens.

RoleTokenDEFAULTACTIVEDISABLED
Trigger borderselected-field/color/{state}/border#D7E0EF#005CE5 (2px)hidden
Trigger bgselected-field/color/{state}/bg#FFFFFF#FFFFFF#EEF2F9
Value (filled)selected-field/color/{state}/value#0A2757#0A2757#90A8D0
Placeholderselected-field/color/{state}/placeholder#90A8D0#90A8D0
Calendar iconselected-field/color/{state}/icon#005CE5#005CE5#9BC5FD
Header labelformgroup-header/color/label#0A2757#0A2757#0A2757
Panel bgdate-picker/month-header/color/bg#FFFFFF
Panel borderdate-picker/month-header/color/border#E5EBF4
Panel shadowelevation/app/shadow0 6px 12px rgba(2,14,34,.16)
Month labeldate-picker/month-header/color/label#0A2757
Month chevrondate-picker/month-header/color/icon#005CE5
Weekday labeldate-picker/week-header/color/label#0A2757
Day (unselected)date-picker/day/color/unselected/label#0A2757
Day (today border)border/color-border-primary#005CE5 (1.5px)
Day (prev/next month)text/color-text-disabled#C2CFE5
Layout
PropertyValue
Component width360px (fixed)
Trigger height46px
Trigger height (Active, empty)46px (border 2px inset)
Corner radius (trigger)6px (radius/radius-2)
Trigger padding6px top, 8px bottom, 12px horizontal
Calendar icon size32 × 32 (glyph ~24 × 25 inside)
Panel padding16px all sides
Panel corner radius8px top-left/top-right, 6px bottom-left/bottom-right
Panel gap (rows)8px
Day cell size32 × 32
Day cell radius30px (pill)
Month chevron size24 × 24
Header label padding-bottom8px
Typography
LayerText StyleFontSizeTrackingLine-height
Header labelPrimary/Label/Light/SmallProxima Soft Semibold14px0.25px14px
Trigger value / placeholderPrimary/Label/Light/SmallProxima Soft Semibold14px0.25px14px
Month / Year labelPrimary/Label/LargeProxima Soft Bold18px0.25px18px
Weekday labelPrimary/Label/SmallProxima Soft Bold14px0.25px14px
Day labelPrimary/Label/Light/SmallProxima Soft Semibold14px0.25px14px
Installation Planned API

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:date-picker:1.0.0")
}

Import

import EastBlueDS  // SwiftUI
import com.eastblue.ds.datepicker.*  // Compose

Package not yet published. These are the planned distribution paths. The recommendation is to wrap native date pickers rather than redraw them.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
State=DefaultDefault idle state (picker not shown)
State=ActiveisPresented: Binding<Bool>showPicker: BooleanCalendar panel/dialog visible
isFilled (False/True)selection: Binding<Date?>selectedDate: LocalDate?Derived from non-null selection
isDisabled (Yes/No).disabled(true)enabled=falseNon-interactive state
label (formgroup-header)label: Stringlabel: StringHeader text above the trigger
subtext (optional)helperText: String?helperText: String?Hint / error message below trigger
SwiftUI
ios/Components/DatePicker/EBDatePicker.swift
Jetpack Compose
android/components/datepicker/EBDatePicker.kt
Usage Snippets Planned API
Default
EBDatePicker("Date of Birth", selection: $birthDate)
EBDatePicker(
    label = "Date of Birth",
    selectedDate = birthDate,
    onDateSelected = { birthDate = it }
)
With Helper Text
EBDatePicker("Date of Birth", selection: $birthDate)
    .ebHelperText("You must be 18 or older")
EBDatePicker(
    label = "Date of Birth",
    selectedDate = birthDate,
    onDateSelected = { birthDate = it },
    helperText = "You must be 18 or older"
)
Disabled
EBDatePicker("Date of Birth", selection: $birthDate)
    .disabled(true)
EBDatePicker(
    label = "Date of Birth",
    selectedDate = birthDate,
    onDateSelected = { birthDate = it },
    enabled = false
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 × 44 pt48 × 48 dp
Accessibility label.accessibilityLabel("Date of Birth")contentDescription="Date of Birth"
Selected value.accessibilityValue(formattedDate)semantics { stateDescription }
RoleVoiceOver announces as date pickerTalkBack announces via Role.Button + expanded state
LocalizationNative DatePicker respects locale calendarNative DatePickerDialog respects locale calendar
Usage Guidelines

Do

Wrap the native DatePicker / DatePickerDialog so accessibility, localization, and leap-year logic are handled by the OS.

Don't

Redraw the calendar grid from scratch — the Figma calendar is a visual spec, not a reimplementation target.

Do

Pair the trigger with a visible label above it. Use helper text to clarify expected range ("Pick a future date").

Don't

Use the trigger as a free-text date field — it's for picking from the panel, not typing. If free-entry is required, use Input Field with a date format.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingPartialTrigger reuses Select Field / container / text-container. Day cell rows use weekday names (MondaySaturday) as layer names.
C2Variant & Property NamingNeeds FixisDisabled=Yes/No, isFilled=False/True. Inconsistent casing. Axis design yields 5 of 8 combinations.
C3Token CoverageReadyAll colors bound to selected-field and date-picker tokens. Spacing and radius tokens consistent.
C4Native MappabilityNeeds FixInline calendar panel blocks mapping to native DatePicker / DatePickerDialog. Must be separable.
C5Interaction State CoverageNeeds FixNo Error state (siblings have it). No Pressed state.
C6Asset & Icon QualityNeeds FixCalendar glyph is a raster shape_full image. Month chevrons use shape_full images as well.
C7Code Connect LinkabilityPendingBlocked by C1/C2/C4/C5/C6.
Code Connect
AspectStatusNotes
Property namingBlockedisDisabled/isFilled need lowercase boolean values
Axis designBlockedCollapse to single state enum matching Input/Select Field pattern
CompositionBlockedCalendar panel must be separable to map native dialog/sheet
Asset qualityBlockedCalendar glyph + chevrons must be vector icons
State coverageBlockedError and Pressed states missing
Native component filePendingEBDatePicker.swift / EBDatePicker.kt not yet created
Variants Inventory (5 total)

Declared as a 2×2×2 matrix (State × isFilled × isDisabled) but only 5 combinations exist — Disabled collapses both State and isFilled=false into a single variant.

StateisFilledisDisabledNode ID
DefaultfalseNo12879:49784
DefaulttrueNo12890:42872
ActivefalseNo12879:49827
ActivetrueNo13342:9932
DefaulttrueYes13342:10148
1.0.0 — April 2026 Initial
Initial Assessment · node 12879:49826
Component assessed — 5 usable variants documented across State × isFilled × isDisabled. Field-shaped trigger with inline calendar panel on Active. Lead component of the Date Picker family. Documented
Initial
Axis design produces invalid combinations — 2×2×2 matrix yields only 5 of 8 possible variants. Should collapse to a single state enum like Input/Select Field. Open
C2 Open
Boolean casing inconsistentisDisabled=Yes/No and isFilled=False/True. Both need lowercase true/false for Code Connect. Open
C2 Open
Calendar panel nested in trigger — Date Picker - Group is composed inline rather than being a separate overlay. Blocks mapping to native DatePicker / DatePickerDialog. Open
C4 Open
No Error or Pressed state — Siblings (Input Field, Select Field, Dropdown) have Error. Pressed is required for touch feedback parity. Open
C5 Open
Calendar glyph is a raster imageshape_full image reference, not a vector icon instance. Month chevrons also use raster shape_full assets. Open
C6 Open
Code Connect mappings — Not registered. Blocked by C1/C2/C4/C5/C6. Open
C7 Open
Dropdown Item Group ConsolidateNot ApplicableComponent link

The popover/menu surface that appears when a Dropdown trigger is tapped. Stacks Dropdown Items vertically inside a 6px rounded card with a drop shadow. Today it is a fixed layout of 8 pre-nested Dropdown Items with no slot or count parameter — acting more as a visual preview than a reusable component. Native platforms handle this surface via Menu (iOS) and DropdownMenu (Material 3), so this Figma component should be consolidated into Dropdown's composition pattern rather than shipped as its own DS primitive.

Consolidate into Dropdown composition
Not a reusable DS primitive. Layout is hardcoded (8 rows, last row is a detached frame — not a DropdownItem instance). Native menu infrastructure handles the popover surface on both platforms. Fold into the Dropdown component's expanded state as inline overlay behavior, not a separate component.
In Context

The group appears immediately below a Dropdown trigger in the expanded state. On native platforms it is rendered by the OS menu primitive, not drawn manually.

Live Preview

The group renders a vertical stack of Dropdown Item rows inside a rounded card with drop shadow. Only one visual state.

Properties
variant(none)
item count8 (fixed)
width366px (fixed)
DS Health
Reusable
Warn
Layout is hardcoded to 8 Dropdown Items with a fixed 366px width. No item count property, no content slot, no width-fills-container behavior. Consumers cannot reuse this for a 3-item menu or a 12-item menu without manually rebuilding the composition.
Self-contained
Partial
Carries its own bg, radius, and shadow tokens. But the last row is a detached frame named Dropdown - Item (node 6383:3442) instead of a DropdownItem component instance — breaking the self-contained promise.
Consistent
Warn
Seven rows are DropdownItem instances, the eighth is a detached duplicate frame. The component name uses a hyphenated " - " (Dropdown Item - Group) which doesn't match other DS naming ("Avatar Group", "Button Group").
Composable
Fail
Not a composable container. No slot, no items property, no variant axis. Cannot be nested inside Dropdown as an overlay — the Dropdown's Expanded variant draws its own list instead of referencing this group. Effectively a preview artifact, not a component.
Behavior
StateiOSAndroidFigma PropertyNotes
Default (open)YesYesWhite bg, 6px radius, drop shadow. Rendered by Menu (iOS) / DropdownMenu (Android) automatically.
Item hover/pressYesYesHandled by the per-item Dropdown Item component, not the group. Touch feedback is platform-native.
ScrollYesYesNative menus clip and scroll automatically when item count exceeds available height.
Open Issues
  • Last row is a detached frame, not a component instance. The eighth row (node 6383:3442, Dropdown - Item) is a hand-built frame with inline styles instead of a DropdownItem instance. Breaks the group's consistency and means any DropdownItem property change will not propagate to the last row. C1 · Layer Structure & Naming
  • No slot, no item count, no width parameter. The group is a fixed layout of 8 rows at 366px. There is no items slot, no numeric count property, and no fill-container option. Consumers cannot build a 3-item menu or a wider menu without rebuilding it. C1 · Layer Structure & Naming
  • Component name uses irregular " - " separator.Dropdown Item - Group doesn't match other DS group naming (Avatar Group, Button Group, List). Consider renaming to Dropdown Menu or folding under Dropdown/Menu. C2 · Variant & Property Naming
  • Popover surface does not exist as a separate native primitive. Both SwiftUI (Menu) and Compose (DropdownMenu) handle the surface — shadow, radius, bg, positioning, clipping — automatically. Modeling it as a standalone Figma component creates a phantom that cannot be Code Connect-mapped 1:1. C4 · Native Mappability
  • Last row has a bottom border. Every row in the group carries border-b including the final row, producing a redundant separator flush with the card's bottom edge. Should be removed on the last item or moved to a top-border-all-except-first pattern. C1 · Layer Structure & Naming
  • Code Connect mappings not registered. If the component is kept, it has no parameters to map. If consolidated into Dropdown, it will not need its own mapping. C7 · Code Connect Linkability
Design Recommendations
  • Consolidate into the Dropdown component's Expanded state. The popover surface is not independently reusable — it always pairs with a Dropdown trigger. Remove this as a separate DS primitive and express the overlay via Dropdown's type=Expanded variant. Native Menu/DropdownMenu primitives render the surface automatically. Family
  • Alternatively, convert to a Slot-based container. If kept, replace the hardcoded 8 instances with a Figma Slot accepting any number of Dropdown Items. Rename to Dropdown Menu. Set width to fill-container so the parent trigger decides sizing. Slot
  • Replace the detached last row with a DropdownItem instance. Node 6383:3442 is a hand-built frame — swap it for a DropdownItem component instance so property changes propagate uniformly. Composition
  • Remove the bottom border on the last item. Use a ::not(:last-child)-equivalent pattern (separator between rows, not after the final row) to avoid the double line against the card's bottom edge. Property
  • Rename to Dropdown Menu (if kept). "Dropdown Item - Group" reads as a group of items; "Dropdown Menu" matches the native primitive (Menu / DropdownMenu) and reads better in the component picker. Rename
  • Document as an internal artifact, not a shipped component. If the team keeps it purely for Figma layout previews, mark it _internal or move it to a Hidden page so it doesn't appear in the public component picker. Docs
Variants

Single variant. No property axes. Fixed 8-row layout at 366px width.

Default

Rounded card surface containing a vertical stack of Dropdown Items. 6px corner radius, white background, 12px-blur drop shadow at 6px offset.

Properties
variant(none)
item count8 (fixed)
Colors by State

Display-only surface. The group itself only contributes bg, shadow, and row dividers — all per-item colors come from Dropdown Item.

RoleTokenVALUE
Surface bgbg/color-bg-main#FFFFFF
Row dividermain/dropdown-item/color/default/border#E5EBF4
Shadow colorelevation/app/shadow/color-shadow#020E2229
Shadow borderelevation/app/shadow/color-border#FFFFFF00
Item labelmain/dropdown-item/color/default/label#0A2757
Layout
PropertyValue
Width366px (fixed)
Item count8 (fixed)
Corner radius6px
Item padding16px vertical, 12px left, 16px right
Item gapspace-8 (0px effective)
Row divider1px bottom border per row
Shadow offset0 6px (x y)
Shadow blur12px
Shadow spread-8px
Typography
LayerText StyleFontSizeTrackingLine-height
Dropdown item labelPrimary/Label/Light/LargeProxima Soft Semibold18px0.25px18px
Native Handling Not Applicable

This surface does not require a dedicated native component. Both iOS and Android expose menu primitives that render a shadowed card around items automatically. If the EB Dropdown is built using those primitives, no additional "group" wrapper is needed.

iOS — SwiftUI Menu

Menu("Category") {
    ForEach(items) { item in
        Button(item.name) { select(item) }
    }
}
// The shadowed card surface is drawn by SwiftUI, not by the app.

Android — Jetpack Compose DropdownMenu

DropdownMenu(
    expanded = expanded,
    onDismissRequest = { expanded = false }
) {
    items.forEach { item ->
        DropdownMenuItem(
            text = { Text(item.name) },
            onClick = { select(item) }
        )
    }
}
// Material 3 renders the card, shadow, and clipping automatically.

If the design team wants a custom overlay (e.g. to override shadow or radius), a thin EBDropdownMenu wrapper can be added — but the Figma component should still act as a slot, not a fixed 8-item layout.

Property Mapping
Figma PropertySwiftUI EquivalentCompose EquivalentNotes
(no properties)Menu / View ZStackDropdownMenuNo variant axis, no slot, no count. Nothing to map.
Width 366pxNative menus size to the anchor / content automatically.
Item count 8ForEach(items)items.forEachConsumers control the count via the collection they pass in.
SwiftUI (if wrapper is needed)
ios/Components/Dropdown/EBDropdownMenu.swift
Jetpack Compose (if wrapper is needed)
android/components/dropdown/EBDropdownMenu.kt
Accessibility
RequirementiOSAndroid
Menu roleAutomatic via MenuAutomatic via DropdownMenu (Role.DropdownList)
Focus trapVoiceOver moves focus into the menu on openTalkBack moves focus into the menu on open
Dismiss on outside tapAutomaticonDismissRequest
Max visible itemsNative scroll when exceededNative scroll when exceeded
Usage Guidelines

Do

Render the menu surface through SwiftUI's Menu or Compose's DropdownMenu. Let the platform handle shadow, clipping, positioning, and keyboard dismissal.

Don't

Don't hand-draw a shadowed card and place items inside it. You'll reimplement focus management, scroll clipping, and accessibility semantics that the platform gives you for free.

Do

Keep Dropdown Item as the per-row component. Use it inside the native menu's builder closure (SwiftUI) or inside DropdownMenuItem's text slot (Compose).

Don't

Don't use this Figma component as a layout reference for production — the fixed 8-row, 366px-wide, detached-last-row structure won't match real menu content.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds FixFixed 8-item layout with no slot. Last row is a detached frame (6383:3442), not a DropdownItem instance. Component name uses irregular " - " separator.
C2Variant & Property NamingPartialNo variant properties exist. Component name Dropdown Item - Group is irregular; prefer Dropdown Menu.
C3Token CoverageReadyAll colors, radius, spacing, and shadow values bound to tokens (bg/color-bg-main, elevation/app/shadow/*, space/space-*).
C4Native MappabilityNot ApplicablePopover surface is rendered by Menu / DropdownMenu natively. No 1:1 component to map to.
C5Interaction State CoverageReadySurface has no interactive states of its own. Per-item states live on Dropdown Item.
C6Asset & Icon QualityReadyNo icons or raster assets. Pure layout + shadow.
C7Code Connect LinkabilityNot ApplicableNo native component to map to. Consolidate into Dropdown instead.
Code Connect
AspectStatusNotes
Property namingN/ANo properties exist on the component.
Slot coverageMissingNo items slot — blocks any meaningful mapping.
Native component fileN/AHandled by Menu/DropdownMenu. No dedicated EB component required.
RecommendationConsolidateFold into Dropdown's Expanded variant or convert to a slot-based Dropdown Menu before mapping.
Variants Inventory (1 total)

Single variant, no property axes.

VariantWidthItem CountNode ID
Default366px8 (7 instances + 1 detached frame)6383:3446
1.0.0 — April 2026 Initial
Initial Assessment · node 6383:3446
Component assessed — Single variant, 8-item fixed layout at 366px. Rounded 6px card, white bg, 6px/12px drop shadow. All tokens bound (bg, radius, shadow, space). Documented
Initial
Last row is a detached frame — Node 6383:3442 is a hand-built Dropdown - Item frame instead of a DropdownItem component instance. Breaks consistency. Open
C1 Open
No slot, no item count, no fill-container width — Layout is hardcoded to 8 rows at 366px. Cannot be reused for menus of different sizes. Open
C1 Open
Irregular component name — "Dropdown Item - Group" uses a " - " separator inconsistent with other DS group names. Recommend rename to "Dropdown Menu". Open
C2 Open
Popover surface not a native primitive — Both Menu (iOS) and DropdownMenu (Compose) draw the shadowed card automatically. This component has no 1:1 native mapping. Open
C4 Open
Code Connect not registered — No properties to map. Consolidate into Dropdown before mapping. Open
C7 Open
Dropdown Item FixNeeds RefinementComponent link

The row primitive used inside Dropdown and Dropdown Item Group. 9 variants across type (text / amount / country / text with tag / disabeld) × selected (true/false). Carries the label, optional badge tag, peso amount, or country flag + dial code, plus a bottom divider. Selected state uses brand blue for the label; disabled uses a soft fill and muted label.

Fix required before handoff
Enum value typo disabeld (C2). Country variant uses a raster PNG flag (C6). No pressed/focused state variants (C5). Disabled is modeled as a type value rather than an orthogonal state (C4).
In Context

Dropdown Item is the row primitive consumed by the Dropdown overlay and by Dropdown Item Group. Not used standalone.

Live Preview

Toggle type and selected to see the row update in real time.

Properties
type
selected
DS Health
Reusable
Pass
Single row primitive that powers every Dropdown overlay and Dropdown Item Group. Covers plain text, tag labeled, amount, country, and disabled content types from one component.
Self-contained
Pass
Ships its own divider, padding, label styling, optional badge slot, peso sign, and flag slot. Tokens cover default, active, and disabled roles via main/dropdown-item/color/*.
Consistent
Warn
Enum value disabeld is misspelled and ships in the generated TypeScript type. Disabled is modeled as a type rather than an orthogonal state, so you cannot combine it with text / amount / country cleanly.
Composable
Partial
Fits inside Dropdown overlay and Dropdown Item Group. The country variant hardcodes the Philippines flag as a raster image rather than a swappable flag slot, which blocks reuse for any other locale.
Behavior
StateiOSAndroidFigma PropertyNotes
Default (unselected)YesYesselected=falseLabel #0A2757, bottom border #E5EBF4.
SelectedYesYesselected=trueLabel #005CE5 (brand), same divider. No background highlight — relies on text color alone.
DisabledYesYestype=disabeldSoft fill #F6F9FD, label #C2CFE5. Modeled as a content type rather than a state (C4).
PressedNoNoNot defined. iOS highlight and Android ripple will have to be improvised at instance level.
FocusedNoNoNot defined. Required for keyboard / D-pad navigation in dropdown overlays.
Open Issues
  • Enum value disabeld is misspelled. The variant name ships into the generated TS type (type?: "text" | "amount" | "country" | "text with tag" | "disabeld"). Every consumer has to mirror the typo or lose autocomplete. Rename to disabled. C2 · Variant & Property Naming
  • Country flag is a raster PNG. The country variant embeds a Philippines image (imgPhilippines) as a raster fill, not a vector flag instance. Blocks clean native handoff and freezes the row to a single locale. C6 · Asset & Icon Quality
  • No pressed or focused state variants. Only selected on/off plus a pseudo-disabled type. Touch feedback (iOS highlight, Android ripple) and keyboard focus are not covered at the DS layer. C5 · Interaction State Coverage
  • Disabled is modeled as a type value. It collides with content types (text, amount, country) — you can't express "amount + disabled" or "country + disabled" in the current schema. Should be an orthogonal state / disabled axis. C4 · Native Mappability
  • No selected-visual affordance beyond label color. Selected state only changes label hex (#0A2757 → #005CE5). A checkmark trailing slot or background fill would make the picked item unambiguous, especially for color-blind users. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked by the typo, missing states, and raster asset above. C7 · Code Connect Linkability
Design Recommendations
  • Rename disabeld enum value to disabled. Zero visual change, fixes the type surface, and unblocks Code Connect naming hygiene. Rename
  • Promote disabled to its own axis. Split the current 5-value type into two props: type (text / text with tag / amount / country) × disabled (true / false). Matches how every native primitive models enabled/disabled and collapses the matrix to 4 × 2 × 2 (selected)=16 compositional variants with clean semantics. Property
  • Replace the raster Philippines flag with a vector flag slot. Introduce a leadingAsset slot (or `flag` slot) that accepts a vector flag instance. Current PNG blocks reuse for any non-PH locale and ships a raster asset to native. Slot
  • Add pressed and focused state variants. Define a state axis so iOS highlight and Android ripple map to tokenized backgrounds instead of being improvised at instance level. State
  • Add an explicit selected-visual affordance. A trailing checkmark (or tokenized background fill) on selected=true removes the reliance on label color alone — improves accessibility and scannability. State
  • Generalize the amount variant around a trailing value slot. Instead of a baked-in peso + amount text, expose a trailing content slot that the peso sign and amount compose into. Opens the row to reuse for any key/value pair (balance, fee, exchange rate). Slot
  • Register Code Connect mapping to EBDropdownItem. After the rename and state work, wire the Figma properties 1:1 to the SwiftUI / Compose API. Docs
Variants

9 variants across type (text / amount / country / text with tag / disabeld) × selected (true/false). Note: disabeld only ships with selected=false, so the matrix is 4 × 2 + 1=9.

Text

Plain text row. Default content type used by Dropdown. Label switches from neutral #0A2757 (default) to brand #005CE5 (selected).

Properties
typetext
selected
Text with tag

Row with a trailing Badge instance (Negative/Heavy variant in stock). Used when an option needs an inline status label.

Properties
typetext with tag
selected
Amount

Peso sign (vector, Proxima-sized) + amount text. Icon currency token flips to brand on selected.

Properties
typeamount
selected
Country

Leading flag (25 × 16, 2px radius) + country name and dial code. Flag is a raster PNG, not a vector instance — open issue (C6).

Properties
typecountry
selected
Disabeld Typo

Soft fill row with muted label. Currently only exists at selected=false. Enum value is misspelled (disabeld) — open issue (C2).

Properties
typedisabeld
selectedfalse
Colors by State

All color roles are bound to the main/dropdown-item/color/* token family. Dropdown Item has no variable modes — colors are keyed by state only.

ROLETOKENDEFAULTSELECTEDDISABLED
Row bgmain/dropdown-item/color/{state}/bgtransparenttransparent#F6F9FD
Labelmain/dropdown-item/color/{state}/label#0A2757#005CE5#C2CFE5
Bottom bordermain/dropdown-item/color/{state}/border#E5EBF4#E5EBF4#E5EBF4
Peso sign (amount)main/dropdown-item/color/{state}/icon-currency#0A2757#005CE5
Badge bg (text with tag)main/badge/negative/heavy/background#D61B2C#D61B2C
Badge label (text with tag)main/badge/negative/heavy/label#FFFFFF#FFFFFF
Layout
PropertyValueToken
Row width366px (fill)
Row height50px (text / text with tag) · 51.2–52px (amount / country)
Padding top/bottom16pxspace/space-16
Padding left12pxspace/space-12
Padding right16pxspace/space-16
Gap (country / text with tag)8pxspace/space-8
Flag size (country)25 × 16
Flag radius2pxradius/radius-1 (approx)
Peso sign size (amount)18 × 18
Bottom border1px solidmain/dropdown-item/color/{state}/border
Row corner radius0radius/radius-0
Typography
LayerText StyleFontSizeTrackingLine-height
Label (all types)Primary/Label/Light/LargeProxima Soft Semibold18px0.25px18px
Amount textPrimary/Label/Light/LargeProxima Soft Semibold18px0.25px18px
Badge label (text with tag)Primary/Label/FineProxima Soft Bold12px0.5px12px
Installation Planned API

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

Dropdown Item is bundled with the Dropdown package. The planned SwiftUI API exposes it as EBDropdownItem; on Compose it maps to Material 3's DropdownMenuItem with EB-styled content.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
type=textEBDropdownItem(label:)DropdownMenuItem(text={ Text(label) })Default content type
type=text with tagEBDropdownItem(label:tag:)trailing={ EBBadge(…) }Trailing Badge slot (Negative/Heavy in stock)
type=amount.ebStyle(.amount)style=EBDropdownItemStyle.AmountLeading peso sign + trailing amount text
type=countryEBDropdownItem(flag:name:dialCode:)leadingIcon={ FlagIcon(…) }Flag must be a vector instance — raster PNG is an open issue (C6)
type=disabeld Typo.disabled(true)enabled=falseShould be an orthogonal disabled prop, not a type value (C4)
selected=trueisSelected: Boolselected: BooleanSwitches label + icon-currency token to active
selected=falseisSelected: Bool (default)selected: Boolean (default)Default unselected state
SwiftUI
ios/Components/Dropdown/EBDropdownItem.swift
Jetpack Compose
android/components/dropdown/EBDropdownItem.kt
Usage Snippets Planned API
Text
EBDropdownItem("Dropdown Item")
    .isSelected(category == "item")
    .onTap { category = "item" }
EBDropdownItem(
    label = "Dropdown Item",
    selected = selected == "item",
    onClick = { selected = "item" }
)
Text with tag
EBDropdownItem("Dropdown Item") {
    EBBadge("Label", level: .heavy, state: .negative)
}
EBDropdownItem(
    label = "Dropdown Item",
    trailing = { EBBadge("Label", level = EBBadgeLevel.Heavy, state = EBBadgeState.Negative) }
)
Amount
EBDropdownItem(amount: "1,000.00")
    .ebStyle(.amount)
EBDropdownItem(
    label = "1,000.00",
    style = EBDropdownItemStyle.Amount
)
Country
EBDropdownItem(
    flag: Image("flag_ph"),
    name: "Philippines",
    dialCode: "+63"
)
EBDropdownItem(
    label = "Philippines +63",
    leadingIcon = { FlagIcon(CountryCode.PH) }
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 × 44 pt (row is 50pt tall)48 × 48 dp
Accessibility label.accessibilityLabel("Philippines, +63")contentDescription="Philippines, +63"
Role.accessibilityAddTraits(.isButton)Role.Button
Selected announcement.accessibilityAddTraits(.isSelected)selected=true in semantics
Disabled announcement.disabled(true) → VoiceOver says "dimmed"enabled=false → TalkBack says "disabled"
Usage Guidelines

Do

Consume Dropdown Item through Dropdown or Dropdown Item Group. Keep labels short — 18px Semibold is the only supported text size.

Don't

Don't use Dropdown Item outside a dropdown overlay. For standalone list rows, use the List Item component instead.

Do

Pair selected state with a trailing checkmark (once added) so the picked item is unambiguous, especially on the country and amount variants.

Don't

Don't rely on the disabeld type to build disabled versions of amount or country — it only ships for the text content type. Use the planned disabled state axis instead.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: container, name, offset, Peso Sign - Proxima, Field Trailing Flag, philippines.
C2Variant & Property NamingNeeds FixEnum value disabeld is misspelled and ships into the generated TS type.
C3Token CoverageReadyAll colors bound to main/dropdown-item/color/*; space + radius + typography tokens all present.
C4Native MappabilityPartialMaps to a custom SwiftUI row / Material 3 DropdownMenuItem. Disabled should be an orthogonal prop, not a type value.
C5Interaction State CoverageNeeds FixNo pressed or focused variants. Selected state relies on label color alone — no checkmark or background fill.
C6Asset & Icon QualityNeeds FixCountry variant uses a raster PNG flag, not a vector instance. Peso sign is a vector via Peso Sign - Proxima.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet; blocked by C2, C5, C6.
Code Connect
AspectStatusNotes
Property namingBlockedRename disabelddisabled before registering
Asset qualityBlockedReplace raster PH flag with vector flag slot
State coverageBlockedPressed / focused variants missing
Native component filePendingEBDropdownItem.swift / EBDropdownItem.kt not yet created
Variants Inventory (9 total)

5 type values × 2 selected values=10 theoretical slots, but disabeld only ships with selected=false, giving 9 actual variants.

typeselectedNode IDNotes
textfalse23:199454Neutral label
texttrue23:199456Brand label
text with tagfalse883:29328Trailing Badge instance
text with tagtrue883:30370Brand label + Badge
amountfalse23:199458Peso sign + "X,XXX.XX"
amounttrue23:199465Brand peso + brand label
countryfalse23:199472Raster flag + "Philippines +63"
countrytrue23:199476Brand "Philippines +63"
disabeld Typofalse883:30386Soft fill + muted label
1.0.0 — April 2026 Initial
Initial Assessment · node 18577:13033
Component assessed — 9 variants documented across type (text / amount / country / text with tag / disabeld) × selected (true/false). Row primitive for Dropdown overlay. Documented
Initial
Enum value disabeld misspelled — ships into the generated TS type. Rename to disabled. Open
C2 Open
Country variant uses a raster PNG flag — Philippines image is bitmap, not a vector flag instance. Blocks reuse and native handoff. Open
C6 Open
No pressed or focused state variants — only selected on/off and a pseudo-disabled content type. Touch/keyboard feedback unmodeled. Open
C5 Open
Disabled modeled as type value — collides with content types (text / amount / country). Should be an orthogonal disabled axis. Open
C4 Open
Code Connect mappings — no CLI mappings registered yet; blocked by C2, C5, C6. Open
C7 Open
Dropdown FixNeeds RefinementComponent link

A 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.

Fix required before handoff
Missing disabled and pressed states (C5). DropdownItem selected uses yes/no instead of true/false (C2). Amount variant Peso Sign uses BOOLEAN_OPERATION (C6).
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Select Option
Live Preview

Toggle variant and type to see the dropdown update in real time.

Properties
variant
type
DS Health
Reusable
Partial
Text and Error variants are generic dropdown patterns usable across many flows. Amount (peso-specific) and Mobile (phone input) variants are product-specific to GCash and limit cross-context reuse.
Self-contained
Pass
Bundles trigger field, chevron affordance, dropdown list overlay with shadow, and subtext slot. All visual states (collapsed/expanded/error) are self-contained with proper token bindings.
Consistent
Partial
DropdownItem 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.
Composable
Partial
Text/Error/Amount variants compose well with form layouts. Mobile variant bundles too many concerns (dropdown + phone input) — should be separated into discrete components. Trigger field is not a swappable slot.
Behavior
StateiOSAndroidFigma PropertyNotes
Collapsed (Default)YesYestype=CollapsedGray #D7E0EF border, white bg. Chevron down.
Expanded (Active)YesYestype=ExpandedBlue #005CE5 border, chevron up. Dropdown list overlay with shadow.
ErrorYesYesvariant=ErrorRed border — weak #F4C7C9 (collapsed), strong #D61B2C (expanded).
DisabledNoNoNot defined. Required for form accessibility.
PressedNoNoNot defined. Touch feedback expected on mobile.
Open Issues
  • DropdownItem selected uses yes/no strings. Should be true/false for direct Swift Bool / Kotlin Boolean mapping in Code Connect. C2 · Variant & Property Naming
  • No disabled state. Form dropdowns must support a non-interactive state for accessibility and conditional form flows (e.g. "Country" disabled until "Region" is picked). C5 · Interaction State Coverage
  • No pressed state. Touch feedback is expected on both platforms — iOS highlight, Android ripple. Currently consumers improvise this. C5 · Interaction State Coverage
  • Peso Sign uses a shape_full BOOLEAN_OPERATION. Not a clean vector path — renders inconsistently across SVG export and native platforms. Flatten to a single path. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until the boolean and state issues above are resolved. C7 · Code Connect Linkability
Design Recommendations
  • Add a Disabled state to the variant matrix. Required for form accessibility and conditional logic — without it, consumers hack opacity filters on the parent frame. State
  • Rename type to isExpanded. Avoids platform keyword conflicts (type is reserved in many languages) and aligns with boolean naming convention. Rename
  • Rename DropdownItem selected values to true/false. Direct boolean mapping eliminates the string-to-bool conversion layer when wiring Code Connect. Rename
  • Extract Mobile variant into a dedicated CountryCodeDropdown. It bundles too many concerns (dropdown + phone input) for a generic dropdown and forces the base component to carry country-specific logic. Family
  • Add a selected-visual state to DropdownItem. Checkmark or background highlight makes the current selection unambiguous — today the open menu looks the same whether an item is picked or not. State
Variants

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.

Text

Default text dropdown. Label header, select trigger with placeholder text and chevron, optional subtext. Used for general-purpose list selection.

Properties
variantText
type
Error

Error state dropdown with red border. Collapsed uses weak border (#F4C7C9), expanded uses strong border (#D61B2C). Subtext turns red for error messaging.

Properties
variantError
type
Amount

Amount selection with peso sign prefix. Same trigger structure as Text but with a currency indicator for monetary value selection.

Properties
variantAmount
type
Mobile

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.

Properties
variantMobile
type
Colors by State

Trigger field and dropdown list colors. Border color is the primary state indicator. Error variant uses distinct border tokens.

RoleTokenDEFAULTACTIVEERROR (collapsed)ERROR (expanded)
Trigger borderselected-field/color/{state}/border#D7E0EF#005CE5#F4C7C9#D61B2C
Trigger bgselected-field/color/{state}/bg#FFFFFF#FFFFFF#FFFFFF#FFFFFF
Placeholderselected-field/color/{state}/placeholder#90A8D0#90A8D0#90A8D0#90A8D0
Chevron iconselected-field/color/{state}/icon#005CE5#005CE5#005CE5#005CE5
Peso sign (Amount)selected-field/color/{state}/icon-currency#183462#183462
Header labelformgroup-header/color/label#0A2757#0A2757#0A2757#0A2757
Item labeldropdown-item/color/default/label#0A2757#0A2757
Item borderdropdown-item/color/default/border#E5EBF4#E5EBF4
Dropdown bgbg/color-bg-main#FFFFFF#FFFFFF
Subtexttext/color-text-weak#445C85#445C85
Error subtextborder/color-border-destructive#D61B2C#D61B2C
Layout
PropertyValue
Trigger height46px
Corner radius6px (radius-2)
Trigger padding6px top, 8px bottom, 12px horizontal
Chevron size32 × 32
Peso sign size (Amount)15 × 15
Item padding16px vertical, 12px left, 16px right
Dropdown corner radius6px
Dropdown shadow0 6px 12px rgba(2,14,34,0.16)
Header padding bottom8px
Typography
LayerText StyleFontSizeTrackingLine-height
Header labelPrimary/Label/Light/SmallHeyMeow Rnd Semibold14px0.25px14px
Trigger placeholderPrimary/Label/Light/SmallHeyMeow Rnd Semibold14px0.25px14px
Dropdown itemPrimary/Label/Light/LargeHeyMeow Rnd Semibold18px0.25px18px
SubtextBarkAda Semibold12px0px18px
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
variant=TextEBDropdown(label:items:)EBDropdown(label, items)Default text selection
variant=Error.ebError(true)isError=trueValidation failed state
variant=Amount.ebStyle(.amount)style=EBDropdownStyle.AmountShows peso sign prefix
variant=Mobile.ebStyle(.mobile)style=EBDropdownStyle.MobileCountry code + phone input
type=CollapsedDefault closed state (managed internally)
type=ExpandedisPresented: Binding<Bool>expanded: BooleanDropdown list visible
subtext (boolean)helperText: String?helperText: String?Optional helper/error text
SwiftUI
ios/Components/Dropdown/EBDropdown.swift
Jetpack Compose
android/components/dropdown/EBDropdown.kt
Usage Snippets Planned API
Text (Default)
EBDropdown("Category", selection: $category) {
    ForEach(categories) { item in
        Text(item.name)
    }
}
EBDropdown(
    label = "Category",
    items = categories,
    selectedItem = selected,
    onItemSelected = { selected = it }
)
Error
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"
)
Amount
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
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 × 44 pt48 × 48 dp
Accessibility label.accessibilityLabel("Select category")contentDescription
Role.accessibilityAddTraits(.isButton)semantics { role=Role.DropdownList }
Expanded stateVoiceOver: "collapsed" / "expanded"TalkBack: announce expansion state
Item selection.accessibilityValue(selectedItem)semantics { stateDescription }
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: label, container, text-container, peso-sign, Chevron Up/Down. Minor: dropdowncontainer missing separator.
C2Variant & Property NamingPartialDropdownItem selected uses yes/no instead of true/false. type is a generic property name.
C3Token CoverageReadyAll colors bound to design tokens. Space, radius, typography, and elevation tokens all present.
C4Native MappabilityPartialText/Error/Amount map to Menu (iOS) / ExposedDropdownMenuBox (Android). Mobile variant needs custom composition.
C5Interaction State CoverageNeeds FixMissing disabled and pressed states. Only Collapsed, Expanded, and Error defined.
C6Asset & Icon QualityPartialChevrons are vector instances. Amount variant Peso Sign uses BOOLEAN_OPERATION (shape_full).
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Code Connect
AspectStatusNotes
Property namingPartialDropdownItem selected needs boolean rename; type is generic
Asset qualityPartialPeso Sign BOOLEAN_OPERATION in Amount variant
State coverageBlockedMissing disabled/pressed states blocks complete mapping
Native component filePendingEBDropdown.swift / EBDropdown.kt not yet created
Variants Inventory (8 total)

4 variant values × 2 type values (Collapsed/Expanded). subtext boolean toggleable on all variants.

varianttypeNode ID
TextCollapsed18482:31966
TextExpanded18482:31960
ErrorCollapsed18482:31955
ErrorExpanded18482:31949
AmountCollapsed18482:31944
AmountExpanded18482:31938
MobileCollapsed18482:31911
MobileExpanded18482:31924
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:31910
Component assessed — 8 variants documented across variant (Text/Error/Amount/Mobile) × type (Collapsed/Expanded). Generic dropdown with trigger field, chevron affordance, and overlay item list. Documented
Initial
DropdownItem selected uses yes/noselected=yes/no instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Open
C2 Open
Missing disabled and pressed states — Only Collapsed, Expanded, and Error states defined. No disabled state for non-interactive forms, no pressed state for touch feedback. Open
C5 Open
Amount variant Peso Sign uses BOOLEAN_OPERATIONshape_full is a BOOLEAN_OPERATION, not a clean vector path. May render inconsistently on native platforms. Open
C6 Open
Code Connect mappings — No CLI mappings registered yet. Open
C7 Open
Generic Card FixRefineComponent link

A tappable list-row card used for product/service entries — 360×146 fixed container with a leading icon slot, a content block (blurb + tag pill, heading, 1–2 description lines, optional bottom badge), and a trailing chevron. 12 Figma variants across iconSize (64/52/46/40/32/24) × state (Default / skeleton), plus 6 orthogonal boolean slot toggles: hasBlurb, hasTag, hasSubtitle, has2ndDescription, hasBadge, hasChevron.

Fix — collapse iconSize, swap placeholders to slots, ship the pressed state
Rename iconSize to semantic values (XL / L / M / S). Replace the icon placeholder with a swappable Avatar / Icon slot via instance swap. Replace the raster chevron with a vector. Add a pressed state — this is clearly a tappable row but only Default + skeleton are modeled today.
In Context

Generic Card stacks vertically into a scrolling list — product catalogs, service menus, transaction history detail screens. Icon size tightens as the density of the list increases.

Live Preview

Type your own copy to test flexibility. Toggle slots individually to see how the card responds when pieces are hidden. Flip state between Default and skeleton to see the loading pattern.

Content
blurb
tag
heading
description
badge
Properties
iconSize
state
Slots
hasSubtitle
hasBlurb
hasTag
has2ndDesc
hasBadge
hasChevron
DS Health
Reusable
Pass
Solid general-purpose list-row card. Works across catalogs, services, transaction history, and detail-screen rows.
Self-contained
Partial
Owns colors, typography, spacing tokens. But the icon is drawn (placeholder circle) not instanced, and the chevron ships as a raster.
Consistent
Partial
6 numeric iconSize values is a lot. Tag uses Badge (good) and bottom pill uses Badge (good) — composition is correct where it happens.
Composable
Pass
Stacks cleanly into a scrollable list. Skeleton state is a first-class variant — rare and worth highlighting as a DS convention.
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYesstate=DefaultNormal row — all content visible, chevron shown when hasChevron.
Skeleton (loading)YesYesstate=skeletonLoading pattern — gray rounded placeholders where each content slot would render. Kudos for shipping this as a first-class variant.
PressedMissingMissingNot builtTappable row (has chevron) — needs a pressed state with background tint for tap feedback.
DisabledMissingMissingNot builtFor temporarily-unavailable services (e.g. maintenance). Typically dimmed label + muted icon.
Open Issues
  • iconSize uses 6 numeric values.64 / 52 / 46 / 40 / 32 / 24 is too granular — consumers can't tell when to pick 46 vs 40. Collapse to 3–4 semantic sizes (XL / L / M / S) with fixed pixel values behind the scenes. C2 · Variant & Property Naming
  • Icon slot is a hardcoded placeholder circle. Not an Avatar or Icon instance — blocks swappable composition. Designers can't drop in a brand icon, illustration, or Avatar without detaching. C6 · Asset & Icon Quality
  • Chevron is a raster image. Ships as a PNG (shape_full) rather than a vector glyph — blurs at large render sizes and blocks token-based tinting. C6 · Asset & Icon Quality
  • No pressed / disabled states. Has a chevron, so clearly tappable — but only Default + skeleton states are built. Pressed tint and disabled appearance are standard row affordances. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until iconSize collapse + icon-slot adoption land. C7 · Code Connect Linkability
Design Recommendations
  • Collapse iconSize to semantic sizes.xl (64) / l (52) / m (40) / s (32) — drops 6 numeric values to 4 meaningful ones. (46 and 24 can be retired or mapped to the nearest semantic size.) Matches how Avatar sizes are named elsewhere. Rename
  • Replace the icon placeholder with a Figma Slot. Accept an Avatar instance (for person/brand rows) or an Icon instance (for service rows). Native maps to @ViewBuilder (SwiftUI) or @Composable slot (Compose) via Code Connect. Slot
  • Convert the chevron to a vector. Replace the raster shape_full with a vector path — token-bindable color and crisp at any scale. Asset
  • Add pressed + disabled states. Pressed: subtle bg tint on the whole row. Disabled: muted label + icon opacity. Rows are tappable and need both. State
  • Document the skeleton pattern as a DS convention. Generic Card's skeleton variant is exactly the pattern other row/card components should follow. Call it out in the guidelines so the same loading treatment spreads consistently. Docs
  • See sibling:Generic Transaction Card. Similar "card row" primitive — the two could share a common schema once both are cleaned up. Family
Default — iconSize=64 node 18482:35807

Full-featured row: icon + blurb with tag, heading, 2 description lines, bottom badge, chevron.

Layout
Width × Height360 × 146
Padding16 24 16 12
Gap (icon ↔ content)24
Gap (content ↔ chevron)24
Bottom border1 px
Icon size64 × 64
Chevron size32 × 32
Colors by State
ROLETOKENVALUE
Surfacecard-list/bg#FFFFFF
Bottom bordercard-list/border#E5EBF4
Headingcard-list/label-header#0A2757
Blurbcard-list/label-blurb#005CE5 @ 90%
Description labelcard-list/label#90A8D0
Description valuecard-list/description#445C85
Icon placeholder(not tokenized)#C2C6CF
Typography
HeadingHeyMeow Rnd Bold · 18 / 23 · +0.25
BlurbHeyMeow Rnd Bold · 14 / 14 · +0.25
DescriptionBarkAda Semibold · 12 / 18 · +0
Tag labelHeyMeow Rnd Bold · 12 / 12 · +0.5
Badge labelHeyMeow Rnd Bold · 12 / 12 · +0.5
Composed sub-components
TagBadge · Negative · Heavy
Bottom pillBadge · Information · Light
Icon (today)Drawn placeholder
Icon (proposed)Avatar / Icon slot
Skeleton — loading state node 18482:35832

The loading pattern for the card. Every content slot becomes a rounded rectangle placeholder in neutral gray. Use while awaiting data.

Placeholder color
Fill#E0E6F2
Radius6 (rects) · 50% (icon circle)
Best practice
AnimationOptional shimmer sweep, 1.2 s ease-in-out
TransitionFade skeleton → content on load
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBGenericCard
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
iconSize: 64 | 52 | 46 | 40 | 32 | 24size: xl | l | m | s.controlSize(.large) etc.size: EBGenericCardSize
(drawn circle)leadingMedia: Avatar | Icon (slot)leadingMedia: EBLeadingMedia?leadingMedia: @Composable (() -> Unit)?
(hardcoded text)heading: Stringheading: Stringheading: String
hasBlurbblurb?: Stringblurb: String?blurb: String?
hasTagtag?: Badge (instance)tag: EBBadge?tag: @Composable (() -> Unit)?
hasSubtitle(derived: shown if blurb or tag present)
(hardcoded "Description goes here")descriptions: [LabelValue]descriptions: [EBLabelValue]descriptions: List<EBLabelValue>
has2ndDescription(derived: up to N rows rendered)
hasBadgebadge?: Badge (instance)badge: EBBadge?badge: @Composable (() -> Unit)?
hasChevronshowChevron: Bool=trueshowChevron: Bool=trueshowChevron: Boolean=true
state: Default | skeletonloading: Boolloading: Boolloading: Boolean
(not modeled)onTap?: () -> VoidonTap: (() -> Void)?onClick: (() -> Unit)?
Suggested file paths
  • ios/Components/GenericCard/EBGenericCard.swift
  • android/components/genericcard/EBGenericCard.kt
Usage
// Full-featured row
EBGenericCard(
    leadingMedia: .icon(Image("send-money")),
    blurb: "PROMO",
    tag: EBBadge("New", state: .negative),
    heading: "Send Money Abroad",
    descriptions: [
        .init(label: "Fee:", value: "Free for first transfer"),
        .init(label: "Delivery:", value: "Same day")
    ],
    badge: EBBadge("Recommended", state: .information, level: .light)
) {
    onRowTap()
}
.controlSize(.large)

// Loading state
EBGenericCard.skeleton(size: .large)
// Full-featured row
EBGenericCard(
    leadingMedia = { EBIcon(EBIcons.SendMoney) },
    blurb = "PROMO",
    tag = { EBBadge("New", state = EBBadgeState.Negative) },
    heading = "Send Money Abroad",
    descriptions = listOf(
        EBLabelValue("Fee:", "Free for first transfer"),
        EBLabelValue("Delivery:", "Same day")
    ),
    badge = { EBBadge("Recommended", state = EBBadgeState.Information, level = EBBadgeLevel.Light) },
    size = EBGenericCardSize.Large,
    onClick = { onRowTap() }
)

// Loading state
EBGenericCard.Skeleton(size = EBGenericCardSize.Large)
Accessibility
RequirementiOSAndroid
Row as a buttonWhole row wrapped in Button with combined accessibilityLabel (heading + blurb + tag).Modifier.clickable { onTap() }.semantics(mergeDescendants=true) on the row.
Combined announcement"Send Money Abroad, PROMO, New, Free for first transfer, Same day, Recommended" — VoiceOver reads top-to-bottom.Same reading order — TalkBack follows composition.
Loading stateAnnounce "Loading" once on mount; suppress per-placeholder announcements.Apply contentDescription="Loading" to the skeleton container.
Min touch target146 px row height ≫ 44 pt ✓146 dp ≫ 48 dp ✓
Usage Guidelines
Do
  • Use for navigational lists of services, products, or detail entries.
  • Stack vertically into a scrollable list — don't put side-by-side.
  • Show the skeleton variant while data loads — never blank rows.
  • Match iconSize to list density (XL for top-level, S for dense sub-lists).
Don't
  • Don't use for transaction history rows — use Generic Transaction Card.
  • Don't overload the blurb + tag combo — one accent is enough per row.
  • Don't hide the chevron if the row is tappable — the affordance is the point.
  • Don't mix iconSizes in the same list.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyClean container / content / chevron hierarchy. Tag and bottom pill are Badge instances.
C2Variant & Property NamingNeeds Refinement6 numeric iconSize values — collapse to semantic scale.
C3Token CoverageReadyAll colors bound to main/card-list/color/* + Badge tokens.
C4Native MappabilityNeeds RefinementMaps cleanly to a row composable once icon slot + chevron are fixed.
C5Interaction State CoverageNeeds RefinementDefault + skeleton built. Missing pressed + disabled for a tappable row.
C6Asset & Icon QualityNeeds RefinementIcon placeholder isn't an instance; chevron is a raster.
C7Code Connect LinkabilityNot MappedBlocked until iconSize rename and icon slot adoption land.
Variants Inventory (12 total)

iconSize (6) × state (2)=12 variants. The 6 boolean slot props (hasBlurb, hasTag, hasSubtitle, has2ndDescription, hasBadge, hasChevron) toggle content independently and don't multiply the variant count.

iconSizeDefault nodeSkeleton nodeDimensions
6418482:3580718482:35832360 × 146
5218482:3584318482:35868360 × 146
4618482:3587918482:35904360 × 146
4018482:3591518482:35940360 × 146
3218482:3595118482:35976360 × 146
2418482:3598718482:36012360 × 146
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:35806
Verdict: Fix — Collapse iconSize to semantic scale, swap icon placeholder for a slot, vectorize the chevron, add pressed state. Open
Schema
Skeleton pattern acknowledged — First-class loading variant is rare and valuable. Adopt this pattern across the card/row family. Noted
Praise
C2 — iconSize collapse — 6 numeric values → 4 semantic sizes (xl/l/m/s). Open
C2
C6 — Icon slot + vector chevron — Adopt Figma Slot for leading media; vectorize chevron. Open
C6
C5 — Pressed / disabled — Tappable row needs both. Open
C5
C7 — Code Connect — Blocked on above. Open
C7
Generic Transaction Card RestructureRefineComponent link

A compact transaction-history row with a label, date/time metadata (plus an optional badge tag), and trailing content (amount, menu button, or a reference number). 5 variants under a single type enum: default, more information, with avatar, no amount, skeleton loader. Used inside GCash Jr. transaction lists and Detailed Transaction History screens.

Restructure — type enum hides 5 different layouts
The five type values are visually distinct layouts, not variants of the same pattern. Replace the enum with slot-based composition (leadingMedia?, badge?, trailing=amount | menu | reference, loading). Same fix pattern as Alert's Full Width. Also align heading weight with Generic Card (both should use Bold, not Semibold).
In Context

Transaction-history rows stack vertically in the Activity / Transactions screen. Different rows use different variants depending on the context (recipient avatar, reference number, action menu).

Live Preview

Flip type to swap between the 5 layouts. Content inputs stress-test flexibility. Notice how different types expose different slots — today Figma encodes this as a single enum; the proposal is to expose slots directly.

Content
label
badge
date / meta
amount
reference
avatar initials
Properties
type
DS Health
Reusable
Pass
Solid transaction-row primitive — covers the main patterns seen in Activity screens.
Self-contained
Pass
Owns tokens, composes Avatar + Badge correctly. Good structure internally.
Consistent
Warn
5 structurally different layouts hidden behind one type enum. Heading uses Semibold 600 while Generic Card uses Bold 700 — inconsistent across the card family.
Composable
Partial
Today consumers must pick a type and live with its fixed slot composition. A slot-based API would let them mix freely (e.g. avatar + reference).
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYestype=defaultLabel + badge + date + amount. The baseline transaction row.
With avatarYesYestype=with avatarAdds a 32 × 32 Avatar at the leading edge. Used for person-to-person transactions.
More informationYesYestype=more informationReplaces the badge with an overflow menu button (⋯). Used when a row has context-menu actions.
No amountYesYestype=no amountSwaps the amount for a trailing badge; swaps date for a reference number. Used for confirmations without monetary value.
Skeleton loaderYesYestype=skeleton loaderLoading placeholder pattern. Worth documenting alongside Generic Card's skeleton as a DS-wide convention.
PressedMissingMissingNot builtRows typically drill into transaction detail — need pressed tint for tap feedback.
Open Issues
  • type enum hides 5 structurally different layouts. Same anti-pattern as Alert's Full Width boolean. default / more information / with avatar / no amount are not variants of one pattern — they're four different slot compositions. Replace with a slot-based API (leadingMedia, badge, trailing, loading). C1 · Layer Structure & Naming
  • no amount and more information describe absence, not role. Value names should describe what the variant IS, not what it lacks. no amount becomes "swap amount for a trailing badge"; more information becomes "show action menu." Once slot-based, the enum disappears entirely. C2 · Variant & Property Naming
  • Heading uses Semibold (600) while Generic Card uses Bold (700). Inconsistent title weight across the card family. Standardize — either both Bold or both Semibold. C2 · Variant & Property Naming
  • No pressed / disabled states. Transaction rows drill into a detail screen on tap — need pressed tint. Also disabled state for pending/failed transactions that can't be reopened. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked on the slot restructure. C7 · Code Connect Linkability
Design Recommendations
  • Replace type with slot-based props.leadingMedia?: Avatar | Icon | none, badge?: Badge, metadata: String, trailing: amount | menu | badge | reference, loading: Bool. Eliminates the 5-type union and lets consumers compose any valid row without editing the master. Property
  • Align heading weight with Generic Card. Pick one (Bold 700 is more common across the DS) and apply it to both components. Card family should read as one system, not two dialects. Family
  • Promote skeleton to a cross-family convention. Generic Card + Generic Transaction Card both ship skeletons — document the pattern (#E0E6F2 fill, rounded rect placeholders, no spinner) as the DS loading standard so future card/row primitives follow the same treatment. Docs
  • Add pressed + disabled states. Pressed: subtle bg tint across the whole row. Disabled: muted label + amount opacity. State
  • Reconcile with Generic Card. The two share ~80 % of the "row with leading / trailing / meta" shape. Consider a shared EBRow primitive with variants for "with subtitle" (Generic Card) and "with metadata + amount" (Generic Transaction Card). Family
Default — label + badge + date + amount node 18482:35754

The baseline transaction row. Leading label, mid-row badge + date metadata, trailing amount. 78 px tall.

Layout
Width360
Padding16 24 18 22
Content gap6
Meta-row gap8
Bottom border1 px
Colors by State
ROLETOKENVALUE
Surfacecard-list/bg#FFFFFF
Bottom bordercard-list/border#E5EBF4
Label / Amountcard-list/label-header#0A2757
Metadatacard-list/label-metadata#6780A9
Badge bgbadge/information/light/bg#E5F1FF
Badge labelbadge/information/light/label#005CE5
Typography
Label (title)HeyMeow Rnd Semibold · 18 / 18 · +0.25
AmountHeyMeow Rnd Semibold · 18 / 18 · +0.25
Metadata (date)BarkAda Semibold · 12 / 18 · +0
Badge labelHeyMeow Rnd Bold · 12 / 12 · +0.5
Composed sub-components
BadgeBadge · Information · Light
Avatar (in "with avatar")Avatar · dark-initials · 32 px
Heading weightUses Semibold 600 — inconsistent with Generic Card's Bold 700 (flagged)
With avatar node 18482:35776

Adds a 32 × 32 Avatar at the leading edge (instance-swapped from the Avatar component). Used for person-to-person transactions.

No amount node 18482:35789

Used for non-monetary confirmations (KYC acknowledgments, voucher redemptions). Swaps amount for a trailing badge and the date row for a reference number.

Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBTransactionRow
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
(hardcoded)label: Stringlabel: Stringlabel: String
(hardcoded)metadata?: Stringmetadata: String?metadata: String?
type=with avatarleadingMedia?: Avatar (slot)leadingMedia: EBAvatar?leadingMedia: @Composable (() -> Unit)?
(drawn)badge?: Badge (slot)badge: EBBadge?badge: @Composable (() -> Unit)?
type=default/with avatartrailing=.amount(String)trailing: EBRowTrailingtrailing: EBRowTrailing
type=more informationtrailing=.menu(() -> Void)同上同上
type=no amounttrailing=.badge(Badge) + metadata=reference同上同上
type=skeleton loaderloading: Boolloading: Boolloading: Boolean
(not modeled)onTap?: () -> VoidonTap: (() -> Void)?onClick: (() -> Unit)?
Suggested file paths
  • ios/Components/TransactionRow/EBTransactionRow.swift
  • android/components/transactionrow/EBTransactionRow.kt
Usage
// Default — label + badge + date + amount
EBTransactionRow(
    label: "Juan Dela Cruz",
    badge: EBBadge("Sent", state: .information, level: .light),
    metadata: "Apr 14, 2026, 10:24 AM",
    trailing: .amount("PHP 1,500.00")
) { onRowTap() }

// With avatar
EBTransactionRow(
    leadingMedia: EBAvatar(initials: "JD"),
    label: "Juan Dela Cruz",
    badge: EBBadge("Sent"),
    metadata: "Apr 14, 2026, 10:24 AM",
    trailing: .amount("PHP 1,500.00")
)

// No amount — trailing badge + reference number
EBTransactionRow(
    label: "KYC Verification",
    metadata: "Reference No: GC123456789876543",
    trailing: .badge(EBBadge("Approved", state: .success))
)

// Loading
EBTransactionRow.skeleton()
// Default — label + badge + date + amount
EBTransactionRow(
    label = "Juan Dela Cruz",
    badge = { EBBadge("Sent") },
    metadata = "Apr 14, 2026, 10:24 AM",
    trailing = EBRowTrailing.Amount("PHP 1,500.00"),
    onClick = { onRowTap() }
)

// With avatar
EBTransactionRow(
    leadingMedia = { EBAvatar(initials = "JD") },
    label = "Juan Dela Cruz",
    badge = { EBBadge("Sent") },
    metadata = "Apr 14, 2026, 10:24 AM",
    trailing = EBRowTrailing.Amount("PHP 1,500.00")
)

// No amount — trailing badge + reference
EBTransactionRow(
    label = "KYC Verification",
    metadata = "Reference No: GC123456789876543",
    trailing = EBRowTrailing.BadgeSlot { EBBadge("Approved") }
)

// Loading
EBTransactionRow.Skeleton()
Accessibility
RequirementiOSAndroid
Row as buttonWhole row in Button with combined label (person + amount + date).Modifier.clickable { onTap() }.semantics(mergeDescendants=true).
Currency announcement"Juan Dela Cruz, Sent, 1,500 pesos, April 14 10:24 AM" — use localized currency formatter, not raw "PHP 1,500.00".Same — announce via contentDescription with currency formatter applied.
Reference numberSpell out long reference numbers for clarity: "GC 1 2 3 4..." — avoid run-together digits.Same.
LoadingAnnounce "Loading transactions" once on mount.contentDescription="Loading" on skeleton container.
Usage Guidelines
Do
  • Use for transaction history lists (Activity, Send / Receive logs).
  • Use avatar variant for person-to-person transactions.
  • Use skeleton on load — never a blank list.
  • Format currency per locale; format dates per user preference.
Don't
  • Don't mix type values in the same list without reason — it looks unstable.
  • Don't use for service/catalog rows — use Generic Card.
  • Don't omit the date/meta — transaction rows without context are confusing.
  • Don't hide the row in a pressed state — the row still exists, it's just visually pressed.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworktype enum hides 5 layouts — restructure to slot-based.
C2Variant & Property NamingReworkAbsence-based value names; heading weight inconsistent with Generic Card.
C3Token CoverageReadyColors bound to main/card-list/color/*.
C4Native MappabilityNeeds RefinementMaps cleanly once slots replace the type enum.
C5Interaction State CoverageNeeds RefinementNo pressed / disabled. Skeleton ✓.
C6Asset & Icon QualityReadyAvatar + Badge composed correctly as instances.
C7Code Connect LinkabilityNot MappedBlocked on restructure.
Variants Inventory (5 total)

type is a single enum with 5 values, each a structurally different layout.

#NodetypeLayoutDimensions
118482:35754defaultlabel · badge · date · amount360 × 78
218482:35765more informationlabel · date · amount · menu (⋯)360 × 76
318482:35776with avataravatar · label · badge · date · amount360 × 84
418482:35789no amountlabel · reference · trailing badge360 × 76
518482:35797skeleton loaderloading placeholders360 × 81
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:35753
Verdict: Restructure — Replace type enum (5 layouts) with slot-based composition. Align heading weight with Generic Card. Add pressed state. Open
Architecture
C1 — Type enum hides layouts — Same anti-pattern as Alert's Full Width. Split into leadingMedia, badge, trailing, loading. Open
C1
C2 — Absence-based namesno amount / more information describe what's missing. Semantic slot names replace them. Open
C2
C2 — Heading weight inconsistency — Semibold 600 vs Generic Card's Bold 700. Standardize across card family. Open
C2
C5 — Pressed state — Transaction rows drill into detail on tap; needs tap feedback. Open
C5
Skeleton pattern ✓ — First-class loading variant, matches Generic Card. Adopt as DS-wide convention. Noted
Praise
C7 — Code Connect — Blocked on restructure. Open
C7
Header - Centered RestructureReworkComponent link

A centered label banner sitting at the top of a screen, modal, or feature. 4 variants from type=dark | light × description=yes | no. The dark variant uses a brand-blue surface with white text; the light variant uses a default surface with dark text. Used for page titles in modal sheets, settings sub-screens, and feature banners.

Restructure — rename and re-label the surface property
Rename to Page Banner — "Header" conflates with three structurally different components. Replace type=dark | light with surface=brand | default — the current name describes appearance, not semantic intent. See the Header family restructure for the full plan.
In Context

Page Banner sits at the top of a screen, modal, or feature card — centered, taking full width, setting the title of the surface below it.

Live Preview
Properties
type
description
DS Health
Reusable
Pass
Generic page banner, reusable anywhere a centered title is needed.
Self-contained
Pass
Owns its typography, color tokens, and surface fill.
Consistent
Warn
Shares "Header" prefix with 3 structurally different components. type=dark|light names appearance rather than semantic tone.
Composable
Pass
Drops into any screen, modal, or card as a page title.
Behavior
StateiOSAndroidFigma SpecNotes
Default (brand)YesYestype=darkWhite text on brand-blue surface.
Default (default)YesYestype=lightDark text on default surface.
PressedN/AN/ABanner is informational — no pressed state.
DisabledN/AN/ANot interactive.
Open Issues
  • Rename to Page Banner. "Header" prefix shared with 3 structurally different components. C1 · Layer Structure & Naming
  • Rename type=dark|light to surface=brand|default. Current name describes appearance; convention is to name by semantic surface role. C2 · Variant & Property Naming
  • No Code Connect mapping.C7 · Code Connect Linkability
Design Recommendations
  • Rename to Page Banner. Frees the "Header" namespace and signals semantic role clearly. Rename
  • Use surface=brand | default instead of type=dark | light. Matches how other DS components (Button appearance, Badge tone) describe surface variants. Property
  • Optional: expose alignment prop (center | leading) for teams that want a left-aligned variant without creating a new component. Property
  • See siblings:Header, Header - With Logo, Header - Transaction. Family
Dark / brand surface node 18430:2859

White title on brand-blue surface. The "hero" variant — used for primary feature banners.

Properties
typedark
descriptionyes
Colors
ROLETOKENVALUE
Surfacesurface/brand#005CE5
Titletext/on-brand#FFFFFF
Descriptiontext/on-brand-muted#C8D8F5
Layout
WidthFill
Height104 (hug)
Paddingspace/space-24 space/space-16
Gapspace/space-4
Typography
TitleHeading/L · BarkAda 18/24
DescriptionBody/S · 12/16
Alignmentcenter
Light / default surface node 18430:2865

Dark title on default surface. Used for modal sheet titles and subdued banners.

Properties
typelight
descriptionyes
Colors
ROLETOKENVALUE
Surfacesurface/default#FFFFFF
Titletext/primary#0A2757
Descriptiontext/secondary#90A8D0
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBPageBanner
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
(implicit)title: Stringtitle: Stringtitle: String
description: booleandescription?: Stringdescription: String?description: String?
type: dark | lightsurface: brand | default.ebSurface(.brand) modifiersurface=EBSurface.Brand
Suggested file paths
  • ios/Components/PageBanner/EBPageBanner.swift
  • android/components/pagebanner/EBPageBanner.kt
Usage
// Brand surface with description
EBPageBanner(
    title: "Send Money",
    description: "To any GCash user"
)
.ebSurface(.brand)

// Default surface, title only
EBPageBanner(title: "Settings")
    .ebSurface(.default)
// Brand surface with description
EBPageBanner(
    title = "Send Money",
    description = "To any GCash user",
    surface = EBSurface.Brand
)

// Default surface, title only
EBPageBanner(title = "Settings", surface = EBSurface.Default)
Accessibility
RequirementiOSAndroid
Heading trait.accessibilityAddTraits(.isHeader) on the title.Modifier.semantics { heading() } on the title.
ContrastBrand surface: white on #005CE5=8.5:1 ✓. Default surface: #0A2757 on #FFFFFF=15.4:1 ✓.Same contrast ratios apply.
Screen reader orderTitle → Description. VoiceOver reads in DOM order.Same — TalkBack follows composition order.
Usage Guidelines
Do
  • Use as a page title banner at the top of a screen, modal, or feature card.
  • Pick brand surface for hero/promotional banners; default for subdued titles.
  • Apply the heading a11y trait so screen readers can navigate by structure.
Don't
  • Don't use for in-screen section titles — use Section Header.
  • Don't use for app-level top bars — use Title Bar.
  • Don't stack multiple banners on one screen.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkRename to Page Banner.
C2Variant & Property NamingReworkRename type=dark|lightsurface=brand|default.
C3Token CoverageReadySurface and text tokens bound.
C4Native MappabilityReadyMaps cleanly to a EBPageBanner view/composable.
C5Interaction State CoverageN/AStatic — no interactive states.
C6Asset & Icon QualityN/ANo assets.
C7Code Connect LinkabilityNot MappedTrivial once renamed.
Variants Inventory (4 total)

type × description=4 variants.

#NodetypedescriptionDimensions
118430:2859darkyes360 × 104
218430:2865lightyes360 × 104
318430:2871darkno360 × 84
418430:2873lightno360 × 84
1.0.0 — April 2026 Initial
Initial Assessment · node 18430:2858
Verdict: Restructure — Rename to Page Banner. Swap type=dark|light for surface=brand|default. Open
Naming
C7 — Code Connect — Trivial mapping once renamed. Open
C7
Header - Transaction RestructureReworkComponent link

A brand-surface hero block with an avatar, title ("Add Label Here"), a horizontal divider, an optional inline email: address@example.com row, and 1–2 lines of description. Used on transaction detail and recipient profile screens. Structurally this is a card hero, not a header — its anatomy (avatar + title + divider + label-value + body) matches the hero patterns seen in profile cards and detail screens across other DS. 2 variants: email=yes | no.

Restructure — move out of Header family, rename to Detail Hero
This is not a header. It has no navigation role, no title-only scope, and a card-like structure (avatar + divider + inline label-value). Rename to Detail Hero and rehome with card/hero patterns. Then re-architect: the avatar should be a real Avatar instance, the email row should compose an inline label-value pair, and the description should accept structured content. See Header family restructure.
In Context

Detail Hero appears at the top of transaction detail screens and recipient profile cards — introducing the person or transaction below the app bar.

Live Preview
Properties
email
DS Health
Reusable
Partial
Reusable as a hero block, but misfiled as a header and tightly coupled to transaction-specific copy ("email:"). Generic card hero should accept any label-value rows.
Self-contained
Warn
Avatar slot is a placeholder circle (not a real Avatar instance). Label-value row is drawn, not composed.
Consistent
Warn
"Header" prefix misleads — this solves a card-hero problem. Pattern should live with card/hero primitives.
Composable
Warn
Single email boolean can't extend to other metadata rows (phone, MCC, reference number). Needs a flexible rows slot.
Behavior
StateiOSAndroidFigma SpecNotes
Default (no email)YesYesemail=noAvatar + title + divider + description.
With emailYesYesemail=yesAdds an inline email: value row above the description.
Pressed / DisabledN/AN/AStatic — no interactive states.
Open Issues
  • Misfiled as a header. Anatomy is a card hero (avatar + title + divider + label-value + description). Rename to Detail Hero and move out of the Header family. C1 · Layer Structure & Naming
  • Avatar is a placeholder, not an instance. Should accept a real Avatar instance (status ring, initials, image — all of which Avatar already supports). C4 · Native Mappability
  • Label-value row is hardcoded to "email:". A detail hero needs flexible metadata rows (phone, reference #, MCC, transaction ID). Replace the boolean with a rows slot or accept a Labeled Field instance. C2 · Variant & Property Naming
  • Pressed state on avatar is not defined — if the avatar is tappable (opens profile, edits photo), it needs state coverage. C5 · Interaction State Coverage
  • No Code Connect mapping. Blocked on the Detail Hero rehome decision. C7 · Code Connect Linkability
Design Recommendations
  • Rename to Detail Hero and move out of the Header family. Rehome next to Visual Popup / card primitives. Rename
  • Replace the avatar placeholder with a real Avatar instance. Avatar already supports image, initials, and status ring — no reason to re-draw it here. Composition
  • Replace email=yes|no with a flexible rows slot.metadata: [LabelValuePair] lets consumers supply any number of rows (email, phone, reference, MCC). Each row could be a small inline-label component or a Labeled Field variant. Property
  • Expose surface=brand | default (same pattern as Page Banner). Detail heroes on settings screens may want the default surface. Property
  • See siblings:Header, Header - Centered, Header - With Logo. Family
No email node 18430:2906

The minimal variant — avatar + title + divider + description. Used when the profile/transaction has no extra metadata to show.

Properties
emailno
Colors
ROLETOKENVALUE
Surfacesurface/brand#005CE5
Titletext/on-brand#FFFFFF
Descriptiontext/on-brand-muted#C8D8F5
Dividerborder/on-brand-subtle#2B6BEA
Avatar fill(placeholder)#C8CDD5
Layout
WidthFill
Height220 (hug)
Paddingspace/space-24
Avatar size48 × 48
Gap (stacked)space/space-12
Typography
TitleHeading/L · BarkAda 20/26
DescriptionBody/S · 13/18
With email row node 18430:2898

Adds an inline email: value row between the divider and the description. Used on recipient profile cards.

Properties
emailyes
Proposed
Generic rows slotmetadata: [LabelValuePair]
Row anatomymuted label · strong value
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBDetailHero
Property Mapping (Proposed — Detail Hero)
Figma (today)Figma (proposed)SwiftUICompose
(implicit)title: Stringtitle: Stringtitle: String
(placeholder)avatar: Avatar (instance)avatar: EBAvataravatar: @Composable (() -> Unit)
email: booleanmetadata: [LabelValuePair]metadata: [EBLabelValue]metadata: List<EBLabelValue>
(implicit)description?: Stringdescription: String?description: String?
(implicit brand)surface: brand | default.ebSurface(.brand) modifiersurface=EBSurface.Brand
Suggested file paths
  • ios/Components/DetailHero/EBDetailHero.swift
  • android/components/detailhero/EBDetailHero.kt
Usage
// Recipient profile card
EBDetailHero(
    title: "Juan Dela Cruz",
    avatar: EBAvatar(initials: "JD"),
    metadata: [
        .init(label: "email", value: "juan@gmail.com"),
        .init(label: "phone", value: "0917 123 4567")
    ],
    description: "Recipient since 2023"
)
.ebSurface(.brand)

// Transaction detail
EBDetailHero(
    title: "₱1,500.00",
    avatar: EBAvatar(icon: .arrowUp),
    metadata: [.init(label: "ref #", value: "2026-0414-00917")],
    description: "Sent to GCash account"
)
// Recipient profile card
EBDetailHero(
    title = "Juan Dela Cruz",
    avatar = { EBAvatar(initials = "JD") },
    metadata = listOf(
        EBLabelValue("email", "juan@gmail.com"),
        EBLabelValue("phone", "0917 123 4567")
    ),
    description = "Recipient since 2023",
    surface = EBSurface.Brand
)
Accessibility
RequirementiOSAndroid
Heading traitApply to the title line.Modifier.semantics { heading() } on the title.
Avatar a11yIf decorative, mark .accessibilityHidden(true). If identifying, label with person's name.Same — contentDescription empty when decorative, or person's name when identifying.
Label-value pairsGroup each pair with .accessibilityElement(children: .combine) so VoiceOver reads "email, juan@gmail.com" as one utterance.Use Modifier.semantics(mergeDescendants=true) per row.
Contrast on brand surfaceWhite text on #005CE5=8.5:1 ✓. Muted #C8D8F5 on #005CE5=2.1:1 — fails AA body text. Use only for secondary labels ≥14pt bold.Same ratios — reserve muted color for label text, not body copy.
Usage Guidelines
Do
  • Use as a hero card at the top of detail/profile screens — after the app bar, before the content.
  • Supply real Avatar instances (image, initials, icon) — not placeholder circles.
  • Use the metadata rows for short label-value pairs only (email, phone, reference).
Don't
  • Don't confuse with a header or app bar — this is a card hero.
  • Don't pack more than 2–3 metadata rows — use a list below the hero instead.
  • Don't put interactive controls inside the hero — keep actions in the bar above or the content below.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRework"Header - Transaction" misfiled. Rename to Detail Hero.
C2Variant & Property NamingReworkemail=yes|no should become metadata: [LabelValuePair].
C3Token CoverageReadySurface, title, description tokens bound.
C4Native MappabilityReworkAvatar should be a real instance; metadata should be structured, not drawn.
C5Interaction State CoverageNeeds RefinementAvatar pressed state not defined — needed if tappable.
C6Asset & Icon QualityNeeds RefinementAvatar is a drawn placeholder, not a vector Avatar instance.
C7Code Connect LinkabilityNot MappedBlocked on rehome + avatar-instance decisions.
Variants Inventory (2 total)
#NodeemailDimensions
118430:2906no360 × 220
218430:2898yes360 × 191
1.0.0 — April 2026 Initial
Initial Assessment · node 18430:2897
Verdict: Restructure — Not a header. Rename to Detail Hero and move out of the Header family. Open
Architecture
C2 — metadata rows — Replace email=yes|no with a flexible metadata: [LabelValuePair] slot. Open
C2
C4 — Avatar instance — Replace drawn placeholder with a real Avatar instance. Open
C4
C5 — Avatar state — Define pressed/disabled for tappable avatar. Open
C5
C7 — Code Connect — Blocked. Open
C7
Header RestructureReworkComponent link

In-screen section-title block (e.g. "Recent transactions · View all"). 16 variants generated from 8 independent boolean properties: preamble, description, icon, left illustration, right illustration, link, edit, counter. Not a navigation header or app bar — see Title Bar for that. Part of a 4-component "Header" family that needs restructuring.

Restructure — rename, split, and collapse the variant matrix
Today four components share the "Header" prefix but solve four different problems. The base Header (this one) should be renamed Section Header, its 8 boolean slots collapsed into 3 props (preamble, leadingMedia, trailing), and the sibling "Header - *" components either renamed by role or merged into existing primitives. See the Family Restructure section below for the full plan.
In Context

Section headers sit above grouped content — a list of transactions, a set of services, a carousel of offers — to label the section and optionally expose a trailing action.

Live Preview

Toggle the slots to see how the current 16-variant matrix responds. Note how many combinations are theoretically possible but not built — that's the boolean-model tax.

Properties
preamble
description
leading media
trailing
DS Health
Reusable
Partial
Works across many screen sections, but boolean slots force consumers to think in invalid combinations (icon + left illustration + right illustration all true?).
Self-contained
Pass
Owns its typography, spacing, and color tokens. No external state required.
Consistent
Warn
The "Header" name is shared with three structurally different components (Centered, With Logo, Transaction). Property model (8 booleans) conflicts with the enum-slot model used by other components in the DS.
Composable
Warn
Actionable slots (link, edit, counter) are drawn in-place rather than accepting Button/Badge/Link instances. Consumers can't swap in their own action component.
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYes16 variantsThe only state today — no pressed/disabled/focused.
Trailing action pressedVia slotVia slotNot modeledLink, edit, counter should be real Button/Link instances that carry their own pressed state.
DisabledN/AN/ANot modeledSection headers are informational — no disabled variant needed.
Focused (a11y)N/AN/AFocus lives on the trailing action, not the header itself.
Family Restructure — the big picture

The Header family spans four components that share a visual prefix but solve four different problems. Before cleaning this component, align on the family shape.

TodayProposedSemantic roleAction
Header (this component) · 16 variantsSection HeaderIn-screen section titleRename + collapse 8 booleans → 3 props.
Header - Centered · 4 variantsPage BannerCentered page/modal titleRename. Swap type=dark|light for surface=brand|default.
Header - With Logo · 2 variantsMerge intoTitle BarBrand app barAdd leading=title | logo slot to Title Bar. Retire this file.
Header - Transaction · 2 variantsDetail HeroCard hero — not a headerMove out of Header family. Rehome near Visual Popup / card patterns.
Proposed API — Section Header

From 8 booleans to 3 props — covering the same design space with zero invalid combinations.

// Proposed API — replaces 8 boolean props with 3
EBSectionHeader(
    preamble: "RECENT",
    title: "Transactions",
    description: "Your activity this month",
    leadingMedia: .icon(Image(systemName: "chart.bar")),
    trailing: .link("View All", action: onViewAll)
)
// Proposed API — slot-based with typed trailing enum
EBSectionHeader(
    title = "Transactions",
    preamble = "RECENT",
    description = "Your activity this month",
    leadingMedia = { Icon(EBIcons.Chart, contentDescription = null) },
    trailing = EBHeaderTrailing.Link("View All", onClick = onViewAll)
)
Open Issues
  • Family-wide naming conflict. "Header" prefix conflates 4 roles (section header, page banner, brand bar, detail hero). Rename each by role. C1 · Layer Structure & Naming
  • 8 boolean props, 256 theoretical combos, 16 built. The boolean model implies combinations that don't exist. Collapse icon + left illustration into leadingMedia, and right illustration + link + edit + counter into trailing. C2 · Variant & Property Naming
  • Trailing actions aren't real components. Link ("View All"), Edit, and Counter are drawn in-place rather than accepting Button / Badge / Link instances. Breaks composition and blocks state handling (pressed, disabled). C4 · Native Mappability
  • No pressed/disabled states on the actionable slots. Natively, those slots need full state coverage — at minimum pressed + disabled. C5 · Interaction State Coverage
  • No Code Connect mappings. Trivial once slots are enumerated. C7 · Code Connect Linkability
Design Recommendations
  • Rename to "Section Header". Unambiguously signals in-screen section title; frees "Header" namespace. Rename
  • Collapse to 3 propspreamble?, leadingMedia?: icon | illustration, trailing?: illustration | link | edit | counter — plus the required title. Eliminates invalid combos, drops variant count from 16 to ~6 canonical patterns. Property
  • Promote actionable slots to real components. "View All" becomes a Text Button instance. "Edit" becomes an Icon Button. "Counter" becomes a Badge instance. Each carries its own pressed/disabled states. Composition
  • Move "Header - Transaction" out of the family. It's a card hero, not a header. Rename to Detail Hero and rehome near Visual Popup. Family
  • Merge "Header - With Logo" into Title Bar. Add leading=title | logo slot to Title Bar instead of maintaining a second app-bar component. Family
  • See siblings:Header - Centered, Header - With Logo, Header - Transaction. Restructure is a family-wide decision. Family
Title only (baseline) node 18430:2932

The simplest variant — a bare title. This is the baseline the other 15 variants layer slots onto.

Properties
preambleno
descriptionno
all media/action slotsno
Typography
Title styleHeading/L — BarkAda 18/24
Colortext/primary · #0A2757
Layout
WidthFill
Height58 (hug)
Padding0
Gap (stacked slots)space/space-4
Colors by State
ROLETOKENVALUE
Titletext/primary#0A2757
Preambletext/brand#005CE5
Descriptiontext/secondary#90A8D0
Link labeltext/brand#005CE5
Full stack (preamble + title + description) node 18430:2920

All three text slots filled. This is the canonical "announce a section" pattern.

Properties
preambleyes
descriptionyes
media/actionsno
Typography
PreambleLabel/S caps · 12/16 · text/brand
TitleHeading/L · 18/24 · text/primary
DescriptionBody/S · 12/16 · text/secondary
Title + trailing link node 18430:2984

Title on the left, "View All" link on the right. Common list-section pattern.

Proposed: the "View All" text should be an instance of EBTextButton — not a drawn text layer — so it inherits pressed/disabled states automatically.

Title + trailing edit node 18430:2989

Title left, pencil icon + "Edit details" link right. Used on profile/settings sections.

Proposed: "Edit details" should be an EBTextButton with leadingIcon: Image("pencil"). That's already a supported pattern in Button — no new component needed.

Title + trailing counter node 18430:2996

Title left, numeric counter pill right. Used on inbox/notifications.

Proposed: counter should be an instance of EBBadge (numeric variant). Badge already handles count + overflow ("99+") correctly.

Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBSectionHeader
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
preamble: booleanpreamble?: Stringpreamble: String?preamble: String?
(implicit)title: Stringtitle: String (required)title: String (required)
description: booleandescription?: Stringdescription: String?description: String?
icon + left illustrationleadingMedia?: icon | illustrationleadingMedia: EBLeadingMedia?leadingMedia: @Composable (() -> Unit)?
right illustration + link + edit + countertrailing?: illustration | link | edit | countertrailing: EBHeaderTrailing?trailing: EBHeaderTrailing?
Suggested file paths
  • ios/Components/SectionHeader/EBSectionHeader.swift
  • android/components/sectionheader/EBSectionHeader.kt
Usage
// 1 · Title only
EBSectionHeader(title: "Services")

// 2 · Title + View All link
EBSectionHeader(
    title: "Transactions",
    trailing: .link("View All", action: onViewAll)
)

// 3 · Full stack with leading icon
EBSectionHeader(
    preamble: "RECENT",
    title: "Activity",
    description: "Past 30 days",
    leadingMedia: .icon(Image(systemName: "chart.bar"))
)

// 4 · Title + counter badge
EBSectionHeader(
    title: "Notifications",
    trailing: .counter(12)
)
// 1 · Title only
EBSectionHeader(title = "Services")

// 2 · Title + View All link
EBSectionHeader(
    title = "Transactions",
    trailing = EBHeaderTrailing.Link("View All", onClick = onViewAll)
)

// 3 · Full stack with leading icon
EBSectionHeader(
    preamble = "RECENT",
    title = "Activity",
    description = "Past 30 days",
    leadingMedia = { Icon(EBIcons.Chart, contentDescription = null) }
)

// 4 · Title + counter badge
EBSectionHeader(
    title = "Notifications",
    trailing = EBHeaderTrailing.Counter(12)
)
Accessibility
RequirementiOSAndroid
Heading traitApply .accessibilityAddTraits(.isHeader) to the title.Apply Modifier.semantics { heading() } to the title text.
Trailing action labelLink/Edit/Counter must each carry their own accessibility label. Counter should announce count ("12 unread").Same — each trailing slot owns its own semantics.
Minimum touch targetTrailing interactive element must be ≥44×44pt.Trailing interactive element must be ≥48×48dp.
Reading orderPreamble → Title → Description → Trailing. VoiceOver follows DOM order.Same reading order — TalkBack follows composition order.
Usage Guidelines
Do
  • Use for in-screen section titles above grouped content.
  • Keep the title short — ideally one line, max two.
  • When you need a trailing action, use a real Button/Link/Badge instance.
  • Apply the heading a11y trait so screen readers can navigate by headings.
Don't
  • Don't use Section Header as a page title or app bar — use Title Bar / Page Banner.
  • Don't stack multiple leading media (icon + illustration both filled).
  • Don't draw "View All" as static text — use Text Button so it handles pressed/disabled.
  • Don't combine every slot on one instance — the 256-combo temptation is the anti-pattern this component is built on.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRework"Header" prefix shared with 3 structurally different components. Rename to Section Header.
C2Variant & Property NamingRework8 booleans → 3 props (preamble, leadingMedia, trailing). Drops 16 variants to ~6 canonical patterns.
C3Token CoverageReadyTypography and color bound to DS tokens.
C4Native MappabilityNeeds RefinementMaps to a simple EBSectionHeader view/composable once slots collapse. Trailing actions should be real Button/Badge instances, not drawn.
C5Interaction State CoverageNeeds RefinementHeader itself is static; trailing actions inherit Button/Link state coverage once they become instances.
C6Asset & Icon QualityNeeds RefinementConfirm leading/trailing "illustration" slots accept vector instances (Avatar / Icon / custom). Placeholder circle suggests unverified.
C7Code Connect LinkabilityNot MappedCannot map until property model collapses and trailing slots resolve to real components.
Variants Inventory (16 total)

Today: 8 independent boolean properties — preamble, description, icon, left illustration, right illustration, link, edit, counter. 2⁸=256 theoretical combos, only 16 built — most combinations are either invalid or unsupported.

GroupCountSlots enabled
Text-only4preamble × description permutations
With right icon (top-aligned)4preamble × description × icon
With leading illustration2description × left illustration
With trailing illustration2description × right illustration
With link (View All)2description × link
With edit1edit only
With counter1counter only
View full breakdown (16 rows)
#NodePreambleDescriptionIconL-IllusR-IllusLinkEditCounter
118430:2920
218430:2925
318430:2929
418430:2932
518430:2934
618430:2941
718430:2947
818430:2952
918430:2956
1018430:2962
1118430:2967
1218430:2973
1318430:2978
1418430:2984
1518430:2989
1618430:2996
1.0.0 — April 2026 Initial
Initial Assessment · node 18430:2919
Verdict: Restructure — Rename to Section Header, collapse 8 boolean props into 3 slots (preamble, leadingMedia, trailing). Open
Architecture
Family restructure plan — 4 "Header*" components should be renamed by role; "With Logo" merges into Title Bar; "Transaction" moves out of family. Open
Family
Trailing actions should be real components — Link/Edit/Counter should be Text Button / Icon Button / Badge instances, not drawn in-place. Open
C4
C7 — Code Connect — Blocked until property model collapses. Open
C7
Horizontal Voucher ConsolidateReworkComponent link

A 336-wide horizontal voucher tile composed of a full-width 336×144 raster hero image (with the "GrabFood" wordmark and two stacked discount Badge instances — "10% off" and "35% off" — both anchored top-right) plus a content block with a single row of 4 hardcoded status badges ("Limited", "Expiring", "Hot", "Discounted"), a hardcoded title ("Grab Food"), description, price (PHP 100.00 with PHP 150.00 strikethrough), and validity period. 1 symbol, no variants, 6 boolean toggles. All text and badge labels are frozen placeholder strings.

Consolidate into one Voucher Card with orientation + state axes
Horizontal Voucher, Vertical Voucher (5119:1635), and Voucher Card Horizontal (5119:1786) are three parallel records of the same component. Merge into a single Voucher Card with orientation: vertical | horizontal, state: default | limited | expiring | used | expired (borrowed from Voucher Card Horizontal — the canonical sibling since it ships the state axis), an image Slot that accepts Voucher Asset instances, a composable badges: [Badge] array, and text slots for title / description / price / original price / validity. Drop the 6 booleans in favor of real string properties.
Current Figma

Single symbol, 336 × 265. A 336×144 raster hero image sits on top of a 336-wide content block. The image carries two duplicate discount Badges stacked at the same top-right anchor ("10% off" and "35% off"). The content block holds a single row of 4 hardcoded badges, a hardcoded title, description, price (with strikethrough original), and validity period. Every text string is placeholder; every badge label is frozen.

DS Health
Reusable
Fail
All text content is hardcoded placeholder. Consumers cannot set a title ("Grab Food"), price (PHP 100.00), validity, or badge label without detaching. A "reusable" voucher component that can only render one frozen sample string is not reusable.
Self-contained
Warn
The symbol carries its own layout, spacing, and token-bound colors via main/vouchers/color/default/*. But it ships two stacked discount Badge instances ("10% off" + "35% off") at the same anchor, assuming one is invisible — nothing in the schema picks between them.
Consistent
Fail
Parallel to Vertical Voucher and Voucher Card Horizontal — three components for one concept. Voucher Card Horizontal ships a proper state axis (Default/Limited/Expiring/Used/Expired); Horizontal Voucher ships none. Property shape diverges across the family (6 booleans here vs 8 in Vertical vs 2 booleans + 4-state enum in Voucher Card Horizontal).
Composable
Warn
Nests Badge instances for the badge row (composition works). But the hero image is a raster asset baked into the frame, not a Voucher Asset instance — consumers cannot swap partner imagery without detaching. And since all voucher content is locked placeholder, a parent screen cannot compose real voucher data.
Behavior
AspectiOSAndroidFigmaNotes
Hero imageImage Slot: image: () -> some Viewimage: @Composable () -> Unitasset boolean (raster frozen)Currently a 336×144 raster with the "GrabFood" wordmark baked in. Should accept any Voucher Asset variant.
Discount amountdiscount: String? on image slotSameTwo stacked Badges ("10% off" + "35% off")Two Badge instances at the same anchor. Should be a single discount string property on the Voucher Asset, not two stacked layers.
Titletitle: Stringtitle: Stringheader boolean (string hardcoded)"Grab Good" frozen in the symbol. Boolean only toggles visibility.
Descriptiondescription: String?Samedescription boolean (string hardcoded)"This is the description of the voucher." frozen.
Price / originalprice: String, originalPrice: String?Sameamount boolean (strings hardcoded)"PHP 100.00" and "PHP 150.00" frozen; one boolean toggles both.
Validityvalidity: String?SamevalidityPeriod boolean (string hardcoded)"Validity: Dec 25 2022 - Jan 5 2023" frozen.
Status badgesbadges: [EBBadge]badges: List<EBBadge>badges boolean (row of 4 hardcoded)Single row of 4 fixed badge labels ("Limited", "Expiring", "Hot", "Discounted"). Row-level visibility only.
Statestate: .default | .limited | .expiring | .used | .expiredSame enumNot modelledAbsent entirely. Voucher Card Horizontal has it; Horizontal Voucher does not.
Tap targetEntire card as Button with PlainButtonStyleCard with onClick + rippleNot modelledVouchers are always tappable; current symbol has no pressed/disabled states.
Resolved Issues
  • Voucher content tokens exist. Background (text/color-text-inverse=white card surface), title (main/vouchers/color/default/label-title), description (label-description), amount (label-amount), strikethrough amount (rendered as #90a8d0, matches label-amount-original), and metadata (label-metadata) are all bound to the voucher component's variable collection. C3 · Token Coverage
  • Card elevation is tokenised. The card uses app/shadow/shadow-low (0px/0px/4px, elevation/app/shadow-low/color rgba(2,14,34,0.06)). No hardcoded shadow values on the parent frame. C3 · Token Coverage
Open Issues
  • Three parallel components for one concept. Horizontal Voucher, Vertical Voucher (5119:1635), and Voucher Card Horizontal (5119:1786) share the same anatomy — voucher image + title + description + price + validity + status badges — but ship as three separate components with divergent property shapes. This is a family-level consolidation, not a single-component fix. C4 · Native Mappability
  • No state axis. Voucher Card Horizontal ships Default / Limited / Expiring / Used / Expired as a proper state variant that drives background, label colors, and badge treatment. Horizontal Voucher has no state concept — a used or expired horizontal voucher cannot be rendered in greyed-out treatment. C5 · Interaction State Coverage
  • All text content is hardcoded placeholder. Title "Grab Food", description "This is the description of the voucher.", price "PHP 100.00", original price "PHP 150.00", and validity "Validity: Dec 25 2022 - Jan 5 2023" are all frozen strings inside the symbol. Booleans toggle visibility but not content. Consumers cannot render a real voucher without detaching. C2 · Variant & Property Naming
  • Two stacked discount Badges at the same anchor. The Voucher Asset image frame nests two Badge instances (I5121:4534;6983:110671 "10% off" and I5121:4534;6983:110685 "35% off") both absolutely positioned at the top-right. Only one is ever intended to be visible, but no property selects between them — both render on top of each other in the raw symbol. C1 · Layer Structure & Naming
  • Discount amount is baked into the image frame. The "35% off" / "10% off" label is a hardcoded Badge text inside the Voucher Asset, not a property on the parent Horizontal Voucher. A voucher offering "50% off" or "BUY1 TAKE1" cannot be rendered. Should be a discount: String? property on the image slot. C2 · Variant & Property Naming
  • Status badges are row-level, not array-level. The badges boolean toggles a single fixed row of 4 hardcoded Badge instances ("Limited" + "Expiring" + "Hot" + "Discounted") on or off. A real voucher with one "Limited" badge and nothing else cannot be rendered. Badges should be a composable array, not a fixed row. C2 · Variant & Property Naming
  • Hero image is a raster photograph with burned-in partner wordmark.Paste Image Here / imgPasteImageHere is a 336×144 raster asset with the "GrabFood" wordmark burned into the pixels. Partner branding and image content are frozen. Should be an Image Slot that accepts any Voucher Asset variant (GrabFood, Globe, Smart, Shopee, etc.). C6 · Asset & Icon Quality
  • No native component maps to this shape. 6 booleans with hardcoded content do not map to any reasonable native API. A proper EBVoucherCard takes title, price, validity, badges array, and image as parameters — not six visibility toggles over frozen strings. Code Connect has no 1:1 target. C4 · Native Mappability
  • Code Connect cannot link a 6-boolean symbol with frozen strings. Even if a mapping existed, swapping the "title" string, the hero image, or the badge labels would require detaching the component. Linkability requires real string/array properties and an image Slot first. C7 · Code Connect Linkability
Design Recommendations
  • Merge the three voucher cards into a single Voucher Card component. Horizontal Voucher + Vertical Voucher + Voucher Card Horizontal become one component with orientation: vertical | horizontal (swaps the layout axis) and state: default | limited | expiring | used | expired (borrowed from Voucher Card Horizontal — the canonical sibling since it already ships the state axis). Target shape: 2 orientations × 5 states=10 variants instead of three separate components with divergent schemas. Family
  • Promote every text string to a property. Add title: String, description: String, price: String, originalPrice: String?, validity: String?. Retire the header / amount / description / validityPeriod booleans — visibility falls out of whether the string is empty. Property
  • Replace the stacked discount Badges with one discount string on the image slot. Drop the duplicated "10% off" and "35% off" Badge instances inside the Voucher Asset frame. Expose discount: String? on the voucher-image Slot so any discount value ("10% off", "50% off", "BUY1 TAKE1") can be rendered without editing the symbol. Property
  • Adopt a Figma Slot for the hero image. Replace the raster Paste Image Here fill with a Slot that accepts a Voucher Asset instance (or any partner illustration component). The discount overlay and the raster wordmark both move into the swapped-in asset, not into the parent Voucher. Slot
  • Replace the fixed 4-badge row with a composable badges array. Drop the single badges boolean. Expose a badges Slot that accepts 0..n Badge instances and wraps when it runs out of width. Consumers choose which badges apply ("Limited" alone, "Hot" + "Discounted", "New" + "Featured", etc.). Slot
  • Add the state axis missing from Horizontal Voucher. Used and Expired vouchers render in greyed-out treatment with muted labels and a dimmed hero image — a pattern Voucher Card Horizontal already ships. Port the same 5-state treatment to the unified Voucher Card. State
  • Remove the dead 10% off Badge layer. The image frame carries both I5121:4534;6983:110671 ("10% off") and I5121:4534;6983:110685 ("35% off") at identical coordinates; after the discount property above is introduced, only one Badge instance should remain, with its text bound to the new property. Composition
  • Document that Voucher Card is the tap target. Vouchers are always tappable entry points to the voucher detail screen. The unified component should ship a pressed/focused state on the card frame; the handoff is an onTap closure, not an internal CTA button. Docs
Native equivalent Planned API

Native models the voucher family as a single EBVoucherCard with orientation, state, text properties, a composable badges array, and a Voucher Asset image parameter. The current Horizontal Voucher's 6 booleans do not have a 1:1 native analog; consumers write real strings and state enums instead.

// Proposed API — unified Voucher Card (horizontal orientation)
EBVoucherCard(
    orientation: .horizontal,
    state: .limited,
    title: "Grab Food",
    description: "Get a discount on your next GrabFood order.",
    price: "PHP 100.00",
    originalPrice: "PHP 150.00",
    validity: "Dec 25 2022 - Jan 5 2023",
    badges: [
        EBBadge("Limited", style: .informationHeavy),
        EBBadge("Expiring", style: .negativeHeavy)
    ]
) {
    EBVoucherImageFrame(discount: "35% off") {
        Image("voucher-grabfood").resizable().scaledToFill()
    }
}
.onTapGesture { // navigate to voucher details }

// Used/expired treatment
EBVoucherCard(orientation: .horizontal, state: .used, title: "Grab Food", price: "PHP 100.00") {
    EBVoucherImageFrame {
        Image("voucher-grabfood").resizable().scaledToFill()
    }
}
// Proposed API — unified Voucher Card (horizontal orientation)
EBVoucherCard(
    orientation = EBVoucherOrientation.Horizontal,
    state = EBVoucherState.Limited,
    title = "Grab Food",
    description = "Get a discount on your next GrabFood order.",
    price = "PHP 100.00",
    originalPrice = "PHP 150.00",
    validity = "Dec 25 2022 - Jan 5 2023",
    badges = listOf(
        EBBadge("Limited", style = EBBadgeStyle.InformationHeavy),
        EBBadge("Expiring", style = EBBadgeStyle.NegativeHeavy)
    ),
    onClick = { /* navigate to voucher details */ },
    image = {
        EBVoucherImageFrame(discount = "35% off") {
            Image(
                painter = painterResource(R.drawable.voucher_grabfood),
                contentDescription = null,
                contentScale = ContentScale.Crop
            )
        }
    }
)
Property Mapping (Proposed)

The current 6 booleans do not map cleanly to native. The table below shows the target shape after the family consolidation — each row captures what the proposed EBVoucherCard replaces from the current Horizontal Voucher.

Current FigmaProposed FigmaSwiftUIComposeNotes
orientationorientation: EBVoucherOrientationorientation=EBVoucherOrientationvertical | horizontal — collapses 3 components into 1
statestate: EBVoucherStatestate=EBVoucherStatedefault | limited | expiring | used | expired (port from Voucher Card Horizontal)
asset (boolean, raster frozen)Image Slottrailing closureimage: @Composable () -> UnitAccepts any EBVoucherImageFrame instance; partner wordmark lives inside the asset, not the parent
"10% off" + "35% off" Badgesdiscount on Voucher AssetEBVoucherImageFrame(discount: "35% off")EBVoucherImageFrame(discount="35% off")One string on the image-frame child, not two stacked Badge layers on the parent
header (boolean, string frozen)title (string)title: Stringtitle: StringVisibility=whether string is empty
description (boolean, string frozen)description (string)description: String?SameSame
amount (boolean, PHP 100 / PHP 150 frozen)price + originalPriceprice: String, originalPrice: String?SameTwo strings, strikethrough applied to originalPrice
validityPeriod (boolean, string frozen)validity (string)validity: String?SameSame
badges (boolean, row of 4 hardcoded)badges Slotbadges: [EBBadge]badges: List<EBBadge>Composable array, wraps on overflow
onTap: () -> VoidonClick: () -> UnitCard is the tap target
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkTwo discount Badge instances ("10% off" + "35% off") stacked at the same anchor inside the Voucher Asset frame. Text layers are unnamed. Paste Image Here is a generic placeholder layer name.
C2Variant & Property NamingRework6 booleans where most should be strings (title, price, validity) or a Slot (badges, image). badges boolean toggles a fixed row of 4 hardcoded badges. All content is frozen placeholder.
C3Token CoverageReadyTitle, description, amount, strikethrough amount, and metadata are all bound to main/vouchers/color/default/*. Card elevation uses app/shadow/shadow-low. Typography uses named text styles (Primary/Multi-line Label/Base, Primary/Label/Small, Secondary/Default/Caption, Secondary/Default/Fine).
C4Native MappabilityReworkParallel to 2 other voucher components with divergent schemas. Native has one EBVoucherCard, not three. 6 booleans with frozen strings and a raster hero have no native analog.
C5Interaction State CoverageReworkNo state axis at all. Voucher Card Horizontal ships Default/Limited/Expiring/Used/Expired; Horizontal Voucher has none. No pressed/focused/disabled on the card frame either.
C6Asset & Icon QualityReworkHero image is a raster photograph with the "GrabFood" wordmark burned into the pixels. Discount amount is baked into stacked Badge instances, not a property.
C7Code Connect LinkabilityReworkCannot map. Frozen strings, row-level badge toggle, stacked discount badges, and raster hero do not have 1:1 native parameters. Linkability requires the family consolidation first.
Variants Inventory (1 total)

Single symbol, no variant axes declared. All configurability is through 6 boolean property toggles on the lone instance.

Node IDNameDimensionsProperty toggles
5121:4533Horizontal Voucher336 × 265 (with all properties on)amount, asset, badges, description, header, validityPeriod — all boolean, all default true

Nested instances (two discount badges stacked at the same anchor; row of four status badges with hardcoded labels):

Node IDLayerKindDimensions
5121:4534Voucher AssetImage frame (raster + 2 stacked discount badges)336 × 144
I5121:4534;6983:110671Voucher Asset > Badge "10% off"Badge instance (brand/heavy)auto
I5121:4534;6983:110685Voucher Asset > Badge "35% off"Badge instance (brand/heavy)auto
5121:4537badges > Badge "Limited"Badge instance (information/heavy)auto
5121:4538badges > Badge "Expiring"Badge instance (negative/heavy)auto
5121:4539badges > Badge "Hot"Badge instance (destructive)auto
5121:4540badges > Badge "Discounted"Badge instance (brand/heavy)auto
1.0.0 — April 2026 Initial
Initial Assessment · node 5121:4533
Assessed with Consolidate verdict. Single symbol with no variants, 6 boolean toggles, hardcoded content, raster hero image, and two stacked discount Badges. Parallel to Vertical Voucher (5119:1635) and Voucher Card Horizontal (5119:1786). Open
Design Decision
Proposed family-level merge. Collapse the 3 voucher components into one Voucher Card with orientation + state axes, text slots (title, description, price, originalPrice, validity), a badges array, and a voucher image Slot. Target: 2 × 5=10 variants instead of 3 divergent components. Open
Design Decision
Inline Text RestructureRefineComponent link

A label-value receipt row used as building block inside transaction screens, generic transaction cards, confirmation modals, and list items. Label on the leading edge, value/badge/link on the trailing edge, optional description or text link below. 5 variants under one type enum: Default, with Clipboard, with Badge, with Description, with Text Link. Not a control — it is a layout pattern with opinionated typography and token bindings.

Restructure — type enum hides 5 trailing-slot compositions
Same anti-pattern as Alert's Full Width and Generic Transaction Card's type. Replace the enum with orthogonal booleans (hasCopy, hasDescription, hasTextLink) plus a unified trailing slot so Badge can be instance-swapped instead of drawn inline. The component itself belongs — its four semantic color tokens (label, label-value, description, label-link) give it enough DS opinion to ship as EBInlineText, but behind a cleaner schema.
In Context

Inline Text is a composition primitive. You'll find stacks of it inside Generic Transaction Card's detail modal, Send Money confirmation screens, receipt summaries, and fee-breakdown list items. Rarely used standalone.

Live Preview

Flip type to swap the trailing-slot composition. Notice how the 5 values aren't variations of one pattern — they produce structurally different rows. The proposal replaces this enum with orthogonal slots.

Content
label
value
description
cta
badge
Properties
type
DS Health
Reusable
Pass
Genuinely reused across transaction cards, modal summaries, and list items. A strong primitive.
Self-contained
Warn
Binds tokens cleanly but draws its own Badge inline (information/light hardcoded) instead of instance-swapping from the canonical Badge component — parallel source of truth for badge styling.
Consistent
Warn
The type enum conflates two axes (trailing-slot content and sub-row content). with Description and with Text Link describe the second row; with Clipboard and with Badge describe the first. Merging them into one enum forces consumers to pick incompatible combinations.
Composable
Partial
Used as a stacking block inside larger components — good. But consumers can't freely combine (e.g. clipboard + description) because the enum makes those permutations unrepresentable.
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYestype=DefaultLabel leading, value trailing. The baseline receipt row.
With ClipboardYesYestype=with ClipboardValue + 24 × 24 copy icon. Tapping the icon copies the value to clipboard.
With BadgeYesYestype=with BadgeReplaces the value with a trailing badge pill. Used for voucher / discount selection.
With DescriptionYesYestype=with DescriptionAdds a second row below the label with secondary caption text.
With Text LinkYesYestype=with Text LinkSecond row adds a trailing text link (CTA) next to the description.
Pressed (copy icon)MissingMissingNot builtCopy icon has no pressed state or success toast hook — users get no feedback when the tap lands.
Open Issues
  • type enum conflates two axes and hides 5 different layouts.with Clipboard and with Badge describe the trailing slot, while with Description and with Text Link describe a second row. As one enum, the combinations "clipboard + description" and "badge + text link" are unrepresentable — even though real screens need them. Split into orthogonal booleans (hasCopy, hasDescription, hasTextLink) with a unified trailing slot. C1 · Layer Structure & Naming
  • Enum values mix concerns and use "with X" phrasing.with Clipboard / with Badge / with Description / with Text Link all read as feature toggles ("with feature X"), which reinforces the anti-pattern of the previous bullet. Under the proposed schema, these become boolean props named by what they ARE, not what they add. C2 · Variant & Property Naming
  • Badge variant is drawn inline, not instance-swapped. The with Badge variant hardcodes main/badge/information/light/background and main/badge/information/light/label — duplicating the Badge component's styling rules. If Badge changes its hover state, border, or typography, Inline Text silently drifts. Replace with an instance swap on a trailing slot. C6 · Asset & Icon Quality
  • Copy icon appears to be drawn inline rather than a DS Icon instance. The Copy node is a local instance, but its child layers (shape_half, shape_full) suggest a one-off vector rather than a canonical icon swapped from the DS icon library. Confirm the source and, if necessary, replace with a swappable icon prop so consumers can choose clipboard / share / refresh trailing actions. C6 · Asset & Icon Quality
  • No pressed state on the copy action. The clipboard variant is the only interactive part of Inline Text and it ships with no pressed tint, no focus ring, and no success toast hook. Add a pressed state on the icon and document the success-toast convention. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked on the slot / boolean restructure — mapping today's single type enum would bake the anti-pattern into the native API. C7 · Code Connect Linkability
Design Recommendations
  • Replace type with orthogonal booleans + a trailing slot. Target schema: label: String, trailing: value(String) | badge(Badge) | custom, hasCopy: Bool, hasDescription: Bool, hasTextLink: Bool, description?: String, ctaLabel?: String. This unlocks valid combinations today's enum forbids (copy + description, badge + link) without adding any net new variants. Property
  • Instance-swap Badge instead of drawing it inline. Expose a trailing slot that accepts a Badge instance. Removes the parallel badge-styling source, keeps Inline Text honest as a layout primitive. Composition
  • Rename the clipboard prop to describe the action.hasCopy reads better than today's with Clipboard — "copy" is the action, "clipboard" is the target. Provide an onCopy callback in the native API and document the success-toast convention. Rename
  • Confirm the copy icon is a DS Icon instance. If it's a local one-off, replace with a swapped DS Icon so the whole family shares one icon source. While at it, expose trailingIcon as a slot so screens can use share / external-link / refresh instead of copy. Slot
  • Add a pressed state on the copy icon. Subtle tint change (iconicon-pressed) plus a documented toast ("Copied to clipboard") on tap. This is the only interactive affordance in Inline Text — it needs feedback. State
  • Document the stacking recipe. Most real usage is a vertical stack of 3–6 Inline Text rows inside a modal or card (fee breakdowns, transaction details). Add a "Receipt block" recipe page showing the stack spacing (8 px gap, 1 px divider optional) so consumers don't reinvent it. Docs
  • Reconcile typography with Generic Transaction Card's metadata. Inline Text's description row uses BarkAda Semibold 12/18. Generic Transaction Card's date metadata uses the same font but a different line-height bucket. Align captions family-wide so receipt text reads consistently. Family
Default — label + value node 21:138493

The baseline row. Leading label, trailing value, 25 px tall, full-width auto-layout.

Properties
typeDefault
label"Label"
value"0.00"
Colors by State
ROLETOKENVALUE
Labelinline-text/label#0A2757
Valueinline-text/label-value#445C85
Descriptioninline-text/description#6780A9
Linkinline-text/label-link#005CE5
Iconinline-text/icon#445C85
Layout
Width368 (fill)
Height25
Padding0 (row) · 5 0 5 0 (value cell)
Gap0 (fill + hug)
Typography
Label stylePrimary/Label/Light/Base
Label fontProxima Soft Semibold
Size / Line / Tracking16 / 16 / +0.25
ValueSame as label, opacity 0.80 (via color token)
With Clipboard — value + copy icon node 21:138497

Adds a 24 × 24 copy icon to the right of the value. Tapping the icon copies the value to clipboard. Icon uses inline-text/icon token.

Properties
typewith Clipboard
value"00000000"
Icon size24 × 24
Open concern
Icon sourceAppears drawn inline — confirm instance swap
Pressed stateNot built
Success toastConsumer-defined today; should be documented
With Badge — label + trailing badge node 21:138503

Replaces the value cell with a Badge pill. Used on voucher / discount rows. Today the badge is drawn inline (information/light hardcoded) rather than instance-swapped from the Badge component.

Properties
typewith Badge
badge label"Label"
Badge stateinformation/light
Open concern
Badge sourceDrawn inline — should be an instance of Badge
Badge variantsOnly information/light is reachable from Inline Text today
With Description node 21:138506

Adds a second row below the label/value with a description caption. Description uses BarkAda Semibold 12/18.

Typography — Description
Text styleSecondary/Bold/Caption
FontBarkAda Semibold
Size / Line / Tracking12 / 18 / 0
Layout
Row gap2
Top row height24 (auto)
Description width368 (fill)
With Text Link node 21:138512

Adds a trailing link (CTA) to the description row. Link uses inline-text/label-link token.

Properties
typewith Text Link
cta"CTA"
Open concern
Link pressed stateNot built — text links are interactive, must have a pressed tint
Focus ringNot built
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBInlineText
Delivery Recommendation

Ship as a thin EBInlineText wrapper — not a docs-only recipe. Although Inline Text is "just a styled HStack," its four semantic color tokens (label, label-value, description, label-link), consistent typography rules, and pressable copy icon make a reusable component worth the ~40 lines of native code. A docs recipe would force every consumer to rebind tokens and redraw the clipboard interaction.

Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
(hardcoded)label: Stringlabel: Stringlabel: String
type=Default / with Clipboardtrailing=.value(String)trailing: EBInlineTextTrailingtrailing: EBInlineTextTrailing
type=with Badgetrailing=.badge(Badge) (instance)同上同上
type=with ClipboardhasCopy: BoolonCopy: (() -> Void)?onCopy: (() -> Unit)?
type=with Descriptiondescription?: Stringdescription: String?description: String?
type=with Text LinkctaLabel?: String + onCtaTap?cta: EBInlineTextCTA?cta: EBInlineTextCTA?
(not modeled)trailingIcon?: Icon (slot)trailingIcon: Image?trailingIcon: @Composable (() -> Unit)?
Suggested file paths
  • ios/Components/InlineText/EBInlineText.swift
  • android/components/inlinetext/EBInlineText.kt
Usage
// Default — label + value
EBInlineText(
    label: "Amount",
    trailing: .value("PHP 1,500.00")
)

// With clipboard copy
EBInlineText(
    label: "Reference No",
    trailing: .value("GC123456789"),
    onCopy: { showToast("Copied") }
)

// With trailing badge (instance-swapped)
EBInlineText(
    label: "Voucher",
    trailing: .badge(EBBadge("Applied", state: .success))
)

// With description
EBInlineText(
    label: "Service fee",
    trailing: .value("PHP 10.00"),
    description: "Non-refundable processing fee"
)

// With description + CTA link
EBInlineText(
    label: "Promo code",
    trailing: .value("GC50OFF"),
    description: "Saved PHP 50.00 on this order",
    cta: .init("Change") { openPromoPicker() }
)
// Default — label + value
EBInlineText(
    label = "Amount",
    trailing = EBInlineTextTrailing.Value("PHP 1,500.00")
)

// With clipboard copy
EBInlineText(
    label = "Reference No",
    trailing = EBInlineTextTrailing.Value("GC123456789"),
    onCopy = { showToast("Copied") }
)

// With trailing badge (instance-swapped)
EBInlineText(
    label = "Voucher",
    trailing = EBInlineTextTrailing.BadgeSlot { EBBadge("Applied") }
)

// With description
EBInlineText(
    label = "Service fee",
    trailing = EBInlineTextTrailing.Value("PHP 10.00"),
    description = "Non-refundable processing fee"
)

// With description + CTA link
EBInlineText(
    label = "Promo code",
    trailing = EBInlineTextTrailing.Value("GC50OFF"),
    description = "Saved PHP 50.00 on this order",
    cta = EBInlineTextCTA(label = "Change", onClick = { openPromoPicker() })
)
Accessibility
RequirementiOSAndroid
Row reads as "label, value"Merge children: .accessibilityElement(children: .combine). Announces "Amount, 1,500 pesos".Modifier.semantics(mergeDescendants=true) on the row.
Copy buttonDedicated Button around the icon with accessibilityLabel: "Copy reference number". Announce success via UIAccessibility.post(.announcement, "Copied").IconButton with contentDescription. On click, call view.announceForAccessibility("Copied").
Text link as linkUse Button with accessibilityAddTraits(.isLink). Minimum 44 × 44 touch target.TextButton or clickable text with role Role.Button. Minimum 48 dp touch target.
Currency announcementUse localized currency formatter on accessibilityValue, not raw "PHP 1,500.00".Same — announce via contentDescription with currency formatter applied.
Dynamic Type / font scalingText uses .font(.custom(..., relativeTo: .body)) so it scales with Dynamic Type.Use sp units for text size; respect system font scale.
Usage Guidelines
Do
  • Use for receipt-style rows inside cards, modals, and confirmation screens.
  • Stack multiple rows vertically (8 px gap) to build fee / transaction-detail blocks.
  • Use the clipboard variant for reference numbers, wallet IDs, promo codes.
  • Use the badge variant when the trailing value is a status, not a number.
  • Pair the CTA link with short actions like "Change" or "Edit" — not sentences.
Don't
  • Don't use for top-level page titles — it's a row primitive.
  • Don't omit the label — a value-only row is not Inline Text, it's free text.
  • Don't stack with inconsistent types in one block — pick one rhythm.
  • Don't reinvent the copy interaction — use the built-in onCopy hook.
  • Don't use for primary actions — use Button instead.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworktype enum conflates two axes — split into orthogonal booleans + slot.
C2Variant & Property NamingReworkEnum values use "with X" phrasing and describe features, not semantics.
C3Token CoverageReadyAll colors bound to main/inline-text/color/*. Spacing + typography tokens intact.
C4Native MappabilityNeeds RefinementNo 1:1 native primitive — it's a styled HStack. Maps cleanly as EBInlineText wrapper once restructured.
C5Interaction State CoverageNeeds RefinementNo pressed state on the copy icon or text link.
C6Asset & Icon QualityNeeds RefinementBadge drawn inline; copy icon appears one-off — confirm + instance-swap both.
C7Code Connect LinkabilityNot MappedBlocked on restructure — mapping today's enum would bake the anti-pattern.
Variants Inventory (5 total)

type is a single enum with 5 values — each a structurally different trailing-slot composition.

#NodetypeLayoutDimensions
121:138493Defaultlabel · value368 × 25
221:138497with Clipboardlabel · value · copy-icon368 × 24
321:138503with Badgelabel · trailing badge368 × 24
421:138506with Description[label · value] / description368 × 38
521:138512with Text Link[label · value] / [description · CTA]368 × 41
1.0.0 — April 2026 Initial
Initial Assessment · node 18652:71101
Verdict: Restructure — Replace type enum (5 layouts) with orthogonal booleans + unified trailing slot. Instance-swap Badge. Add pressed state on copy icon. Open
Architecture
C1 — Type enum hides compositions — 5 type values conflate two axes (trailing slot + sub-row). Split into hasCopy, hasDescription, hasTextLink, unified trailing slot. Open
C1
C2 — "with X" value phrasing — Figma enum values describe what's added, not what the row IS. Rename under boolean-prop schema. Open
C2
C6 — Badge drawn inlinewith Badge hardcodes information/light fill + label instead of instance-swapping Badge. Parallel source of truth. Open
C6
C5 — Copy / link pressed states — Copy icon and text link have no pressed tint, focus ring, or success feedback hook. Open
C5
Tokens ✓ — All four semantic color roles (label, label-value, description, label-link) plus icon bound correctly. Noted
Praise
C7 — Code Connect — Blocked on restructure. Open
C7
Input Field FixNeeds RefinementComponent link

A 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.

Fix required before handoff
isFilled uses Yes/No instead of true/false (C2). Property naming blocks direct Swift Bool / Kotlin Boolean mapping.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Live Preview

Toggle state and fill to see the input field update in real time.

Properties
State
isFilled
DS Health
Reusable
Partial
Works across any text input context. Single-line only — no multi-line or textarea variant. No size variants (fixed 46px height).
Self-contained
Pass
Carries its own border, fill, and text styles per state. All 4 interaction states defined. Disabled state has distinct background.
Consistent
Pass
C2 resolved — isFilled now uses true/false. C1 resolved — text layer renamed from #text-label to #label, consistent with sibling fields.
Composable
Pass
Serves as the base primitive for Labeled Field, Select Field, and Recipient Field. Nests cleanly in form layouts.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesState=DefaultGray #D7E0EF border, white bg.
Active (Focused)YesYesState=ActiveBlue #005CE5 border.
ErrorYesYesState=ErrorRed #D61B2C border.
DisabledYesYesState=Disabled#EEF2F9 bg, border hidden.
Resolved Issues
  • isFilled property renamed from Yes/No to true/false — now maps directly to Swift Bool / Kotlin BooleanC2 Fixed
  • Text layer renamed from #text-label to #label — now consistent with sibling fields C1 Fixed
Open Issues
  • Code Connect mappings not registered. Structural work is complete — registration can proceed against the 8-variant State × isFilled schema. C7 · Code Connect Linkability
Design Recommendations
  • Standardize text layer naming across Form Elements. Input Field now uses #label; verify the remaining fields (Labeled, Select, Recipient, View Only) follow the same convention so Code Connect label-slot mapping is uniform. Rename
  • Add a helperText slot below the field. Validation messages and hint copy are currently handled outside the component — a first-class slot keeps form anatomy self-contained and matches native TextField hint affordances. Slot
  • Add leadingIcon / trailingIcon slots. Labeled Field already has these — extend to Input Field for search, clear, and validation-indicator use cases. Lets the DS cover the full form-element palette without per-screen customization. Slot
States

8 variants across 2 axes: State (Default/Active/Error/Disabled) × isFilled (Yes/No). All share the same 366×46px container with 6px corner radius.

Default

Idle state with gray border. Text color depends on whether the field has a value.

Properties
StateDefault
isFilled
Active (Focused)

Focused state with blue border indicating active input.

Properties
StateActive
isFilled
Error

Validation error state with red border.

Properties
StateError
isFilled
Disabled

Non-interactive state with gray background and hidden border.

Properties
StateDisabled
isFilled
Colors by State

All states share the same container structure. Border color is the primary state indicator.

RoleTokenDEFAULTACTIVEERRORDISABLED
Borderfield/border#D7E0EF#005CE5#D61B2Chidden
Backgroundfield/bg#FFFFFF#FFFFFF#FFFFFF#EEF2F9
Text (filled)field/text/filled#0A2757#0A2757#0A2757#90A8D0
Text (empty)field/text/placeholder#90A8D0#90A8D0#90A8D0#C2CFE5
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
isFilled (Yes/No)text: Binding<String>value: StringDerived from text content
State=DefaultDefault idle state
State=Active.focused()interactionSourceKeyboard active
State=Error.ebError(true)isError=trueValidation failed
State=Disabled.disabled(true)enabled=falseNon-interactive
SwiftUI
ios/Components/FormElements/EBInputField.swift
Jetpack Compose
android/components/form/EBInputField.kt
Usage Snippets Planned API
Default
EBInputField("Placeholder", text: $value)
EBInputField(
    value = text,
    onValueChange = { text = it },
    placeholder = "Placeholder"
)
Error
EBInputField("Placeholder", text: $value)
    .ebError(true)
EBInputField(
    value = text,
    onValueChange = { text = it },
    placeholder = "Placeholder",
    isError = true
)
Disabled
EBInputField("Placeholder", text: $value)
    .disabled(true)
EBInputField(
    value = text,
    onValueChange = { text = it },
    placeholder = "Placeholder",
    enabled = false
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 x 44 pt48 x 48 dp
Accessibility label.accessibilityLabel("Input")contentDescription
Error announcementVoiceOver reads error via .accessibilityValueTalkBack reads error via semantics { error() }
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyLayer renamed to #label, now consistent with sibling fields.
C2Variant & Property NamingReadyisFilled now uses true/false. Boolean naming resolved.
C3Token CoveragePartialColors appear correct but token binding not verified.
C4Native MappabilityReadyMaps to TextField (SwiftUI) / OutlinedTextField (Compose).
C5Interaction State CoverageReadyAll 4 states defined: Default, Active, Error, Disabled.
C6Asset & Icon QualityReadyNo icons in base Input Field — text only.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Code Connect
AspectStatusNotes
Property namingReadyisFilled uses true/false — maps directly to native booleans
State coverageReadyAll 4 states defined
Native component filePendingEBInputField.swift / EBInputField.kt not yet created
Variants Inventory (8 total)

4 State values × 2 isFilled values.

StateisFilledNode ID
Defaulttrue17758:3688
Defaultfalse17758:3691
Activetrue17758:3694
Activefalse17758:3697
Errortrue17758:3700
Errorfalse17758:3703
Disabledtrue17758:3706
Disabledfalse17758:3709
1.1.0 — March 2026 Minor
C1 + C2 Fixes · Layer naming and boolean naming resolved
isFilled property renamedisFilled=Yes/No updated to isFilled=true/false in Figma. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. Fixed
C2 Resolved
Text layer renamed#text-label renamed to #label in Figma. Now consistent with sibling fields (Labeled Field, Select Field, Recipient Field). Fixed
C1 Resolved
1.0.0 — March 2026 Initial
Initial Assessment · node 17758:3687
Component assessed — 8 variants documented across State (Default/Active/Error/Disabled) × isFilled (Yes/No). Part of Form Elements group. Documented
Initial
Boolean property uses Yes/NoisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Open
C2 Open
Code Connect mappings — No CLI mappings registered yet. Blocked by C2 (property naming). Open
C7 Open
Labeled Field FixNeeds RefinementComponent link

An 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.

Fix required before handoff
Trailing icon uses rectangle placeholder instead of swappable icon instance (C6). Code Connect mappings not yet registered (C7).
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Live Preview

Toggle state and fill to see the labeled field update in real time.

Properties
State
isFilled
DS Health
Reusable
Partial
Works across form contexts requiring labeled inputs with icons. Single-line only — no multi-line variant. Fixed 46px height with no size options.
Self-contained
Pass
Carries its own border, fill, icon slots, and text styles per state. All 4 interaction states defined. Disabled state has distinct background and hidden border.
Consistent
Partial
Boolean naming and casing fixed (C2 resolved): isFilled now uses true/false and property renamed to State. Action button layer renamed to action-button (C1 resolved).
Composable
Partial
Nests in form layouts. However, trailing-icon uses a rectangle placeholder instead of a swappable icon instance (C6), limiting icon customization at the consumer level.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesState=DefaultGray #D7E0EF border, white bg.
Active (Focused)YesYesState=ActiveBlue #005CE5 border.
ErrorYesYesState=ErrorRed #D61B2C border.
DisabledYesYesState=Disabled#EEF2F9 bg, border hidden.
Resolved Issues
  • isFilled renamed from Yes/No to true/false — now maps directly to Swift Bool / Kotlin BooleanC2 Fixed
  • Property state renamed to State (capitalized) — consistent with sibling Form Elements fields C2 Fixed
  • Button - XSmall layer renamed to action-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
Open Issues
  • Code Connect mappings not registered. Structural work is complete — registration can proceed against the 8-variant State × isFilled schema. C7 · Code Connect Linkability
Design Recommendations
  • Replace hardcoded Button - XSmall with an action slot. Today the trailing action is baked in — consumers can't swap in other button variants or remove it. A named slot makes the action composable via instance swap. Slot
  • Replace trailing-icon placeholder with a swappable icon instance. The current icon-placeholder RECTANGLE blocks designers from overriding the trailing icon without detaching. Follow the instance-swap pattern used elsewhere in the DS. Slot
  • Add a helperText slot. Validation messages and hint copy are handled externally today — a first-class slot keeps the form anatomy self-contained. Slot
States

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.

Default

Idle state with gray border. Leading/trailing icon placeholders, label + value text container, and XSmall action button.

Properties
StateDefault
isFilled
Active (Focused)

Focused state with blue border indicating active input.

Properties
StateActive
isFilled
Error

Validation error state with red border.

Properties
StateError
isFilled
Disabled

Non-interactive state with gray background and hidden border.

Properties
StateDisabled
isFilled
Colors by State

All states share the same container structure. Border color is the primary state indicator. Text colors depend on isFilled (true/false).

RoleTokenDEFAULTACTIVEERRORDISABLED
Borderfield/border#D7E0EF#005CE5#D61B2Chidden
Backgroundfield/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
Layout
Height46px
Corner radius6px
Leading icon24 × 24
Trailing icon24 × 24
Action button60 × 24 (radius 99)
Typography
FontHeyMeow Rnd
WeightSemibold (600)
Size14px
Letter spacing0.25
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
isFilled (true/false)text: Binding<String>value: StringDerived from text content
State=DefaultDefault idle state
State=Active.focused()interactionSourceKeyboard active
State=Error.ebError(true)isError=trueValidation failed
State=Disabled.disabled(true)enabled=falseNon-interactive
#label (TEXT)label: Stringlabel: StringField label text
#value (TEXT)text: Binding<String>value: StringField value text
leading-iconleadingIcon: Image?leadingIcon: @Composable?24×24 icon slot
trailing-icontrailingIcon: Image?trailingIcon: @Composable?24×24 icon slot (C6: rectangle placeholder)
action-buttonaction: EBFieldAction?action: @Composable?Semantic slot name (C1 resolved)
SwiftUI
ios/Components/FormElements/EBLabeledField.swift
Jetpack Compose
android/components/form/EBLabeledField.kt
Usage Snippets Planned API
Default
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) }
)
Error
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
)
Disabled
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
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 x 44 pt48 x 48 dp
Accessibility label.accessibilityLabel("Label")contentDescription
Error announcementVoiceOver reads error via .accessibilityValueTalkBack reads error via semantics { error() }
Action button label.accessibilityLabel("Action") on buttoncontentDescription on button
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyLayer renamed to action-button, now a semantic slot name.
C2Variant & Property NamingReadyisFilled uses true/false. Property renamed to State (capitalized). Both fixes confirmed in Figma.
C3Token CoveragePartialColors appear correct but token binding not verified.
C4Native MappabilityReadyMaps to custom EBLabeledField on both platforms.
C5Interaction State CoverageReadyAll 4 states defined: Default, Active, Error, Disabled.
C6Asset & Icon QualityPartialtrailing-icon uses icon-placeholder RECTANGLE — not a swappable icon instance.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Code Connect
AspectStatusNotes
Property namingReadyisFilled=true/false and State (capitalized) — C2 fixed in Figma, ready for mapping
State coverageReadyAll 4 states defined
Icon slotsPartialleading-icon uses Placeholder instance (OK). trailing-icon uses RECTANGLE (blocked).
Action slotReadyRenamed to action-button — semantic slot name, ready for Code Connect mapping
Native component filePendingEBLabeledField.swift / EBLabeledField.kt not yet created
Variants Inventory (8 total)

4 State values × 2 isFilled values.

StateisFilledNode ID
Defaulttrue17758:3714
Defaultfalse17758:3723
Activetrue17758:3732
Activefalse17758:3741
Errortrue17758:3750
Errorfalse17758:3759
Disabledtrue17758:3768
Disabledfalse17758:3777
1.2.0 — March 2026 Minor
C1 Figma Fix · node 17758:3713
Action button layer renamedButton - XSmall renamed to action-button. Now uses a semantic slot name, enabling flexible consumer customization and clean Code Connect mapping. Fixed
C1 Fixed
1.1.0 — March 2026 Minor
C2 Figma Fix · node 17758:3713
Boolean property renamedisFilled values changed from Yes/No to true/false. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. Fixed
C2 Fixed
Property casing correctedstate renamed to State (capitalized) to align with sibling Form Elements fields (Input Field, etc.). Fixed
C2 Fixed
1.0.0 — March 2026 Initial
Initial Assessment · node 17758:3713
Component assessed — 8 variants documented across State (Default/Active/Error/Disabled) × isFilled (true/false). Part of Form Elements group. Enhanced input with leading icon, label/value text, action button, and trailing icon. Documented
Initial
Boolean property uses Yes/NoisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Open
C2 Open
Lowercase property namestate uses lowercase, inconsistent with other Form Elements using State (capitalized). Open
C2 Open
Hardcoded button instanceButton - XSmall is not a named action slot, limiting consumer customization. Open
C1 Open
Trailing icon uses RECTANGLEicon-placeholder in trailing-icon is a RECTANGLE, not a swappable icon instance. Open
C6 Open
Code Connect mappings — No CLI mappings registered yet. Blocked by C2 (property naming) and C6 (icon quality). Open
C7 Open
List Item Asset RestructureNeeds RefinementComponent link

The 16 × 16 leading marker atom for a List Item — checks, pending dots, bullets, hollow dots, squares, numbered indicators, and a custom asset slot. Currently exposes three entangled properties (type, indicator, state) that only produce ~10 valid combinations out of 72 theoretical.

Flatten the variant matrix + adopt Figma Slots
Replace the entangled type × indicator × state axes with a single semantic variant enum (check, check-positive, pending, pending-notice, bullet, hollow, square, numbered, custom). Replace the Custom placeholder circle with a Figma Slot so product teams can drop in any 16×16 asset without instance-swap.
In Context

List Item Asset appears inside List Items — see the List Item preview for the composed layout.

Live Preview

Pick a variant to see the 16 × 16 marker render.

Proposed API
variant
DS Health
Reusable
Pass
Covers task lists, ordered/unordered lists, and custom-marker lists. Used across onboarding flows, terms pages, and step indicators.
Self-contained
Pass
All colors, sizes, typography bound to tokens. 16 × 16 bounding box is fixed.
Consistent
Warn
Three properties (type, indicator, state) that shouldn't multiply freely — most combinations are invalid. Creates phantom variants in Figma's variant picker. C2
Composable
Warn
indicator=Custom exposes a gray circle icon-placeholder instead of a Figma Slot. Blocks product teams from dropping in custom 16 × 16 assets without local overrides. C6
Open Issues
  • Variant matrix is entangled.type × indicator × state=72 theoretical combinations but only ~10 are valid. Flatten into one semantic variant enum to eliminate invalid combos. C2 · Variant & Property Naming
  • Numbered indicator hardcodes "1." No way to pass the ordinal — numbered lists of 5 items all show "1." in Figma. Expose a number text property. C5 · Interaction State Coverage
  • indicator=Custom is a hardcoded gray circle. Should be a Figma Slot that accepts any 16×16 asset via instance swap. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until variant flatten and slot adoption land. C7 · Code Connect Linkability
Design Recommendations
  • Flatten the variant matrix — replace type + indicator + state with a single semantic variant enum: check / check-positive / pending / pending-notice / bullet / hollow / square / numbered / custom. ~9 real variants, no invalid combinations possible. Property
  • Adopt a Figma Slot for variant=custom — declare a named asset slot so product teams can drop in any 16 × 16 component instance without editing the master. Maps cleanly to a @ViewBuilder slot (SwiftUI) or @Composable slot (Compose) via Code Connect. Slot
  • Parameterize the numbered indicator — expose a number text property so ordered lists can show the actual ordinal. In Figma this is a text override; in native it's a number: Int param. Property
Variants

9 real variants under the proposed flattened enum. All render inside a 16 × 16 bounding box.

All markers

Row of all 9 markers at actual size. Left to right: check, check-positive, pending, pending-notice, bullet, hollow, square, numbered, custom (slot).

Colors by Variant
VariantRoleTokenValue
checkiconmain/list-item/color/default/icon-item#90A8D0
check-positiveiconmain/list-item/color/positive/icon-item#27C990
pendingiconmain/list-item/color/default/icon-item#90A8D0
pending-noticeiconmain/list-item/color/notice/icon-item#CA970C
bullet / hollow / squareiconmain/list-item/color/default/icon-item#90A8D0
numbered (bg)container bgmain/list-item/color/default/icon-bg#EEF2F9
numbered (label)number textmain/list-item/color/default/icon-item#90A8D0
custom(slot — inherits from provided asset)
Layout
PropertyTokenValue
Bounding box16 × 16
Check / Pending icon16 × 16 vector
Bullet / Hollow / Square size5 × 5
Numbered container radius16px (pill)
Numbered padding4L / 2R / 2v
Vertical padding (most variants)space/space-22px
Typography
VariantDS text styleSpec
numbered (pill)Primary/Label/FineHeyMeow Rnd Bold · 12 / 12 · +0.5
Ordered / Normal (legacy)Primary/Label/Light/SmallHeyMeow Rnd Semibold · 14 / 14 · +0.25
Installation Planned API

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:list:1.0.0")
}
Property Mapping (proposed — after flatten)
Current FigmaProposedSwiftUICompose
type × indicator × statevariant (enum).ebVariant(.check)variant=EBListMarker.Check
— (hardcoded "1.")number: Int?number: Int?number: Int?
indicator=Custom placeholderFigma Slot → ViewBuilder / @Composable@ViewBuilder assetasset: @Composable () -> Unit
SwiftUI
ios/Components/List/EBListMarker.swift
Jetpack Compose
android/components/list/EBListMarker.kt
Usage Snippets Planned API
// Standard variants
EBListMarker(variant: .check)
EBListMarker(variant: .checkPositive)
EBListMarker(variant: .pendingNotice)

// Numbered
EBListMarker(variant: .numbered, number: 1)

// Custom — Figma Slot maps to @ViewBuilder
EBListMarker {
    Image(systemName: "star.fill")
        .foregroundStyle(.yellow)
}
// Standard variants
EBListMarker(variant = EBListMarker.Check)
EBListMarker(variant = EBListMarker.CheckPositive)
EBListMarker(variant = EBListMarker.PendingNotice)

// Numbered
EBListMarker(variant = EBListMarker.Numbered, number = 1)

// Custom — Figma Slot maps to @Composable slot
EBListMarker {
    Icon(painterResource(R.drawable.star), contentDescription = null)
}
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic: container, Pending, Checkmark, shape_full.
C2Variant & Property NamingNeeds FixThree entangled axes; most combinations invalid. Flatten to single variant enum.
C3Token CoverageReadyAll colors, typography, spacing bound to tokens.
C4Native MappabilityReadyCustom atom on both platforms — straightforward render switch.
C5Interaction State CoveragePartialNumbered hardcodes "1."; no per-item ordinal. Otherwise display-only.
C6Asset & Icon QualityNeeds FixCustom indicator is a gray circle; should be a Figma Slot.
C7Code Connect LinkabilityPendingNot mapped. Clean mapping lands after flatten + Slot adoption.
Variants Inventory (10 total)
typeindicatorstateNode ID
PlaceholderPlaceholderDefault18482:34426
UnorderedPendingDefault18482:34407
UnorderedPendingNotice18482:34409
UnorderedCheckDefault18482:34411
UnorderedCheckPositive18482:34413
UnorderedBulletDefault18482:34415
UnorderedHollowDefault18482:34417
UnorderedSquareDefault18482:34419
OrderedCustomDefault18482:34423
OrderedNormalDefault18482:34421

After flatten + Slot adoption, these 10 collapse to 9 semantic variants + 1 custom slot — 0 invalid combinations possible.

1.0.0 — April 2026 Initial
Initial Assessment · node 18482:34406
Component assessed — 10 variants across entangled type × indicator × state. Recommended flatten + Figma Slot adoption. Documented
Initial
Variant matrix entangled — 72 theoretical, ~10 valid. Flatten to one variant enum. Open
C2 Open
Numbered indicator hardcodes "1." — Needs a number parameter. Open
C5 Open
Custom indicator is a placeholder — Should be a Figma Slot. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
List Item FixNeeds RefinementComponent link

A single row in a list: leading asset + body text. 3 variants by level (1, 2, 3) that control indent (0 / 16 / 32 px). Composes a List Item Asset instance for the leading marker.

Adopt Figma Slots for the asset
The leading asset is today an instance-swap placeholder. Declare a named asset Slot so consumers can drop in any List Item Asset variant (or a custom 16 × 16 component) directly. Maps 1 : 1 to @ViewBuilder / @Composable slots for Code Connect. Also rename level to an integer or drop it in favor of nesting-based indent.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. List Items compose into multi-line lists on terms pages, onboarding steps, and task checklists.

Terms
Live Preview

Toggle level + asset variant. The asset slot accepts any List Item Asset.

Properties
level
asset
DS Health
Reusable
Pass
Used in terms, onboarding, checklists, step indicators. Handles 3 levels of nesting via indent.
Self-contained
Pass
Carries gap, indent, and typography bindings. Body text uses Secondary/Bold/Base (BarkAda Semibold 14/20).
Consistent
Warn
level uses string values ("1"/"2"/"3") instead of integers. Figma won't let you parameterize it either — each level is a separate variant. C2
Composable
Warn
Leading asset uses instance-swap (default is a gray placeholder circle). Moving to a Figma Slot gives a cleaner composition surface and a direct slot mapping for Code Connect. C6
Open Issues
  • level property uses string values. Should be an integer, or — ideally — dropped in favor of indent inferred from nesting depth inside the List container. C2 · Variant & Property Naming
  • Leading asset is an instance-swap placeholder. Adopt Figma Slots so the asset becomes a first-class slot mapping to @ViewBuilder (SwiftUI) / @Composable (Compose) via Code Connect. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until level rename and the asset-slot adoption land. C7 · Code Connect Linkability
Design Recommendations
  • Adopt a Figma Slot for the asset — declare a named asset slot that accepts any List Item Asset instance (or a bare 16 × 16 component). Native maps to @ViewBuilder (SwiftUI) / @Composable slot (Compose) — Code Connect reads it as a real slot parameter. Slot
  • Rename or drop level — if keeping the prop, change string values to integers. Better: drop the variant entirely and let indent come from nesting depth inside the List container. Rename
  • Consider a Trailing slot — product patterns often pair list items with trailing counters, badges, or chevrons. Adding a trailing slot future-proofs the component without creating variant explosion. Slot
Variants

3 variants split by indent level. Width ranges from hug (level 1) to 294px (levels 2 + 3).

Level 1 — no indent

Base row. Asset + 270px body. No indent.

Level 2 — 16px indent

Indented 16px. Body fills remaining width of the 294px container.

Level 3 — 32px indent

Indented 32px. Deepest supported level.

Colors by State
RoleTokenValue
Body textmain/list-item/color/default/description#445C85

Asset colors are documented on List Item Asset.

Layout
PropertyTokenValue
Asset → body gapspace/space-88px
Level 2 indentspace/space-1616px
Level 3 indentspace/space-3232px
Level 1 body width270px
Levels 2 + 3 container width294px
Body alignmentitems-start (asset top-aligned)
Typography
ElementDS text styleSpec
BodySecondary/Bold/BaseBarkAda Semibold · 14 / 20
Installation Planned API

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:list:1.0.0")
}
Property Mapping (proposed — with Figma Slots)
FigmaSwiftUIComposeNotes
asset (Slot)@ViewBuilder leadingleading: @Composable () -> UnitFirst-class slot — accepts any List Item Asset or custom 16 × 16 component
level=1/2/3level: Intlevel: IntControls indent. Better dropped entirely with nesting-based indent.
text contentcontent: Stringcontent: StringBody text
SwiftUI
ios/Components/List/EBListItem.swift
Jetpack Compose
android/components/list/EBListItem.kt
Usage Snippets Planned API
// Default bullet
EBListItem("Transactions are reviewed within 24 hours.") {
    EBListMarker(variant: .bullet)
}

// Ordered with number
EBListItem("Enter your GCash PIN to continue.") {
    EBListMarker(variant: .numbered, number: 1)
}
.level(2)

// Custom asset via slot
EBListItem("Saved to Favorites") {
    Image(systemName: "heart.fill")
        .foregroundStyle(.red)
}
// Default bullet
EBListItem(content = "Transactions are reviewed within 24 hours.") {
    EBListMarker(variant = EBListMarker.Bullet)
}

// Ordered with number, level 2
EBListItem(
    content = "Enter your GCash PIN to continue.",
    level = 2
) {
    EBListMarker(variant = EBListMarker.Numbered, number = 1)
}

// Custom asset via slot
EBListItem(content = "Saved to Favorites") {
    Icon(painterResource(R.drawable.heart), contentDescription = null, tint = Color.Red)
}
Accessibility
RequirementiOSAndroid
List container roleWrap List Items in a List for VoiceOver row semanticsUse Modifier.semantics { collectionInfo=... } on parent
Marker semanticsDecorative markers: .accessibilityHidden(true) on the assetSame — contentDescription=null
Numbered listsPrepend number to the announced label, or use .accessibilityValueInclude number in contentDescription
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySimple row: asset + body text.
C2Variant & Property NamingPartiallevel uses string values; consider dropping the prop entirely.
C3Token CoverageReadyBody color, indent, gap tokens all bound.
C4Native MappabilityReadyHStack / Row with slot.
C5Interaction State CoverageReadyDisplay-only; interaction lives on the consumer.
C6Asset & Icon QualityPartialInstance-swap works; Figma Slot would be cleaner and map to native slot params.
C7Code Connect LinkabilityPendingNot mapped. Slot adoption improves mapping quality.
Variants Inventory (3 total)
levelIndentWidthNode ID
10hug (body 270px)18482:34430
216px294px18482:34433
332px294px18482:34436
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:34429
Component assessed — 3 variants (level 1/2/3). Composes List Item Asset via instance swap. Documented
Initial
level uses string values — Should be integer or dropped in favor of nesting-based indent. Open
C2 Open
Asset is instance-swap — Adopt Figma Slots for first-class slot mapping. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
List RestructureN/AComponent link

A frame containing 8 hardcoded List Item instances stacked with an 8px gap. No component properties, no variants. Functions as a layout example on the sticker sheet rather than a reusable component.

Remove or restructure
List today is a frame of 8 hardcoded List Item instances — not a reusable component. Two paths forward: (1) remove it from the sticker sheet and keep List Item as the published atom; (2) restructure into a real container that accepts a collection of items (same approach proposed for Tabs).
Current Figma

The List frame as-is: 8 List Item instances with an 8px gap. Three of them use indent levels to demonstrate nesting.

DS Health
Reusable
Fail
Not reusable — consumers can't populate this with their own items. They must detach or rebuild from List Item atoms.
Self-contained
Warn
Carries only the parent flex layout + 8px gap. All real structure lives on the child List Items.
Consistent
Fail
Not a component in the DS sense — it's a frame posing as one. Breaks the pattern established by Tabs (container + atom), Menu Grid + Service Item, Avatar Group + Avatar.
Composable
Pass
Each child is a List Item instance — at least the composition is correct.
Open Issues
  • Not a real component. List today is a frame with 8 hardcoded List Item instances — no property set, no variants, no slots. Consumers can't populate it with their own items without detaching and rebuilding from the List Item atom. C2 · Variant & Property Naming
Design Recommendations
  • Remove List from the sticker sheet. Publish List Item (and List Item Asset) as the shipped components. Consumers stack List Items themselves using auto-layout — same pattern as plain text lists. Keeps the sticker sheet focused on reusable atoms. Family
  • Or restructure into a real container. Expose a single flexible List component with auto-layout that accepts a collection of List Items (same approach proposed for Tabs dropping tabsCount). Native maps to ForEach / LazyColumn. Provides a documented home for list-level concerns like spacing, dividers, or separators. Property
  • If restructuring, consider adding list-level props for spacing (gap=8/12/16) and an optional divider between items. These are the decisions that logically belong to a container, not an item. Property
Native equivalent Planned API

Since List isn't a real component today, native consumers simply stack List Items. If the restructure option is pursued, a real EBList composable would wrap the iteration.

// Option A — just stack List Items
VStack(alignment: .leading, spacing: 8) {
    ForEach(items) { item in
        EBListItem(item.text) {
            EBListMarker(variant: item.marker)
        }
    }
}

// Option B — EBList container (if restructure)
EBList(items: items, gap: 8) { item in
    EBListItem(item.text) {
        EBListMarker(variant: item.marker)
    }
}
// Option A — just stack List Items
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
    items.forEach { item ->
        EBListItem(content = item.text) {
            EBListMarker(variant = item.marker)
        }
    }
}

// Option B — EBList container (if restructure)
EBList(items = items, gap = 8.dp) { item ->
    EBListItem(content = item.text) {
        EBListMarker(variant = item.marker)
    }
}
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyTrivial wrapper — List Item children named correctly.
C2Variant & Property NamingN/ANo properties.
C3Token CoverageReadyGap bound to space/space-8.
C4Native MappabilityN/AMaps to plain VStack / Column. No DS-specific component needed unless restructured.
C5Interaction State CoverageN/ALayout only.
C6Asset & Icon QualityN/ANo assets.
C7Code Connect LinkabilityN/ANot a linkable DS component in current form.
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:34737
Assessed as layout-only — Frame of 8 hardcoded List Item instances. Not a reusable component in its current form. Documented
Initial
Recommendation: Remove or Restructure — Either remove from the sticker sheet (List Item is the shipped atom) or restructure into a real container that accepts a collection. Open
Design Decision
Menu Grid FixNeeds RefinementComponent link

Dashboard service grid — a 336px-wide layout container that arranges Service Item instances (icon + label) in a configurable grid. 20 variants combine Row (2/3/4/5) and Column (1/2/3/4/5). Used for service shortcuts on dashboard surfaces (Send Money, Pay Bills, Cash In, etc.).

Open issues remain
Variant property values use pseudo-numeric strings ("by 4") instead of integers (C2). Service Item only ships an active color set — no pressed/disabled tokens (C5). Code Connect mappings not yet registered (C7).
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. Menu Grid sits on the dashboard as the primary service shortcut surface — typically Row=2, Column=4 (8 services) on the home screen.

GCash
Live Preview

Toggle Row × Column to see the grid recalculate. Each cell is a Service Item (icon + label).

Properties
Row
Column
DS Health
Reusable
Pass
Used on dashboard surfaces and any screen needing a uniform service shortcut grid. 20 row/column combinations cover most layout needs from a single column list to a 5×5 grid.
Self-contained
Pass
Container carries its own background, padding (8h / 10t / 6b), gap (4×4), and bottom radius (6). All values bound to design tokens.
Consistent
Warn
Variant property values are pseudo-numeric strings ("by 4") instead of clean integer enums. Should be rows: 4 / columns: 4 for native parity. C2
Composable
Pass
Cleanly composes Service Item instances (node 17787:1700). Service Item is a separate canonical component — icon and label are easily overridable per cell.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesRow × ColumnActive state only — uses dashboard/service-item/color/active/{icon,label}
PressedN/AN/ANo pressed token defined for Service Item. Native may need to derive from icon brand color. C5
DisabledN/AN/ANo disabled token defined. C5

Menu Grid is a layout container; tap behavior lives on each Service Item child.

Open Issues
  • Variant values use pseudo-numeric strings.Row="by 4", Column="by 4" can't map cleanly to native ints or enums. Should be plain integers (rows: 4, columns: 4). C2 · Variant & Property Naming
  • Service Item only defines an active color set. No pressed or disabled tokens exist — engineers must invent these colors on native. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the variant explosion and token gaps are addressed. C7 · Code Connect Linkability
Design Recommendations
  • Replace the 20-variant matrix with two integer props.rows and columns as ints. Variant explosion is a Figma-only construct; native uses a lazy grid that takes any int. Could collapse to ~1–2 variants. Property
  • Add pressed and disabled color tokens. Extend dashboard/service-item/color/* so all states are documented at the token layer, not improvised in code. Token
  • Promote Service Item to a first-class DS component. It's currently a child of Menu Grid but is reused independently across other surfaces. Publishing it standalone (same pattern as Avatar + Avatar Group) makes reuse explicit. Family
Variants

20 variants formed by Row (2/3/4/5) × Column (1/2/3/4/5). All share the same 336px container width, 4×4 gap, and 8/10/6 padding. Cell count=Row × Column.

Row 2 × Column 4 — 8 services (most common)

The default dashboard layout — 2 rows × 4 columns=8 services. Used on the home dashboard for primary service shortcuts.

Row 4 × Column 4 — 16 services

Expanded grid for "All Services" sheets — 4 rows × 4 columns=16 services. Used on category pages or full-list views.

Row 5 × Column 5 — 25 services (max)

Maximum density — 5 rows × 5 columns=25 services. Use sparingly; label legibility tightens at this density.

Colors by State

Service Item ships only the active color set. Pressed and disabled are not yet defined at the token layer — see C5 in the Open Issues.

RoleTokenValue
Container bgbg/color-bg-main#FFFFFF
Service icondashboard/service-item/color/active/icon#005CE5
Service labeldashboard/service-item/color/active/label#072592
Border (weak)border/color-border-weak#E5EBF4
Layout
PropertyTokenValue
Container width336px (fixed)
Padding (horizontal)space/space-88px
Padding (top)space/space-1010px
Padding (bottom)space/space-66px
Cell gap (row & col)space/space-44px
Bottom radiusradius/radius-26px
Service Item min-width64px
Service Item icon container40 × 40
Icon paddingspace/space-44px
Icon → label gapspace/space-66px
Typography (Service Item Label)
PropertyValue
DS text stylePrimary/Label/Fine
FontHeyMeow Rnd
Weight700 (Bold)
Size12px
Line height12px
Tracking+0.5
Alignmentcenter
Installation Planned API

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:menu-grid:1.0.0")
}
Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
Row="by N"rows: Introws: IntNative takes a plain integer; Figma string parsed to N
Column="by N"columns: Intcolumns: IntSame — integer parameter
Service Item (instance)items: [EBServiceItem]items: List<EBServiceItem>Each cell is a Service Item with icon + label
SwiftUI
ios/Components/MenuGrid/EBMenuGrid.swift
Jetpack Compose
android/components/menugrid/EBMenuGrid.kt
Usage Snippets Planned API
// Default dashboard grid — 2 rows × 4 columns
EBMenuGrid(columns: 4, items: services)

// Service Item child
EBServiceItem(
    icon: Image("send-money"),
    label: "Send Money",
    action: { /* tap */ }
)
// Default dashboard grid — 2 rows × 4 columns
EBMenuGrid(
    columns = 4,
    items = services
)

// Service Item child
EBServiceItem(
    icon = painterResource(R.drawable.send_money),
    label = "Send Money",
    onClick = { /* tap */ }
)
Accessibility
RequirementiOSAndroid
Tap targetService Item is 64+ px wide × ~64 px tall — meets HIG 44pt minimumMeets Material 48dp minimum
Accessibility label.accessibilityLabel(label) on each itemcontentDescription=label
Grid semanticsContainer exposes grid traits via LazyVGridLazyVerticalGrid announces row/column position
Disabled stateCurrently undefined — needs token + .disabled(true) handlingCurrently undefined — needs token + enabled=false
Usage Guidelines

Do

Use Row=2 × Column=4 for the primary dashboard surface — 8 services is the established home pattern.

Don't

Use Row=5 × Column=5 unless density is essential — 25 cells reduces label legibility.

Do

Pair Menu Grid with a section heading or container card so users understand the grouping.

Don't

Mix icon styles within a single grid — keep all Service Item icons in the same vector style.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: Service Item, icon-container, Label. No Frame/Group debris.
C2Variant & Property NamingNeeds FixProperty values are pseudo-numeric strings ("by 4") instead of integers. Native takes Int.
C3Token CoverageReadyAll colors, spacing, radii, and typography bound to tokens.
C4Native MappabilityPartialMaps to LazyVGrid / LazyVerticalGrid. The 20-variant matrix is Figma-only — native uses one component with two int props.
C5Interaction State CoverageNeeds FixOnly active token defined. No pressed or disabled color tokens for Service Item.
C6Asset & Icon QualityReadyIcon container is a vector instance, token-bound color.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Variants Inventory (20 total)

4 Row × 5 Column=20 variants. All variants share the same 336px container, 4×4 cell gap, and Service Item child.

RowColumnsCell rangeCount
by 2by 1, 2, 3, 4, 52 – 105
by 3by 1, 2, 3, 4, 53 – 155
by 4by 1, 2, 3, 4, 54 – 205
by 5by 1, 2, 3, 4, 55 – 255
View full Row × Column breakdown (20 rows)
RowColumnCellsNode ID
by 2by 1218320:14413
by 2by 2418320:14407
by 2by 3618320:14398
by 2by 4818320:14371
by 2by 51018320:14383
by 3by 1318320:14423
by 3by 2618320:14416
by 3by 3918320:14563
by 3by 41218320:14550
by 3by 51518320:14534
by 4by 1418320:14503
by 4by 2818320:14477
by 4by 31218320:14448
by 4by 41618320:14333
by 4by 52018320:14350
by 5by 1518320:14497
by 5by 21018320:14486
by 5by 31518320:14461
by 5by 42018320:14427
by 5by 52518320:14508
1.0.0 — April 2026 Initial
Initial Assessment · node 18320:14332
Component assessed — 20 variants formed by Row × Column (Row 2/3/4/5, Column 1/2/3/4/5). Layout container of Service Item children. 336px fixed width. Documented
Initial
Variant property values are strings, not integersRow="by 4", Column="by 4". Native takes Int; the string prefix forces parsing. Open
C2 Open
Service Item missing pressed/disabled tokens — Only dashboard/service-item/color/active/{icon,label} defined. Other states must be improvised. Open
C5 Open
Code Connect mappings — No CLI mappings registered yet. Open
C7 Open
Modal RestructureReworkComponent link

A centered dialog layer that surfaces content requiring user interaction on top of a dimmed page. Ships as 7 variants crossing type (default / with icon / transaction_v1 / transaction_v2) and cta (1 / 1-vertical / 2-horizontal / 2-vertical). Uses main/modal-popup/color/* tokens for bg, label, border, and accent. Description copy is typeset in the secondary font BarkAda.

Restructure — duplicates Overlay scope and compresses two unrelated layouts
A single Modal component is trying to serve both a general-purpose dialog (default / with icon) and a specialised transaction-receipt layout (transaction_v1 / v2). These are not "variants" of the same thing — they have different information architectures, different tokens, and different native mappings. On top of that, Modal overlaps in name and scope with the existing Overlay component (47:329691), which currently ships the scrim only. Consolidate the family: one canonical Modal that owns the surface + scrim, and a separate Transaction Receipt Card for the receipt layout.

Modal sits centered over a dimmed page with the Overlay scrim behind it. It blocks interaction with background content until the user resolves the dialog.

Toggle between the four type values and the four cta arrangements. Not every combination exists in Figma — the picker falls back to the closest shipped variant.

Reusable
Warn
General-dialog variants (default / with icon) are reusable across many screens, but transaction_v1 / v2 are specific to the receipt / order-summary use case and shouldn't live inside a generic Modal.
Self-contained
Partial
Modal owns its bg, border, shadow, and label tokens, but relies on an external Overlay to dim the page. The relationship between the two components is not annotated anywhere.
Consistent
Warn
Enum casing is mixed within the same property: transaction_v1 / transaction_v2 use snake_case, 1 - vertical / 2 - horizontal use space-dashed-space. Also duplicates scope with the Overlay component.
Composable
Warn
Transaction variants hard-code inner transaction rows and a reference-number slot — consumers can't swap content. Should be split into a Modal shell + a composable Transaction Receipt child.
BehavioriOSAndroidFigma SpecNotes
Present / dismissYesYesNot annotatedScale-in + fade animation is implied by pattern but not documented on the component.
Tap-outside dismissYesYesNot annotatedOverlay owns the tap-region. Contract should be documented (dismissible vs. modal).
CTA resolutionYesYesVia Button child1 / 2-horizontal / 2-vertical layouts maped by cta property.
Copy to clipboard (transaction)YesYesIcon only, no stateCopy icon is raster, with no pressed / success feedback state defined.
Focus trap / a11yYesYesImplicitFocus should be trapped inside the modal while open; restore to trigger on close.
  • Duplicate scope with Overlay component. A separate Overlay record (node 47:329691) already owns the scrim primitive; this Modal should compose it, not re-declare the modal surface. Today the two are maintained independently and there's no annotation describing which is canonical. C7 · Code Connect Linkability
  • Two unrelated layouts compressed into one component.type=default and type=with icon are general-purpose dialog shapes, while type=transaction_v1 and type=transaction_v2 are transaction-receipt layouts with their own inner rows and reference-number slot. These are different components masquerading as variants. C4 · Native Mappability
  • Mixed enum casing within a single property.type uses default, with icon (space), transaction_v1, transaction_v2 (snake_case). cta uses 1, 1 - vertical, 2 - horizontal, 2 - vertical (spaces around dashes). Neither is consistent with the rest of the DS. C2 · Variant & Property Naming
  • Opacity-0 spacer frames instead of gap. Inner layers named _space_16, _space_12 with opacity:0 are used to create vertical rhythm. These are non-semantic and don't translate to native auto-layout. Use gap on the parent instead. C1 · Layer Structure & Naming
  • Raster copy-to-clipboard icon. The transaction variants use PNG assets shape_half / shape_full for the copy icon. Should be a vector icon instance bound to main/modal-popup/color/icon-copy. C6 · Asset & Icon Quality
  • Icon-placeholder slot is a grey circle, not an instance swap. The with icon variants render a raw #C2C6CF circle (icon-placeholder). Consumers can't swap in a real icon without detaching. Should be a Slot backed by the Icon component. C1 · Layer Structure & Naming
  • No interaction states on the modal surface. The component ships only a default state — no pressed / dragging state for the CTA group, no loading state for async actions, and no entrance/exit transition annotation. C5 · Interaction State Coverage
  • Missing CTA combos. Transaction variants only ship with cta=1; "with icon" only ships with vertical CTAs. The cta axis is not complete across all type values, so designers resort to detaching when they need another arrangement. C2 · Variant & Property Naming
  • No Code Connect mapping. Blocked on restructure — once the transaction layout is extracted and the enum values are cleaned up, mapping is trivial. C7 · Code Connect Linkability
  • Consolidate with Overlay — one canonical Modal family. The current split (Overlay=scrim only, Modal=surface + scrim baked in) duplicates intent. Rename and re-partition into: Overlay (scrim primitive, already assessed) + Modal (surface composition that references Overlay) + TransactionReceipt (the transaction_v1/v2 layout pulled out as its own card). Document which component owns which concern. Family
  • Extract transaction_v1 / transaction_v2 as a separate component. Move the receipt layout into a new Transaction Receipt component (likely a composition of List + Reference Number + Copy action), and drop the transaction_* values from Modal's type enum. Modal keeps only general-dialog variants. Composition
  • Normalise enum casing. Pick one convention for all multi-word values. Recommendation: single lowercase words separated by dashes — default, with-icon, cta-single, cta-single-vertical, cta-double-horizontal, cta-double-vertical. Align with Button and other DS components. Rename
  • Replace opacity-0 spacer frames with auto-layout gap. Remove the _space_16 / _space_12 invisible rectangles and set gap on the parent auto-layout frames (using space/space-16 and space/space-12 tokens). Native translators can then emit proper spacing parameters. Property
  • Convert icon-placeholder into a Figma Slot. Add a named icon slot to the with icon variants backed by the DS Icon component, so designers can instance-swap without detaching. Default to a neutral status icon. Slot
  • Replace raster copy icon with a vector instance. Swap shape_half / shape_full PNGs for the DS vector Copy icon and bind colour to main/modal-popup/color/icon-copy. While there, add a pressed / copied success state. Asset
  • Complete the CTA matrix. Ship every type × cta combination (or constrain the schema so unsupported combos aren't implied). Currently default is missing 1-vertical, with icon is missing horizontal pairs, and transactions only ship with cta=1. Either fill the gaps or reshape the enum. Property
  • Add loading and destructive states. Modals routinely host async confirmations — add a state=loading variant (CTA replaced with spinner) and surface destructive-action styling via a boolean or via the child Button's existing isError prop. State
  • Annotate the dismiss contract. Document on the component: entrance / exit animation, focus trap, restore-focus-on-close, ESC-to-dismiss, tap-outside-dismiss. Developers currently have to infer these from adjacent patterns. Docs
  • Title + description copy should come from the DS text styles. Title is bound to Primary/Headlines/Section and description to Secondary/Default/Base (BarkAda). Confirm the secondary-font description is intentional — flag as the standing custom-font action item if not. Docs
Default node 18507:71792

The general-purpose dialog. Title + description + single CTA on a white card. Use for confirmations, errors, and neutral informational prompts.

Properties
NameModal
typedefault
cta1 / 2 - horizontal / 2 - vertical
Title slotText · Primary/Headlines/Section
Description slotText · Secondary/Default/Base
CTA slotButton instance(s)
Colors by State
ROLETOKENVALUE
Surface bgmain/modal-popup/color/bg#FFFFFF
Title labelmain/modal-popup/color/label#0A2757
Descriptionmain/modal-popup/color/label-primary#6780A9
Bordermain/modal-popup/color/border#E5EBF4
ShadowShadow/Depth 0#E8EEF2 · 79%
Bg, label, description, and border all resolve to the main/modal-popup/color/* collection. No hardcoded values in the default variant.
Layout
Width320
Height (cta=1)212
Height (cta=2-h)212
Height (cta=2-v)270
Padding24 / 32 top · 24 sides
Corner radius6
Bordernone (shadow only)
CTA group paddingpy 24
Typography
Title stylePrimary/Headlines/Section
Title fontProxima Soft · Bold
Title size22 / 26
Description styleSecondary/Default/Base
Description fontBarkAda · Medium
Description size14 / 20
Alignmentcenter
// Default modal · single CTA
.sheet(isPresented: $showModal) {
    EBModal(
        title: "Put the title here",
        description: "Add description here."
    ) {
        EBButton("Label") { showModal = false }
    }
    .presentationDetents([.medium])
}
// Default modal · single CTA
if (showModal) {
    EBModal(
        title = "Put the title here",
        description = "Add description here.",
        onDismissRequest = { showModal = false }
    ) {
        EBButton(onClick = { showModal = false }) {
            Text("Label")
        }
    }
}
With Icon node 18507:71773 / 18507:71783

Dialog that leads with a 92×92 icon to set tone — success, warning, or info. CTAs stack vertically (1 or 2).

Properties
typewith icon
cta1 - vertical / 2 - vertical
Icon slot92 × 92 placeholder circle (should be Slot)
Title slotText · Primary/Headlines/Section
Description slotText · Secondary/Default/Base
Colors by State
ROLETOKENVALUE
Surface bgmain/modal-popup/color/bg#FFFFFF
Title labelmain/modal-popup/color/label#0A2757
Descriptionmain/modal-popup/color/label-primary#6780A9
Icon placeholder(hardcoded)#C2C6CF
Icon placeholder #C2C6CF is hardcoded — it should be replaced with a proper Icon slot that binds its colour to a semantic token.
Layout
Width320
Height (cta=1-v)312
Height (cta=2-v)370
Icon container92 × 92
Icon radius~72.5 (circle)
Gap icon → title16
Gap title → desc16
CTA gap8 (vertical)
Typography
Title stylePrimary/Headlines/Section
Title fontProxima Soft · Bold · 22 / 26
Description styleSecondary/Default/Base
Description fontBarkAda · Medium · 14 / 20
Alignmentcenter
// With icon · two vertical CTAs
.sheet(isPresented: $showModal) {
    EBModal(
        icon: Image("success-badge"),
        title: "Transfer successful",
        description: "Funds sent to Juan Dela Cruz."
    ) {
        EBButton("Done") { showModal = false }
        EBOutlinedButton("Share receipt") { … }
    }
}
// With icon · two vertical CTAs
EBModal(
    icon = { Icon(painterResource(R.drawable.success_badge), null) },
    title = "Transfer successful",
    description = "Funds sent to Juan Dela Cruz.",
    onDismissRequest = { showModal = false }
) {
    EBButton(onClick = { showModal = false }) { Text("Done") }
    EBOutlinedButton(onClick = {}) { Text("Share receipt") }
}
Transaction (v1 · v2) node 18507:71706 / 18507:71732

Receipt-style dialog used for order, transfer, and subscription summaries. v1 stacks label + value per row; v2 is horizontal. Both include a reference-number row with copy-to-clipboard. Recommended for extraction into its own TransactionReceipt component.

Properties
typetransaction_v1 / transaction_v2
cta1 (only)
Detail rows3 per variant, stacked (v1) or inline (v2)
Reference rowLabel + value + copy icon
Copy iconRaster PNG (shape_half, shape_full)
Colors by State
ROLETOKENVALUE
Content bgmain/modal-popup/color/bg#FFFFFF
Footer bgmain/modal-popup/color/bg-subtle#F6F9FD
Title / valuemain/modal-popup/color/label#0A2757
Row labelmain/modal-popup/color/label-primary#6780A9
Dividermain/modal-popup/color/border#E5EBF4
Copy iconmain/modal-popup/color/icon-copy#005CE5
All colours resolve to tokens, but the copy icon itself is a raster PNG that cannot be recoloured via the token in Figma.
Layout
Width320
Height (v1)398
Height (v2)404
Content padding24 all sides
Reference row padding16 top · 8 bottom · 24 sides
CTA padding8 top · 24 bottom · 24 sides
Row gap (v1)12
Row gap (v2)12
Copy icon24 × 24
Typography
Title stylePrimary/Headlines/Section
Title fontProxima Soft · Bold · 22 / 26
Section textPrimary/Multi-line Label/Light/Base · 16 / 20
Row label / valuePrimary/Label/Light/Small · 14 / 14
Multi-line row (v1)Primary/Multi-line Label/Light/Small · 14 / 16
Alignmentleft (v1/v2 body), center (title)
// Recommended — composed from extracted receipt component
.sheet(isPresented: $showReceipt) {
    EBModal(title: "Transaction details") {
        EBTransactionReceipt(
            rows: rows,
            referenceNumber: "165A25912345",
            layout: .inline  // or .stacked
        )
        EBButton("Done") { showReceipt = false }
    }
}
// Recommended — composed from extracted receipt component
EBModal(
    title = "Transaction details",
    onDismissRequest = { showReceipt = false }
) {
    EBTransactionReceipt(
        rows = rows,
        referenceNumber = "165A25912345",
        layout = ReceiptLayout.Inline
    )
    EBButton(onClick = { showReceipt = false }) { Text("Done") }
}
// Swift Package Manager
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
// build.gradle.kts
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBModal
Figma propertySwiftUICompose
type=defaultEBModal(title:, description:)EBModal(title, description)
type=with iconEBModal(icon:, title:, description:)EBModal(icon={ … }, title, description)
type=transaction_v1 / v2Extract → EBTransactionReceipt(layout: .stacked / .inline)Extract → EBTransactionReceipt(layout=Stacked / Inline)
cta=1{ EBButton(…) } (single trailing closure)content: { EBButton(…) }
cta=1 - verticalImplicit — single button is always full-widthImplicit — single button is always full-width
cta=2 - horizontal{ EBButton(…); EBOutlinedButton(…) } + .ctaLayout(.horizontal)ctaLayout=CtaLayout.Horizontal
cta=2 - vertical.ctaLayout(.vertical)ctaLayout=CtaLayout.Vertical
(proposed) Dismissible.interactiveDismissDisabled(!dismissible)DialogProperties(dismissOnClickOutside=dismissible)
(proposed) Loading state.ebLoading(isLoading)isLoading=true
  • ios/Components/Modal/EBModal.swift
  • ios/Components/Modal/EBTransactionReceipt.swift (extracted)
  • android/components/modal/EBModal.kt
  • android/components/modal/EBTransactionReceipt.kt (extracted)
// 1 · Default · single CTA
.sheet(isPresented: $showConfirm) {
    EBModal(title: "Delete account?", description: "This cannot be undone.") {
        EBButton("Delete", role: .destructive) { delete() }
    }
}

// 2 · With icon · two vertical CTAs
.sheet(isPresented: $showSuccess) {
    EBModal(
        icon: Image("check-badge"),
        title: "Transfer successful",
        description: "₱1,500 sent to Juan Dela Cruz."
    ) {
        EBButton("Done") { showSuccess = false }
        EBOutlinedButton("Share receipt") { share() }
    }
    .ctaLayout(.vertical)
}

// 3 · Two horizontal CTAs (cancel/confirm)
.sheet(isPresented: $showConfirm) {
    EBModal(title: "Send money?", description: "You're about to send ₱1,500.") {
        EBOutlinedButton("Cancel") { showConfirm = false }
        EBButton("Confirm") { confirm() }
    }
    .ctaLayout(.horizontal)
}

// 4 · Transaction receipt (extracted — recommended)
.sheet(isPresented: $showReceipt) {
    EBModal(title: "Order details") {
        EBTransactionReceipt(rows: rows, referenceNumber: ref)
        EBButton("Done") { showReceipt = false }
    }
}
// 1 · Default · single CTA
if (showConfirm) {
    EBModal(
        title = "Delete account?",
        description = "This cannot be undone.",
        onDismissRequest = { showConfirm = false }
    ) {
        EBButton(onClick = ::delete, colors = EBButtonDefaults.destructiveColors()) {
            Text("Delete")
        }
    }
}

// 2 · With icon · two vertical CTAs
EBModal(
    icon = { Icon(painterResource(R.drawable.check_badge), null) },
    title = "Transfer successful",
    description = "₱1,500 sent to Juan Dela Cruz.",
    ctaLayout = CtaLayout.Vertical,
    onDismissRequest = { }
) {
    EBButton(onClick = {}) { Text("Done") }
    EBOutlinedButton(onClick = ::share) { Text("Share receipt") }
}

// 3 · Transaction receipt (extracted — recommended)
EBModal(title = "Order details", onDismissRequest = {}) {
    EBTransactionReceipt(rows = rows, referenceNumber = ref)
    EBButton(onClick = {}) { Text("Done") }
}
RequirementiOSAndroid
Modal traitApply .accessibilityAddTraits(.isModal) — VoiceOver trap focus inside.Use Dialog / AlertDialog — TalkBack treats content as modal by default.
Focus managementFocus moves to modal on present; restores to trigger on dismiss.Focus enters dialog content on show; restored to trigger element on dismiss.
Title announcementBind the title Text as the accessibilityHeading so it's read first.Use Modifier.semantics { heading() } on the title; set paneTitle on the surface.
Dismiss gestureESC / tap-outside / swipe-down should all route through one dismiss handler.Back gesture + tap-outside configured via DialogProperties(dismissOnBackPress, dismissOnClickOutside).
Destructive actionUse role: .destructive on the CTA so VoiceOver announces destructive intent.Use destructive colour palette; set contentDescription on CTA explicitly.
Copy to clipboardAnnounce "Copied" via UIAccessibility.post(.announcement, …).Announce via view.announceForAccessibility("Copied").
Tap target (copy icon)Wrap 24×24 icon in a ≥44×44 tappable area.Wrap 24×24 icon in a ≥48×48 dp tappable area.
Do
  • Use Modal for high-friction confirmations, destructive actions, and success / error acknowledgements.
  • Always pair the modal surface with the Overlay scrim — never float the card on bare background.
  • Lead with a clear title; keep description to two lines where possible.
  • Reserve the with icon variant for tone-setting moments (success, warning, error).
  • Extract Transaction variants into a dedicated Receipt component and compose it inside Modal.
Don't
  • Don't use Modal as a full-screen navigation surface — use a route / screen instead.
  • Don't stack modals on top of each other; resolve the current one first.
  • Don't hardcode the icon-placeholder grey (#C2C6CF) — swap in a real DS Icon instance.
  • Don't rely on the opacity-0 spacer frames for rhythm — they don't translate to native.
  • Don't fork Modal or Overlay into a third "Modal" variant somewhere else in the file.
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds RefinementOpacity-0 _space_* spacer rectangles used instead of gap. Icon-placeholder is a raw circle node, not a named icon slot.
C2Variant & Property NamingNeeds RefinementMixed casing: transaction_v1 (snake) vs 1 - vertical (space-dash-space). CTA matrix is sparse across type values.
C3Token CoverageReadybg, label, label-primary, border, bg-subtle, icon-copy all bound to main/modal-popup/color/*. Shadow uses Shadow/Depth 0.
C4Native MappabilityRequires ReworkGeneral-dialog variants map to .sheet / Dialog; transaction variants do not — they need a dedicated Receipt component. Not a single native primitive.
C5Interaction State CoverageNeeds RefinementOnly default state shipped. No loading, no destructive variant, no copy-success feedback, no present/dismiss transition annotation.
C6Asset & Icon QualityNeeds RefinementCopy icon is raster PNG (shape_half, shape_full). Icon-placeholder slot is a hardcoded grey circle instead of a vector icon instance.
C7Code Connect LinkabilityNot MappedBlocked on restructure — duplicate scope with Overlay + embedded transaction layout must be resolved before mapping.
AspectStatusNotes
Component boundaryRequires ReworkNeeds to be split into Modal + TransactionReceipt before mapping.
Property namesNeeds RefinementNormalise casing first; otherwise mapping will be 1:n (one Figma value → multiple code enums).
Token bindingsReadyAll colour tokens present in the main/modal-popup collection.
Slot architectureNeeds RefinementPromote icon and CTA to Figma Slots so native can map to trailing closures / composable content slots.
State coverageNeeds RefinementAdd loading and destructive states to cover the common async-confirmation flows.

A type × cta matrix would yield 4 × 4=16, but only 7 combinations are shipped. The rest are gaps.

TypeCTANodeDimensionsNotes
default118507:71792320 × 212Title + description + single CTA.
default2 - horizontal18507:71799320 × 212Outlined secondary + filled primary, side-by-side.
default2 - vertical18507:71807320 × 270Stacked CTAs, both full-width.
with icon1 - vertical18507:71783320 × 31292×92 icon placeholder + title + desc + single CTA.
with icon2 - vertical18507:71773320 × 37092×92 icon + title + desc + two stacked CTAs.
transaction_v1118507:71706320 × 398Receipt layout — rows stacked (label above value). Reference row with copy icon.
transaction_v2118507:71732320 × 404Receipt layout — rows inline (label left, value right). Reference row with copy icon. Outer surface uses bg-subtle.

Gaps:default × 1 - vertical, with icon × 1 / 2 - horizontal, and every transaction × (non-1) combination are not shipped. Either fill the matrix or constrain the enum to only advertise supported pairs.

1.0.0 — April 2026 Initial
Initial Assessment · node 18507:71705
DS Health — 7 variants across 2 axes. Reusable and Composable flagged Warn due to transaction layouts being compressed into a general-purpose Modal. Documented
Baseline
Duplicate scope with Overlay — Modal and Overlay (47:329691) are maintained independently but overlap in intent. Family consolidation required. Open
C7
C1 — Layer structure — Opacity-0 _space_* spacer rectangles + hardcoded icon-placeholder circle. Open
C1
C2 — Enum naming — Mixed casing (transaction_v1 vs 1 - vertical). Sparse CTA matrix. Open
C2
C4 — Native mappability — Transaction variants are a different component wearing the Modal hat. Recommend extraction into EBTransactionReceipt. Open
C4
C5 — State coverage — No loading / destructive / copy-success states. Open
C5
C6 — Raster copy iconshape_half / shape_full PNGs should be a vector icon instance. Open
C6
C7 — Code Connect — Blocked on restructure. Token coverage is the only Ready-status criterion. Open
C7
Typography note — Description copy uses BarkAda (secondary font). Confirm with design — otherwise covered by the standing custom-font action item. Info
Info
Month and Year Picker - Item ConsolidateRequires ReworkComponent link

The 100×32 selectable cell rendered inside the Date Picker - Group Type=Month and Type=Year views. 3 variants on a single axis: Type=Default | Today | Selected. No Disabled, no Pressed, no Focused — a narrower state axis than the sibling day cell. The same Figma component is instance-swapped as both a month cell (Jan, Feb, Mar…) and a year cell (2024, 2025…); the inner frame is named Month regardless. Sibling to Date Picker - Item (32×32 day cell) — both are the same selectable-cell primitive at different pixel sizes.

Consolidate into a unified Picker Cell
This cell and Date Picker - Item are the same selectable-cell primitive at different pixel sizes (100×32 vs 32×32) with identical state semantics (Default / Today / Selected). Collapse both into a single Picker Cell with kind: day | month | year + state: default | today | selected | disabled. This cell currently lacks Disabled, Pressed, and Focused entirely — the unification should rectangularize the state axis across all kinds.
In Context

The cell is instance-swapped inside Date Picker - Group when the user switches to Month or Year view. 12 cells are rendered as a 4 × 3 month grid, or 20 cells as a 4 × 5 year grid. Same Figma component for both — only the text label changes.

JanFebMarAprMayJunJulAugSepOctNovDec
Live Preview

Switch Type to compare the 3 published variants. There is only one axis — the cell does not publish Disabled, Pressed, or Focused states.

Properties
Type
DS Health
Reusable
Warn
Used inside Date Picker - Group for both the Month and Year views — which is good. But because it is a separate sibling from the 32×32 day cell, the picker has to instance-swap between two near-identical components depending on the view. A single Picker Cell with a kind axis would be truly reusable.
Self-contained
Pass
Bg, label, border, radius, padding, and typography are all token-bound. No absolute-positioned overflow, no external dependencies — the cell is a clean self-contained pill.
Consistent
Warn
State axis is narrower than the sibling day cell (no Disabled, no Pressed, no Focused). The Today ring is also 1px here but 1.5px on the day cell — same visual pattern with drift. Cells in the same family should share state shape and stroke tokens.
Composable
Fail
Doesn't compose into a native picker — SwiftUI DatePicker(.graphical) and Material 3 DatePicker render their own month/year views. And as a DS primitive it duplicates Date Picker - Item at a different size instead of being one Picker Cell with a kind axis.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesType=DefaultPlain label on white. #0A2757 primary label. No ring, no fill.
TodayYesYesType=Today1px blue border, blue label. Marks the current month or year. Border thickness differs from the sibling day cell's 1.5px ring.
SelectedYesYesType=SelectedSolid blue fill (#005CE5), white bold label. Uses the shared main/date-picker/day/color/selected/* token scope.
DisabledNoNoNot defined on any Type. Sibling day cell exposes State=Disabled for Default and Today — this cell has no equivalent, so a disabled month or year has no token-bound appearance.
Pressed / Focused / HoverNoNoNot defined. Native pickers supply these automatically, but a custom wrapper has no tokens to apply.
Today + SelectedNoNoNot defined. Unclear which presentation wins when the current month/year is also the selected one.
Touch target100×32100×32Height is 32px — below WCAG minimum 44×44 on the vertical axis. Native pickers enforce their own hit areas; any custom wrapper would need to pad the tap zone.
Open Issues
  • Sibling duplication with Date Picker - Item. The two components are the same selectable-cell primitive at different pixel sizes (100×32 vs 32×32) with overlapping state semantics (Default, Today, Selected). Maintaining them as siblings doubles the variant surface and forces the Group panel to instance-swap based on view. C1 · Layer Structure & Naming
  • Inner frame is always named Month — even when it's the year cell. The same Figma component is instance-swapped as both a month cell (Jan, Feb, Mar…) and a year cell (2024, 2025…), but the inner auto-layout frame is named Month in all three variants. The layer name lies for half the instances. C1 · Layer Structure & Naming
  • State axis is narrower than the sibling day cell. The day cell has Type × State=Enabled|Disabled. This cell has a single Type axis with no State at all. Cells in the same family should share the same state shape — even if specific values differ by kind. C2 · Variant & Property Naming
  • Tokens named main/date-picker/day/* are used for non-day cells. The cell's bg and label resolve to main/date-picker/day/color/selected/* and main/date-picker/day/color/unselected/* — but it's rendering a month or year, not a day. The token scope is misleadingly named. Either rename to main/date-picker/cell/* or split into main/picker-cell/{day|month|year}/*. C3 · Token Coverage
  • Cell has no 1:1 native primitive. Both SwiftUI DatePicker(.graphical) and Material 3 DatePicker render their own month/year views and don't accept a custom cell view. This Figma component is therefore a reference spec, not a mappable component — should be marked as such or merged into the Picker Cell family. C4 · Native Mappability
  • No Disabled variant. When a parent limits the pickable date range, disabled months or years have no token-bound appearance on this cell. The sibling day cell at least ships Disabled on Default and Today — this cell has no Disabled form for any Type. C5 · Interaction State Coverage
  • No Pressed, Hover, or Focused variants. Tap feedback (iOS ripple / Android state layer) and keyboard focus ring are not defined. Native pickers supply these automatically, but any custom wrapper has no tokens to apply. C5 · Interaction State Coverage
  • Today + Selected collision is unresolved. There is no variant for the common case of the current month/year being the selected one. The design team should decide which presentation wins (ring + fill, fill-only, or a hybrid) and publish a variant. C5 · Interaction State Coverage
  • Today ring thickness drifts from the sibling day cell. This cell uses a 1px border; the sibling day cell uses a 1.5px ring. Same visual pattern, different stroke weight — selection/today emphasis should be one token shared across Picker Cell kinds, not per-component values. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked by the native-pickers-own-it direction (C4) and by the pending Picker Cell family unification (C1). Map only once the unified component exists and the wrapper surface is confirmed. C7 · Code Connect Linkability
Design Recommendations
  • Family — Consolidate Date Picker - Item + Month and Year Picker - Item into ONE Picker Cell. Both are selectable cells with identical state semantics (Default / Today / Selected); only pixel size (32×32 vs 100×32) and corner radius (30px pill vs 8px rounded-rect) differ. Proposed schema: kind=day | month | year + state=default | today | selected | disabled (plus range-middle | prev-next exposed only when kind=day). Collapses 10 sibling variants across two components into one component with two clean axes. A single native PickerCell composable renders the correct dimensions, radius, and typography per kind. Family
  • Property — Add a State axis to match the day cell. Even before consolidation, promote the single-axis Type schema into Type × State=Enabled | Disabled | Pressed | Focused to align with the day cell's shape. At minimum, publish Disabled variants for Default, Today, and Selected so the cell is usable in a date-range-restricted picker. Property
  • Rename — Fix the inner frame name. The inner auto-layout frame is named Month in all three variants, but the same component is instance-swapped as the year cell too. Rename to Cell or Label Container so it reads true for both Month and Year views. Rename
  • Token — Rename main/date-picker/day/* to main/date-picker/cell/*. The shared token scope is named day but also resolves for month and year cells. Rename (or split into main/picker-cell/{day|month|year}/* if treatments intentionally diverge) so the token name honestly describes what it styles. Token
  • Token — Share a selection-emphasis token across Picker Cell kinds. Create main/picker-cell/selection/* tokens that resolve to either "fill" or "ring" based on kind, and harmonise the Today ring stroke (1px here, 1.5px on the day cell) on a single border/picker-cell/today token. Token
  • State — Add Pressed, Focused, and Today+Selected variants. Extend the state coverage with Pressed and Focused (needed for any custom wrapper rendering tap / keyboard affordances), and publish a decision variant for the common "today is also selected" case. State
  • A11y — Flag the 32px height as below minimum target. The 100×32 cell is below WCAG 2.5.5 minimum 44×44 on the vertical axis. If a custom wrapper is ever built, pad the tap zone vertically so the hit area meets 44px. Document on the component so consumers don't ship the tight target outside the native picker context. A11y
  • Docs — Mark as reference, not a production component. Given that both platforms render their own month/year views inside the native DatePicker, this cell is a visual reference for the token-styled wrapper, not a component developers rebuild. Add a description banner and a _reference prefix once the Picker Cell family unification lands. Docs
Variants

3 published variants on a single Type axis. All share a 100×32 frame, 8px corner radius, 4px label gap, and Primary/Label/Light/Small typography. Selected switches to Primary/Label/Small (bold).

Default

The base month/year cell. Plain label on white, no ring or fill.

Properties
TypeDefault
Today

Today marker. 1px blue border, blue label. Note: thinner than the day cell's 1.5px ring.

Properties
TypeToday
Selected

Currently-selected month or year. Solid blue fill, white bold label. No Disabled form.

Properties
TypeSelected
Colors by Type

All cell colors are bound to tokens — but the tokens live under the main/date-picker/day/* scope even though this cell renders months and years. Selected uses main/date-picker/day/color/selected/*; Default/Today use main/date-picker/day/color/unselected/* plus the shared primary text and border tokens for the ring and label. The scope name is a mismatch — flagged as C3.

TypeRoleTokenVALUE
Defaultbgmain/date-picker/day/color/unselected/bg#FFFFFF
Defaultlabelmain/date-picker/day/color/unselected/label#0A2757
Todaybgmain/date-picker/day/color/unselected/bg#FFFFFF
Todayborderborder/color-border-primary#005CE5 (1px)
Todaylabeltext/color-text-primary#005CE5
Selectedbgmain/date-picker/day/color/selected/bg#005CE5
Selectedlabelmain/date-picker/day/color/selected/label#FFFFFF
Layout
PropertyValue
Cell size100 × 32
Corner radius8px (radius/radius-3)
Padding10px top, 8px bottom, 12px horizontal
Label gap4px (space/space-4)
Today border1px solid
Grid gap (month view 4×3)per Date Picker - Group
Grid gap (year view 4×5)per Date Picker - Group
Typography
VariantText StyleFontWeightSizeLine-heightTracking
Default / TodayPrimary/Label/Light/SmallProxima SoftSemibold (600)14px14px0.25px
SelectedPrimary/Label/SmallProxima SoftBold (700)14px14px0.25px
Native Handling Native Owns It

The month/year cell is drawn by the native picker. SwiftUI DatePicker(.graphical) exposes a Month/Year wheel/list and Material 3 DatePicker exposes an input-style Year picker inside its header — neither accepts a custom cell slot. Token-tint the native picker to match the DS appearance — don't ship a custom EBMonthYearPickerItem composable.

iOS — SwiftUI

DatePicker(
    "Birth Month",
    selection: $date,
    displayedComponents: .date
)
.datePickerStyle(.graphical)
.tint(Color.ebBrand)           // Selected month/year fill
.accentColor(Color.ebBrand)     // Today marker
// Tap the month/year header in .graphical style to toggle
// the month/year list view. No custom cell slot is exposed.

Android — Jetpack Compose (Material 3)

val state = rememberDatePickerState()
DatePicker(
    state = state,
    colors = DatePickerDefaults.colors(
        selectedYearContainerColor = EBColors.brand,
        selectedYearContentColor   = EBColors.onBrand,
        currentYearContentColor    = EBColors.brand,     // "Today" year
        yearContentColor           = EBColors.onSurface
    )
)
// Material 3 renders each year cell; month navigation uses
// arrow buttons in the default mode (no month-grid surface).

If the product absolutely needs a custom month/year grid (e.g. a birth-month picker separated from day selection), build a non-native grid and compose PickerCell(kind: .month | .year) inside it — but at that point you lose the native a11y and locale handling.

Property Mapping
Figma PropertySwiftUI EquivalentCompose EquivalentNotes
Type=Default(default rendering)yearContentColor / monthContentColorBase month/year cell. Uses primary text token.
Type=Today.accentColor / automatic current-year markercurrentYearContentColorNative pickers detect "today" from Calendar.current; only the emphasis color is supplied.
Type=Selected.tint (via selection binding)selectedYearContainerColor / selectedYearContentColorSolid fill. Selection is derived from the bound Date.
(missing) Disabledin: Date... range parameteryearRange / selectableDatesEnforced via the allowable range — picker dims out-of-range months/years automatically. Figma has no token for this state.
(missing) Pressed / FocusedNative pickers supply tap feedback and focus ring automatically. Figma needs variants for any custom wrapper.
SwiftUI (if ever materialized, else reference only)
ios/Components/DatePicker/EBPickerCell.swift · kind: month | year
Jetpack Compose (if ever materialized, else reference only)
android/components/datepicker/EBPickerCell.kt · kind: Month | Year
Accessibility
RequirementiOSAndroid
Touch target (44 × 44 min)Figma cell is 100 × 32 — native picker extends vertical hit areaFigma cell is 100 × 32 — native picker extends vertical hit area
Screen reader labelVoiceOver: "March, Month" / "2026, Year" (from DatePicker)TalkBack: "March" / "2026" with "selected" state
Selected announcement"Selected" trait added automatically"Selected" state added automatically
Focus ringSystem focus ring on iPad / hw keyboardSystem focus indicator on D-Pad / hw keyboard
Disabled announcement"Dimmed" trait when outside in: range"Disabled" state when outside selectableDates
Dynamic Type / font scalingAutomaticAutomatic
Usage Guidelines

Do

Treat this cell as a visual reference for how the native picker's month and year views should look in GCash theme — colors, ring thickness, radius, and label weight.

Don't

Don't ship a standalone EBMonthYearPickerItem composable. Native pickers render their own month/year cells; a custom cell composable has no slot to plug into.

Do

If you genuinely need a custom month/year grid, merge with Date Picker - Item into a unified PickerCell(kind:, state:) and compose it inside a custom grid.

Don't

Don't maintain Date Picker - Item and Month and Year Picker - Item as siblings — they're the same primitive at different sizes.

Do

Rely on the native picker for locale-aware month names, era handling, VoiceOver / TalkBack, and minDate/maxDate enforcement on both the month and year axes.

Don't

Don't draw disabled months/years manually. Pass an in: range (SwiftUI) or selectableDates (Compose) and let the platform dim them.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds FixSibling-duplication with Date Picker - Item. Inner frame named Month even when used as the year cell.
C2Variant & Property NamingNeeds FixNarrower state axis than the sibling day cell — single Type axis with no State. Cells in the same family should share state shape.
C3Token CoveragePartialAll values are token-bound, but tokens are scoped main/date-picker/day/* on a cell that renders months and years. Rename to main/date-picker/cell/* or split per kind.
C4Native MappabilityNot Applicable / ReworkNative pickers own the month/year view and don't accept a custom cell view. Reference-only unless merged into a custom PickerCell.
C5Interaction State CoverageNeeds FixNo Disabled, no Pressed, no Focused, no Today + Selected. Single-axis variant matrix with no State dimension.
C6Asset & Icon QualityPartialNo raster assets. But Today ring is 1px here vs 1.5px on the sibling day cell — should be one shared token.
C7Code Connect LinkabilityNot MappedBlocked by C4 (native owns it) and by the pending Picker Cell family unification.
Code Connect
AspectStatusNotes
Property namingNeeds FixAdd a State axis; align with day cell shape or merge into PickerCell.
Family unificationPendingMerge with Date Picker - Item into PickerCell with kind: day | month | year.
Token scopeNeeds FixRename main/date-picker/day/* to main/date-picker/cell/* (or split per kind).
Native component filePendingNo standalone composable — native DatePicker renders month/year cells. Only materialize EBPickerCell if a custom grid is ever built.
RecommendationConsolidateMerge into PickerCell, mark as reference spec for the native picker's month/year view.
Variants Inventory (3 total)

3 variants on a single Type axis. The sibling day cell has 7 variants on Type × State — this cell lacks the State dimension entirely.

TypeDimensionsEmphasisNode ID
Default100 × 32none18414:5851
Today100 × 321px blue border18414:5852
Selected100 × 32solid blue fill, white bold label18414:5853

Missing: Disabled, Pressed, Focused, Hover, Today + Selected — no State axis published.

1.0.0 — April 2026 Initial
Initial Assessment · node 18414:5854
Component assessed — 3 variants on a single Type axis. 100×32 rounded-rect cell with token-bound colors, spacing, radius, and typography. Documented
Initial
Sibling duplication with Date Picker - Item — Same selectable-cell primitive at different sizes. Proposal: collapse both into Picker Cell with kind: day | month | year. Open
C1 Open
Inner frame named Month across all variants — Same component instance-swapped as both month and year cell; layer name is inaccurate for year instances. Open
C1 Open
State axis narrower than sibling day cell — Single Type axis only. No State dimension, so Disabled / Pressed / Focused are all missing. Cells in the same family should share state shape. Open
C2 Open
Tokens scoped main/date-picker/day/* used for non-day cell — Bg and label tokens are named day but style months and years. Rename to main/date-picker/cell/* or split per kind. Open
C3 Open
No native primitive for a month/year cell — Native DatePicker renders its own month/year cells on both platforms. Reference-only unless a custom grid is built. Open
C4 Open
Disabled / Pressed / Focused / Today+Selected missing — No State axis published at all. The common "today is also selected" combination is undefined. Open
C5 Open
Today ring thickness drifts from sibling day cell — 1px here vs 1.5px on Date Picker - Item. Align on one shared stroke token per Picker Cell kind. Open
C6 Open
Code Connect not registered — Blocked by the native-pickers-own-it direction and by the pending Picker Cell family unification. Open
C7 Open
Onboarding - Tooltip ConsolidateReworkComponent link

A directional floating tip — header, description, close X, and a pointer on any of four sides. Ships as 4 variants across a single pointer axis (top, right, bottom, left). Despite the Onboarding - prefix, the component ships no step indicator and no CTAs — it is functionally Tooltip V2's Header + Description + dismiss slice with the CTA and icon axes removed. One of three Tooltip siblings in the DS.

Fold into the unified Tooltip; "Onboarding -" prefix is misleading (no walkthrough content)
This component, Tooltip V2, and Tooltip Blurred and Transparent model the same primitive with slightly different shapes. Merge into one Tooltip with placement: .top | .right | .bottom | .left (the only axis this sibling ships), appearance: .default | .onboarding | .translucent, hasDismiss, and an optional content/CTA body. Replace the 4 raster pointers with one vector and the raw close image with an icon/close instance. Once merged, rename the sibling that truly supports walkthroughs (with step indicator + Next/Skip) to carry the .onboarding appearance — or drop the name entirely.
Tooltip Family

Three sibling components in the DS do roughly the same job. Onboarding - Tooltip is the thinnest slice — a one-axis component that exists only because pointer-direction lives outside Tooltip V2's variant matrix.

ComponentNodeVariantsWhat's differentProposed role
Tooltip V270:149088Header/Description/Icon/CTA presence axes. Pointer as 4 booleans outside the matrix.appearance: .default + content slots
Onboarding - Tooltip (this)51:170664One pointer enum. Header + description + close only — no icon, no CTA, despite the name.placement axis on the unified Tooltip
Tooltip Blurred and Transparent49:335349Translucent surface with backdrop blur — used over photographic / high-contrast content.appearance: .translucent

Target architecture: one Tooltip with an appearance enum, one placement enum, optional hasArrow / hasDismiss, and a single content slot. This sibling collapses into a subset: Tooltip(appearance: .default, placement:_, title:_, body:_, hasDismiss: true).

In Context

A dismissible tip anchored to a feature element — commonly rendered during first-time user education, feature discovery, and coach-mark flows.

Wallet
Live Preview

Toggle the pointer direction. Header, description, and close are fixed in this sibling — there is no way to hide the title or show a CTA without switching to Tooltip V2.

Placement
pointer
Content (not exposed)
headeralways shown
descriptionalways shown
closealways shown
ctanot supported
iconnot supported
DS Health
Reusable
Warn
Works as a dismissible tip anchored to any target. But the "Onboarding -" prefix steers consumers toward walkthroughs it can't support (no step indicator, no Next/Skip). Misleading name fragments the primitive. C1
Self-contained
Warn
Surface, type, and spacing bind to main/nudge/* + space/* tokens — shares the collection with Tooltip V2. But the pointer arrow is 4 raster copies of one shape, and the close is an image asset rather than a DS icon instance. C6
Consistent
Warn
Pointer is modeled as a single enum here, but Tooltip V2 models the same concept as 4 booleans. Siblings should not disagree on the shape of a shared axis. C2
Composable
Fail
No Figma Slot for body content. No way to add an icon, an avatar, or a CTA — consumers who need those must switch components entirely. The primitive is closed. C4
Behavior
BehavioriOSAndroidFigma SpecNotes
Show / hideYesYesNot annotatedExpected: fade + slight scale-in anchored on the pointer side.
Tap close (X)YesYesClose layerDismiss icon is present but not wired to an interactive property. Close is an image asset, not a button instance.
Tap outsideImplicitImplicitNot definedStandard tooltip contract — tap-outside dismisses. Should be documented on the component.
Primary CTAMissingMissingNot builtDespite the "Onboarding" name, no CTA is provided. Consumers must switch to Tooltip V2 for any advance/skip control.
Pressed / Focused on closeMissingMissingNot builtClose is a raw image; no pressed / focused treatment and no hit target metadata.
Open Issues
  • Name implies walkthrough content that isn't shipped.Onboarding - Tooltip has no step indicator, no Next/Skip/Back CTAs, no progress dots, no illustration slot — it's just header + description + close. The prefix misleads consumers. Either rename to Tooltip / Placement and fold into the unified Tooltip, or add the actual onboarding axes (step indicator, Next/Skip) before keeping the name. C1 · Layer Structure & Naming
  • Three sibling Tooltip components for one primitive.Tooltip V2 (70:14908), this component (51:17066), and Tooltip Blurred and Transparent (49:335349) all model the same floating popover with slightly different shapes. Collapse into one Tooltip with appearance + placement. C1 · Layer Structure & Naming
  • Pointer schema disagrees with sibling. Onboarding - Tooltip models pointer direction as one pointer enum (correct), but Tooltip V2 models the same concept as 4 independent booleans. Siblings in the same family should never disagree on the shape of a shared axis. C2 · Variant & Property Naming
  • Pointer triangle is a raster asset. 4 separate image fills (imgPointer, imgPointer1, imgPointer2, imgPointer3) for what should be one vector shape rotated per direction. Replace with a single vector triangle; rotation handled by the placement enum. C6 · Asset & Icon Quality
  • Close (X) is an image asset, not a DS icon instance. The dismiss control uses imgShapeFull inside a generic "Close" frame rather than an instance of the DS's icon/close. Hides the icon from a11y labeling and token updates, blocks Code Connect from seeing it as a real control. C1 · Layer Structure & Naming
  • No body or CTA slot. There is no way to add a leading icon, an illustration, a step indicator, or a CTA. Any consumer that needs those must abandon this component for Tooltip V2 — defeating the point of a dedicated "Onboarding" sibling. C4 · Native Mappability
  • No dismiss / show states modeled. Close button is decorative; no Pressed / Focused state on the dismiss control. No appearing / dismissing lifecycle documented. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked on the family consolidation. Mapping today's 3-sibling shape to native would cement the wrong schema. C7 · Code Connect Linkability
Design Recommendations
  • Fold the 3 Tooltip siblings into one component. New schema: placement: .top | .right | .bottom | .left (this sibling's only axis), appearance: .default | .onboarding | .translucent, hasArrow: Bool, hasDismiss: Bool, cta: .none | .primary | .primaryAndSecondary, and a leading slot. This sibling's 4 variants collapse into the placement axis of the .default appearance — no dedicated component needed. Family
  • Rename or retire the "Onboarding -" prefix. The prefix should either be reserved for a sibling that actually ships onboarding affordances (step indicator, Next/Skip, Back) — in which case this component doesn't qualify — or dropped entirely in favor of an appearance: .onboarding enum on the unified Tooltip. Today's name promises content it doesn't deliver. Rename
  • Replace the 4 raster pointers with one vector triangle. Today there are 4 separate rasters (top/right/bottom/left). One vector shape, rotated per placement, fills the same role with zero asset burden, scales cleanly, and picks up surface + border token updates automatically. Asset
  • Instance-swap the close to icon/close. Use the canonical DS close-icon instance rather than an inline imgShapeFull. Gets you token-driven color, a11y labeling, and press-state handling for free. Composition
  • Adopt a content slot for body + optional CTA. The current closed shape (header + description + close, nothing else) is the reason this sibling exists. Adopt the unified Tooltip's content slot and this component disappears into the single source of truth. Slot
  • Add Pressed / Focused on the close control. Once the close becomes an icon-button instance rather than an image, states follow the canonical Button/Icon pattern. State
  • Document the dismiss contract + lifecycle. Add a description on the component: "Tap the close X or tap outside to dismiss. Tooltip appears with fade + slight scale from the pointer anchor; dismisses with reverse." Close the gap between designer intent and dev implementation. Docs
Variants

4 variants on a single pointer axis — top / right / bottom / left. Content is fixed: header + description + close. Surface is 336 × variable height; pointer adds 12 px on the anchored side.

pointer=top — target element above

Pointer triangle above the bubble, surface below. 336 × 90 (pointer adds 12 px on top). Used when the tip describes an element positioned above the tooltip in the flow.

pointer=bottom — target element below

Pointer triangle below the bubble. 336 × 89. Most common placement for tips anchored to an icon in a toolbar or nav bar.

pointer=left — target element to the left

Pointer triangle on the left edge. 348 × 78 (pointer adds 12 px on the left). Used for tips describing a leading-edge control.

pointer=right — target element to the right

Pointer triangle on the right edge. 348 × 78 (pointer adds 12 px on the right). Used for tips describing a trailing-edge control.

Colors by State
RoleTokenDefault
Surfacemain/nudge/color/primary/bg#FFFFFF
Bordermain/nudge/color/primary/border#E5EBF4
Header labelmain/nudge/color/primary/label#0A2757
Descriptionmain/nudge/color/primary/description#6780A9
Close iconmain/nudge/color/primary/icon-close#0A2757
Shadowelevation/app/shadow-low#020E22 · 6 %
Pointer triangleraster (4 images)

No Pressed / Disabled states modeled at the tooltip level. Pointer triangle should inherit the surface + border tokens once it becomes a vector.

Layout
PropertyTokenValue
Surface width336 px
Surface corner radiusradius/radius-26 px
Surface bordermain/nudge/color/primary/border1 px solid
Surface paddingspace/space-1616 px all sides
Header row gap (title ↔ close)24 px
Title ↔ description gapspace/space-44 px
Close hit-area padding3 px (18 × 18 icon, ~24 × 24 hit)
Pointer width / height24 × 12 (raster image)
Shadow offset / blurelevation/app/shadow-low0 / 0 / 4 / 0

Dimensions change slightly between top/bottom (336 × 89–90) and left/right (348 × 78) because the pointer adds 12 px to the anchored edge.

Typography
ElementDS text styleSpec
HeaderPrimary/Headlines/BlockProxima Soft Bold · 18 / 23 · +0.25
DescriptionSecondary/Bold/CaptionBarkAda Semibold · 12 / 18 · +0

BarkAda (secondary) is used for the description — custom-font standing action item applies.

Installation Planned API

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:tooltip:1.0.0")
}
Property Mapping (proposed — after consolidation)
Figma (today)Figma (proposed)SwiftUICompose
Onboarding - Tooltip (this)Tooltip · appearance: .defaultEBTooltip().ebAppearance(.default)EBTooltip(appearance=.Default)
pointer: top / right / bottom / leftplacement: .top / .right / .bottom / .leftarrowEdge: EdgeanchorPosition: EBTooltipAnchor
header (baked text)title: String?title: String?title: String?=null
description (baked text)body: String? (or content slot)body: String?body: String?=null
close (image asset, always shown)hasDismiss: Booldismissible: Booldismissible: Boolean=true
(not modeled)cta: .none / .primary / .pairprimary / secondary: TooltipAction?primaryAction / secondaryAction: TooltipAction?
(not modeled)leading (Slot)@ViewBuilder leadingleading: @Composable () -> Unit
(not modeled)onDismissonDismiss: () -> VoidonDismiss: () -> Unit
SwiftUI
ios/Components/Tooltip/EBTooltip.swift
Jetpack Compose
android/components/tooltip/EBTooltip.kt
Usage Snippets Planned API
// Equivalent of today's "Onboarding - Tooltip · pointer=bottom"
EBTooltip(
    title: "Quick transfers",
    body: "Tap here to send money to recent contacts.",
    placement: .bottom
)
.onDismiss { showTip = false }

// pointer=right · same appearance, placement flipped
EBTooltip(
    title: "Your balance",
    body: "Tap to reveal or hide.",
    placement: .right
)
// Equivalent of today's "Onboarding - Tooltip · pointer=bottom"
EBTooltip(
    title = "Quick transfers",
    body = "Tap here to send money to recent contacts.",
    placement = EBTooltipPlacement.Bottom,
    onDismiss = { showTip = false }
)

// pointer=right
EBTooltip(
    title = "Your balance",
    body = "Tap to reveal or hide.",
    placement = EBTooltipPlacement.Right
)
Accessibility
RequirementiOSAndroid
Role + focusAnnounce as .accessibilityAddTraits(.isModal) when dismissible; VoiceOver moves focus to the tooltip on appear.semantics { role=Role.Popup }; TalkBack focuses the tooltip container on appear.
Close controlWrap close as a Button with accessibilityLabel "Dismiss tip" and 44×44 hit target (current 24 × 24 is below minimum).IconButton with contentDescription="Dismiss tip"; 48×48 dp minimum (current hit area is ~24 × 24 dp).
Dismiss-outsideDo not auto-dismiss while VoiceOver is active — hold until user explicitly moves on.Do not auto-dismiss while TalkBack is active.
Reduce motionRespect UIAccessibility.isReduceMotionEnabled — fade only, skip scale-in.Respect Settings.Global.TRANSITION_ANIMATION_SCALE — fade only when motion reduced.
Combined labelRead title + body + "Dismiss" as one phrase; avoid reading pointer.mergeDescendants=true on the container.
Usage Guidelines
Do
  • Use for short, dismissible tips anchored to a specific element.
  • Anchor the pointer at the target so users associate the tip with its referent.
  • Write one short title and one short body — if you need two, promote to a richer component.
Don't
  • Don't use this component for real onboarding walkthroughs — it has no step indicator or Next/Skip. Use the unified Tooltip with appearance: .onboarding (when the schema lands) or a Modal with an explicit flow.
  • Don't use for error / status messaging — use Alert.
  • Don't stack multiple tooltips simultaneously — one focal point per screen.
  • Don't hand-edit the pointer — pick a direction with the pointer property.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRework"Onboarding -" prefix misleads — component ships no walkthrough content. 3 sibling Tooltip components for one primitive. Close uses a raw image inside a generic "Close" frame.
C2Variant & Property NamingRefinePointer is correctly modeled as one enum here (cleaner than Tooltip V2's 4 booleans) — but siblings should agree on the shape of the shared axis.
C3Token CoverageReadySurface, border, label, description, close icon, shadow all bound to main/nudge/* / elevation/*. Spacing via space/*. Radius via radius/radius-2.
C4Native MappabilityReworkClosed shape — no slot for body content or CTA. Maps cleanly to native only after consolidation into the unified Tooltip with content slot + cta axis.
C5Interaction State CoverageReworkNo Pressed / Focused on close. No appearing / dismissing lifecycle annotated. Close isn't wired to a dismiss property.
C6Asset & Icon QualityReworkPointer is 4 raster images (one per direction). Close is an image asset (imgShapeFull) rather than an icon/close instance.
C7Code Connect LinkabilityNot MappedBlocked on consolidation. Mapping today's sibling component would cement the wrong schema.
Variants Inventory (4 total)

One axis: pointer (4 values)=4 variants. Content (header / description / close) is fixed across all variants.

#PointerDimensionsPointer assetNode
1top336 × 90imgPointer51:17065
2bottom336 × 89imgPointer151:17063
3left348 × 78imgPointer251:17062
4right348 × 78imgPointer351:17064

Four separate raster images for what should be one vector shape rotated by the pointer value.

1.0.0 — April 2026 Initial
Initial Assessment · node 51:17066
Verdict: Consolidate — Fold into the unified Tooltip as appearance: .default with the placement axis. The "Onboarding -" prefix is misleading (no walkthrough content ships). Open
Architecture
C1 — Misleading name — No step indicator, no Next/Skip/Back CTAs, no progress dots. Component is not onboarding-specific. Open
C1
C1 — 3 siblings for 1 primitive — Tooltip V2, Onboarding - Tooltip, Tooltip Blurred and Transparent. Merge via appearance + placement. Open
C1
C2 — Pointer schema disagrees with sibling — This sibling uses one enum; Tooltip V2 uses 4 booleans. Family members should agree on the shape of a shared axis. Open
C2
C4 — No slot for body or CTA — Closed shape forces consumers to switch to Tooltip V2 for anything richer than header + description. Open
C4
C5 — No dismiss/focus states — Close is a raw image; Pressed / Focused / Lifecycle not modeled. Open
C5
C6 — Raster pointer + raw close — 4 raster images (one per direction). Close is an image asset rather than icon/close. Vector + icon instance. Open
C6
C7 — Code Connect — Blocked on consolidation. Open
C7
Tokens ✓ — Surface / border / label / description / close / shadow all bound to main/nudge/* and elevation/*. Spacing via space/*. Noted
Praise
Overlay FixRefineComponent link

A translucent scrim that dims background content behind modals, bottom sheets, dialogs, and side drawers. One inner layer dim filled with bg/color-bg-overlay-strong (#020E228F · 56% opacity). Used to signal "the screen is in a modal state" and to surface the floating content above it.

Keep — with minor fixes before native handoff
Overlay is correctly token-bound and maps cleanly to native primitives (SwiftUI .presentationBackground, Compose Scrim). Before linking, resize to Fill parent, decide whether a standard-strength variant is needed, and annotate the tap-to-dismiss contract.
In Context

Overlay sits between page content and a floating surface (bottom sheet, dialog, drawer). It dims the content below to focus attention on the surface above.

Live Preview

The overlay's single visual state — a flat, token-bound dim layer. There are no interactive properties on the component itself; dismiss and show/hide behaviors live in the surface it sits behind.

Properties
background
surface above
DS Health
Reusable
Partial
Works as the dim layer behind any modal surface, but the fixed 360×640 frame forces consumers to resize on every use. Should Fill parent.
Self-contained
Pass
Owns its fill and opacity, bound to a semantic token. Nothing external required to render.
Consistent
Partial
Token is named overlay-strong suggesting a standard-strength companion, but only one strength is exposed as a component. Naming implies a set of two.
Composable
Partial
Intended to sit behind sheets/dialogs/drawers, but lacks fill-parent sizing and a documented z-index order.
Behavior
BehavioriOSAndroidFigma SpecNotes
Show / hideYesYesNot definedFades with the presentation transition of its owning surface.
Tap to dismissYesYesNot annotatedContract: tap-scrim dismisses unless surface is marked modal.
Scroll lockImplicitImplicitHandled by surfaceOwning sheet/dialog locks background scroll on mount.
Focus / a11yYesYesImplicitScrim itself is not focusable — owning surface traps focus.
Open Issues
  • No strength variants. Only bg/color-bg-overlay-strong (56%) is exposed. Token name implies a standard-strength (32%) counterpart that isn't surfaced. Consider a Strength=Standard | Strong property. C2 · Variant & Property Naming
  • Fixed frame size 360×640. Forces consumers to resize the instance every time. Should use auto-layout Fill on both axes so it scales to any parent. C4 · Native Mappability
  • Tap-to-dismiss contract not annotated. The standard behavior (tap-scrim dismisses, unless the surface is modal) should be documented on the component so designers and devs agree on the contract. C5 · Interaction State Coverage
  • No Code Connect mapping. Trivial once the size and variant questions above are settled. C7 · Code Connect Linkability
Design Recommendations
  • Set frame to Fill parent. Change both width and height from fixed to Fill so Overlay adapts to any container (phone, tablet, custom sheet). Matches how native Scrim behaves. Composition
  • Decide on strength variants. Two paths: (a) add Strength=Standard (32%) | Strong (56%) property and bind to two tokens, matching Material 3; or (b) keep a single 56% strength and rename the token from overlay-strong to overlay so the name stops implying a second variant exists. Property
  • Annotate the dismiss contract. Add a description on the component: "Tap-outside dismisses the surface above, unless the surface is modal (requires explicit action)." This closes the gap between designer intent and developer implementation. Docs
  • Document layer order. Add a short note: Content → Overlay → Floating surface (Sheet / Dialog / Drawer). Prevents teams from accidentally putting the floating surface under the scrim. Docs
  • Naming note (informational). Other DS call this primitive Scrim (Material 3), Backdrop (Fluent / Polaris), Mask (Ant), Blanket (Atlassian), Underlay (Spectrum). Team keeps Overlay — worth documenting here so cross-DS references aren't confusing. Docs
Default · Strong node 47:329691

The only current variant — a flat translucent fill at 56% opacity of the overlay color. Drop it behind any sheet, dialog, or drawer.

Properties
NameOverlay
Variants1
PropertiesNone exposed
Inner layerdim
Colors by State
ROLETOKENVALUE
Dim fillbg/color-bg-overlay-strong#020E228F
Hex includes 56% alpha channel (8F). Base color #020E22 is the DS neutral-darkest; alpha does the dimming.
Layout
Width (sticker sheet)360
Height (sticker sheet)640
Recommended sizingFill × Fill
Corner radius0
BorderNone
PaddingNone
Typography
N/ANo text layers
// Direct placement — recommended for full-screen overlays
EBOverlay()
    .ignoresSafeArea()

// Behind a sheet surface
.sheet(isPresented: $showSheet) {
    EBBottomSheet { ... }
        .presentationBackground(.ebOverlay())
}
// Direct placement — recommended for full-screen overlays
EBOverlay(modifier = Modifier.fillMaxSize())

// Behind a sheet surface — Material 3 Scrim is the primitive
ModalBottomSheet(
    onDismissRequest = { showSheet = false },
    scrimColor = EBTokens.bgOverlayStrong
) { ... }
Installation Planned API
// Swift Package Manager
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
// build.gradle.kts
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBOverlay
Property Mapping
Figma propertySwiftUICompose
None exposedEBOverlay() — no parameters today.EBOverlay(modifier: Modifier=Modifier)
(proposed) Strength.ebStrength(.standard | .strong)strength=EBOverlayStrength.Standard | Strong
(proposed) onDismiss.onTapGesture { onDismiss() }Modifier.clickable { onDismiss() }
Suggested file paths
  • ios/Components/Overlay/EBOverlay.swift
  • android/components/overlay/EBOverlay.kt
Usage
// 1 · Standalone overlay (e.g. loading blocker)
ZStack {
    PageContent()
    if isBusy {
        EBOverlay()
            .ignoresSafeArea()
            .onTapGesture { }  // blocks taps but doesn't dismiss
    }
}

// 2 · Behind a bottom sheet — with dismiss
.sheet(isPresented: $showSheet) {
    EBBottomSheet(onDismiss: { showSheet = false }) { ... }
        .presentationBackground(.ebOverlay())
        .presentationDetents([.medium, .large])
}

// 3 · Non-dismissible modal (requires explicit action)
.fullScreenCover(isPresented: $showDialog) {
    EBDialog { ... }
        .presentationBackground(.ebOverlay())
        .interactiveDismissDisabled(true)
}
// 1 · Standalone overlay (e.g. loading blocker)
Box(Modifier.fillMaxSize()) {
    PageContent()
    if (isBusy) {
        EBOverlay(Modifier.fillMaxSize())
    }
}

// 2 · Behind a bottom sheet — Material 3 Scrim is the primitive
ModalBottomSheet(
    onDismissRequest = { showSheet = false },
    scrimColor = EBTokens.bgOverlayStrong
) { EBBottomSheet { ... } }

// 3 · Non-dismissible dialog
Dialog(
    onDismissRequest = { },
    properties = DialogProperties(dismissOnClickOutside = false)
) {
    EBOverlay(Modifier.fillMaxSize())
    EBDialog { ... }
}
Accessibility
RequirementiOSAndroid
Not focusable itselfOverlay is decorative. Do not expose it to VoiceOver — focus belongs to the surface above.Use Modifier.clearAndSetSemantics { } on the scrim so TalkBack ignores it.
Modal announcementThe sheet/dialog above owns .accessibilityAddTraits(.isModal).The sheet/dialog above owns semantics { paneTitle="..." } and modal behavior.
Tap-to-dismiss targetFull-screen tap area counts as the dismiss hit region — comfortably above the 44×44pt target.Full-screen tap area — comfortably above the 48×48dp target.
Reduce transparencyRespect UIAccessibility.isReduceTransparencyEnabled — fall back to an opaque dim color if true.Respect Settings.Global.TRANSITION_ANIMATION_SCALE and high-contrast mode — increase opacity or swap to solid dim.
Usage Guidelines
Do
  • Pair Overlay with a floating surface (sheet, dialog, drawer) to create a focused modal moment.
  • Set the overlay to Fill parent so it covers whatever container it lives in.
  • Document the dismiss contract — tap-outside dismisses, unless the surface is marked modal.
  • Animate the overlay's opacity alongside the floating surface so they feel like one motion.
Don't
  • Don't use Overlay as a page background or to visually tint content — use a surface token instead.
  • Don't stack multiple overlays on top of each other; promote to a full-screen experience.
  • Don't forget to block background taps — tapping through the scrim is always a bug.
  • Don't hardcode #020E228F in code — bind to bg/color-bg-overlay-strong.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyInner layer named dim — semantic and accurate.
C2Variant & Property NamingNeeds RefinementNo properties exposed. Token name overlay-strong implies a standard-strength counterpart that isn't available. Either add a Strength property or drop the -strong qualifier from the token.
C3Token CoverageReadyFill bound to bg/color-bg-overlay-strong.
C4Native MappabilityNeeds RefinementMaps cleanly to SwiftUI .presentationBackground and Compose Scrim, but fixed 360×640 frame needs to become Fill × Fill before linking.
C5Interaction State CoverageNeeds RefinementTap-to-dismiss behavior is implicit — should be annotated on the component as a documented contract.
C6Asset & Icon QualityN/ANo assets or icons.
C7Code Connect LinkabilityNot MappedNo Code Connect mapping yet. Trivial once sizing and strength are finalized.
Variants Inventory (1 total)

Single variant — no property matrix.

#NameNodeDimensionsFillNotes
1Overlay / Strong47:329691360 × 640bg/color-bg-overlay-strongDefault state — the single shipped variant.
1.0.0 — April 2026 Initial
Initial Assessment · node 47:329691
DS Health — Single-variant scrim, token-bound fill. Reusable/Composable flagged Partial due to fixed frame size. Documented
Baseline
C2 — Strength variants — Only strong (56%) exposed, token name implies a standard counterpart. Open
C2
C4 — Fill parent — Current 360×640 frame should be Fill × Fill. Open
C4
C5 — Dismiss contract — Tap-to-dismiss not annotated on the component. Open
C5
C7 — Code Connect — No mapping registered. Open
C7
Naming note — Other DS call this Scrim / Backdrop / Mask / Blanket / Underlay. Team keeps Overlay. Documented for cross-DS reference. Convention
Info
Progress Bar RestructureReworkComponent link

A linear task-progress indicator — a brand-blue fill over a light-blue track, used to show how far along a multi-step task is (KYC, file upload, form wizard). Today the component ships 11 variants for percentage=0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, each rendered by swapping a pair of pre-sized raster PNGs (back + front). There is no continuous value, no indeterminate animation, and no success / error state.

Restructure — collapse 11 percentage variants into one component with a continuous progress value
Progress is a scalar — not an enum. The current schema cannot represent 37% or 62%, and every variant ships two raster PNGs for what should be two token-bound rectangles. Rebuild as a single component: progress: Float (0–1), state: determinate | indeterminate | success | error. Replace the raster back / front layers with vector strokes bound to main/progress-bar/color/border-track and main/progress-bar/color/border. Native side maps 1:1 to ProgressView / LinearProgressIndicator.
In Context

Progress Bar appears above or below task content to show completion — KYC steps, file upload progress, multi-step form wizards.

Live Preview

Drag the slider to set progress. Note that Figma today only supports the ten 10%-step jumps — a real implementation would smoothly interpolate. Flip state to preview the proposed indeterminate, success, and error variants.

Content
progress
value45%
Properties (proposed)
state
DS Health
Reusable
Pass
Generic task-progress primitive — fits anywhere a linear completion indicator is needed (KYC, uploads, wizards).
Self-contained
Warn
Renders via raster back / front image layers instead of shape layers. Ships 11 PNGs worth of assets for what should be two rectangles.
Consistent
Fail
Progress modelled as a discrete percentage enum with 11 variants — cannot express 37%, 62%, or any non-decile value. Every other slider-style value in the DS is continuous.
Composable
Partial
Fixed 312-px width with 2-px horizontal padding hugs a single canonical layout; doesn't stretch responsively inside a fill container today.
Behavior
StateiOSAndroidFigma SpecNotes
DeterminateYesYespercentage=0…100Fill grows left-to-right in 10% steps today. Should be continuous (0–1).
IndeterminateMissingMissingNot modeledNot modeled in Figma. Native primitives support it out of the box — add a variant so designers can spec it.
SuccessMissingMissingNot modeledGreen fill at 100%. Used to confirm the task completed cleanly (file uploaded, verification passed).
ErrorMissingMissingNot modeledRed fill at the point of failure. Used when the task fails mid-progress (upload retry, network dropped).
BufferedN/AOptionalNot modeledCompose supports a secondary buffered fill. Optional — only add if media streaming is a real use case.
Open Issues
  • Progress is an enum of 10% steps, not a value. The Figma component exposes percentage as 11 discrete options (0, 10, 20, …, 100). Consumers can't spec 37% or animate smoothly — they must pick the closest variant. Every scalar value in the DS should be continuous. C2 · Variant & Property Naming
  • Track and fill are raster PNGs. The back and front layers are <img> assets, not shape layers. Blocks token-driven theming, breaks at non-1× resolutions, and ships 11 PNG pairs for what should be two rectangles. C6 · Asset & Icon Quality
  • Layer names are structural (back / front), not semantic. Should be Track and Fill to match native parlance and make the inspector readable for handoff. C1 · Layer Structure & Naming
  • No indeterminate, success, or error state modeled. Native ProgressView and LinearProgressIndicator both support indeterminate natively; product flows (KYC failure, upload retry) need success / error color states. Today Figma has only the determinate-blue variants. C5 · Interaction State Coverage
  • Property name percentage reads as a unit, not a ratio. Native APIs use progress as a 0–1 Float. Renaming to progress (with a 0–1 range, or 0–100 if kept as integer) aligns the DS with iOS / Compose conventions. C2 · Variant & Property Naming
  • Code Connect mappings not registered. Cannot land until the progress value is parameterized and the raster layers are replaced with token-bound shapes. C7 · Code Connect Linkability
Design Recommendations
  • Collapse 11 percentage variants into a single component with a continuous progress value. Delete the percentage=0 | 10 | … | 100 enum. Expose progress: Float (0–1). In Figma, drive the fill width by layout — either a single variant with a layer the designer resizes, or a published component that lives as a native primitive on the dev side. Variant math drops from 11 to 1 (+ state). Property
  • Replace raster back / front images with token-bound rectangles. Two filled rectangles (or stroked lines) bound to main/progress-bar/color/border-track and main/progress-bar/color/border. Same visual output, theme-able, resolution-independent, no PNG pairs to ship. Asset
  • Rename layers backTrack and frontFill. Matches native terminology (track / tint in SwiftUI, trackColor / color in Compose) and reads better in the inspector. Rename
  • Add state variant: determinate / indeterminate / success / error. Indeterminate is a looping animation designers should be able to spec. Success (positive green) and error (negative red) cover KYC / upload result states. Token references: main/progress-bar/color/success and main/progress-bar/color/error — add to the collection. State
  • Reuse existing semantic color tokens for success / error. Don't mint new hex values. Align with Alert / Badge semantic colors (text/positive, text/negative) so the progress fill reads the same as the rest of the system. Token
  • Evaluate whether a bespoke EBProgressBar is needed at all. SwiftUI ProgressView(value:) and Compose LinearProgressIndicator are 1:1 matches for this component. If the only custom requirement is token-bound colors, a lightweight theming wrapper suffices; otherwise use the native primitive directly. Document either way. Composition
  • Make the component width-flexible. Today it's locked to 312 px with 2-px horizontal padding. Spec as fill-container so a designer can drop it into any column width. Property
  • Document accessibility expectations.role="progressbar", aria-valuenow / aria-valuemin / aria-valuemax for determinate; announce a localized label for indeterminate ("Loading…"). Both native APIs handle this automatically, but the web/hybrid consumer needs the spec. A11y
Determinate nodes 27:64947 through 27:64985

Linear fill with a light-blue track and a brand-blue fill. The 11 Figma variants step through percentage=0, 10, 20, …, 100 — each variant swaps a pre-sized raster pair. The target implementation renders a single component with a continuous progress value.

Properties (today)
percentage0 | 10 | 20 | … | 100
Fill implementationraster <img>
Track implementationraster <img>
Width312 (fixed)
Colors by State
ROLETOKENVALUE
Trackmain/progress-bar/color/border-track#D2E5FF
Fill (determinate)main/progress-bar/color/border#005CE5
Fill (success) — proposedmain/progress-bar/color/positive#12AF80
Fill (error) — proposedmain/progress-bar/color/negative#D81E1E
Layout
Total width312
Horizontal padding2
Inner track width308
Stroke height4 (raster-baked)
Corner radius0 (radius/radius-0)
Typography
No text
Label pairingexternal
Progress Bar is a graphic-only primitive; labels and percentage text are the consumer's responsibility.
Installation Planned API

Progress Bar maps to native primitives (ProgressView on iOS, LinearProgressIndicator on Android). A thin DS wrapper is only needed to bind the branded colors.

.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBProgressBar
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
percentage: 0 | 10 | … | 100 (enum)progress: Float (0–1)value: Doubleprogress: () -> Float
(not modeled)state: determinate | indeterminate | success | errorProgressView() (indeterminate) / .tint(.green / .red)LinearProgressIndicator() (indeterminate) / color=Color.Green / Red
(raster back)Track (vector, border-track).progressViewStyle(.linear)trackColor=EBColors.progressTrack
(raster front)Fill (vector, border).tint(EBColors.progressFill)color=EBColors.progressFill
312 fixedfill-container.frame(maxWidth: .infinity)modifier=Modifier.fillMaxWidth()
Suggested file paths
  • ios/Components/ProgressBar/EBProgressBar.swift — thin wrapper around ProgressView with DS colors
  • android/components/progressbar/EBProgressBar.kt — thin wrapper around LinearProgressIndicator with DS colors
  • Alternative: use native primitives directly with a token-bound .tint / color. No wrapper required.
Usage
// Determinate — continuous value
EBProgressBar(progress: 0.45)

// Indeterminate — looping animation
EBProgressBar(state: .indeterminate)

// Success — 100% in positive green
EBProgressBar(progress: 1.0, state: .success)

// Error — fails mid-progress in negative red
EBProgressBar(progress: 0.6, state: .error)

// Or use the native primitive directly
ProgressView(value: 0.45)
    .progressViewStyle(.linear)
    .tint(EBColors.progressFill)
// Determinate — continuous value
EBProgressBar(progress = { 0.45f })

// Indeterminate — looping animation
EBProgressBar(state = EBProgressBarState.Indeterminate)

// Success — 100% in positive green
EBProgressBar(progress = { 1.0f }, state = EBProgressBarState.Success)

// Error — fails mid-progress in negative red
EBProgressBar(progress = { 0.6f }, state = EBProgressBarState.Error)

// Or use the native primitive directly
LinearProgressIndicator(
    progress = { 0.45f },
    color = EBColors.progressFill,
    trackColor = EBColors.progressTrack,
    modifier = Modifier.fillMaxWidth()
)
Accessibility
RequirementiOSAndroid
Progress roleProgressView exposes the progressbar trait automatically. Set .accessibilityLabel("Verification progress") for context.LinearProgressIndicator emits ProgressBarInfo via semantics automatically. Set Modifier.semantics { contentDescription="Verification progress" }.
Value announcementVoiceOver reads the current value (0–100%). For non-percentage ranges, use .accessibilityValue("\(step) of \(total)").TalkBack reads the progress fraction. For custom phrasing, set stateDescription.
IndeterminateAnnounce a localized label ("Loading…"). Avoid announcing a fake percentage.Same — LinearProgressIndicator() with no progress lambda handles this natively.
ContrastFill #005CE5 on track #D2E5FF=3.1:1 — passes 3:1 for non-text graphics (WCAG 1.4.11). OK.Same ratio.
Reduced motionIndeterminate animation should honor UIAccessibility.isReduceMotionEnabled. Native ProgressView does this automatically.Native LinearProgressIndicator already respects Animator.getDurationScale.
Usage Guidelines
Do
  • Use determinate when the total duration / step count is known (KYC wizard, upload with known size).
  • Use indeterminate when the total is unknown (network requests, streaming data).
  • Pair with a label explaining what's progressing — "Verifying your ID…", "Uploading 3 of 5 files".
  • Stretch to fill the column width so it reads as a meter, not a pill.
Don't
  • Don't use Progress Bar for rating or slider input — this is display-only, not interactive.
  • Don't ship 10-step variants in product code — use a continuous 0–1 value.
  • Don't place raster images inside to achieve the fill — use the native primitive.
  • Don't animate from 0% to 100% on mount for decorative reasons — the value should reflect real progress.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds RefinementRename backTrack, frontFill. Otherwise structurally clean.
C2Variant & Property NamingReworkpercentage is a discrete enum; should be a continuous progress: Float (0–1).
C3Token CoverageReadyTrack + fill colors bound to main/progress-bar/color/*. Add success / error tokens once states are introduced.
C4Native MappabilityReadyMaps 1:1 to ProgressView(value:) / LinearProgressIndicator. No custom gesture or web-only patterns.
C5Interaction State CoverageReworkMissing indeterminate, success, error. Only determinate (in 10% steps) modeled today.
C6Asset & Icon QualityReworkTrack + fill are raster <img>. Replace with token-bound rectangles or vector strokes.
C7Code Connect LinkabilityNot MappedBlocked until progress is parameterized and rasters replaced.
Variants Inventory (11 total)

A single percentage axis with 11 discrete values (0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100)=11 variants. The target architecture collapses this axis into a continuous progress value — variant count drops to 1 (plus the new state axis for indeterminate / success / error).

#NodepercentageFill widthLayer pair
127:6494700 / 308back only (no front rendered)
227:6494910~31 / 308back + front raster
327:6495320~62 / 308back + front raster
427:6495730~92 / 308back + front raster
527:6496140~123 / 308back + front raster
627:6496550~154 / 308back + front raster
727:6496960~185 / 308back + front raster
827:6497370~216 / 308back + front raster
927:6497780~246 / 308back + front raster
1027:6498190~277 / 308back + front raster
1127:64985100308 / 308back + front raster
1.0.0 — April 2026 Initial
Initial Assessment · node 18577:13227
Verdict: Restructure — Collapse 11 percentage variants into a single component with continuous progress: Float (0–1). Add state axis (determinate / indeterminate / success / error). Replace raster back/front with token-bound rectangles. Open
Schema
C1 — Layer naming — Rename backTrack and frontFill. Aligns with native terminology. Open
C1
C2 — Continuous value — Replace percentage: 0 | 10 | … | 100 enum with progress: Float (0–1). Renaming from percentage to progress aligns with iOS / Compose conventions. Open
C2
C5 — Missing states — Add indeterminate, success, error variants. Native ProgressView / LinearProgressIndicator support these out of the box. Open
C5
C6 — Raster fills — Replace <img> back/front layers with token-bound rectangles using main/progress-bar/color/border-track and main/progress-bar/color/border. Open
C6
C7 — Code Connect — Mappings pending restructure. Blocked until progress is parameterized and rasters replaced. Open
C7
Radio Button with Label FixNeeds RefinementComponent link

A form row pairing a Radio Button with a text label. 4 variants across a size property with mixed values: default, large, default - error, large - error. Only the unselected state is documented across sizes.

Split size + state props
Split the size property into size=default/large and isError: Bool. Add disabled and selected variants. Instance-swap (or Figma Slot) the radio so the large label pairs with a large radio. The label component should track the atom's state via a single selected prop forwarded down.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. Radio Button with Label appears in form questions and preference settings, stacked vertically as a group.

Continue
Live Preview

Toggle size + error state. The proposed prop model splits these from the current single entangled size property.

Proposed API
size
isError
selected
DS Health
Reusable
Pass
Used for mutually exclusive form choices: payment method, shipping option, preference questions.
Self-contained
Pass
Carries gap, vertical padding, and typography. Body color, font, and indent all token-bound.
Consistent
Warn
size values include "default - error" and "large - error" — space-hyphen-space strings that encode state in a size prop. Breaks native enum mapping. C2
Composable
Warn
Always instances the small radio — even when size=large. The large label doesn't visually scale the radio accordingly. C6
Open Issues
  • size property encodes state. Values include "default - error" and "large - error". Should be two orthogonal props: size=default | large + isError: Bool. C2 · Variant & Property Naming
  • Missing state variants. No disabled or selected variants. Forms need all four selection states (selected/unselected × enabled/disabled) plus the error variant. C5 · Interaction State Coverage
  • Radio is hardcoded to the small instance. Even in size=large variants, the radio uses the 16×16 small atom. Large label should pair with the 20×20 large radio. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until the size prop split and missing state variants land. C7 · Code Connect Linkability
Design Recommendations
  • Split the size prop:
    size: default / large
    isError: Bool
    selected: Bool
    disabled: Bool (or unified state enum)
    Flat orthogonal props — eliminates the compound string values. Property
  • Pair radio size to label sizesize=default → small radio (16 × 16); size=large → large radio (20 × 20). The current always-small behavior breaks visual hierarchy. Composition
  • Adopt a Figma Slot for the radio — lets consumers swap in a Radio Button with any state (selected/disabled/error/etc.) from the atom component. Maps to @ViewBuilder / @Composable slots. Slot
  • Add disabled + selected variants — forms need all state combinations documented. State
  • Make the whole row tappable — labels should be tap targets, not just the radio. Document in Accessibility. A11y
Variants

4 variants in the current Figma. Default (Primary/Multi-line Label/Light/Small, 14 / 16). Large (Primary/Multi-line Label/Light/Base, 16 / 20). Error variants tint the radio border red.

Default

Small radio + 14 / 16 label. Default unselected state.

Large

16 / 20 label (HeyMeow Rnd Semibold). Still uses the 16 × 16 radio — should be 20 × 20.

Default — error

Default size with red radio border. Label text color unchanged.

Large — error

Large size with red radio border.

Typography
SizeDS text styleSpec
defaultPrimary/Multi-line Label/Light/SmallHeyMeow Rnd Semibold · 14 / 16 · +0.25
largePrimary/Multi-line Label/Light/BaseHeyMeow Rnd Semibold · 16 / 20 · +0.25
Layout
PropertyTokenValue
Radio → label gapspace/space-1212px
Radio icon offset padding (top)space/space-44px
Text container padding (default)space/space-44px vertical
Text container padding (large)3t / 5b
Default width (demo instance)63px
Large width (demo instance)69px

Widths reflect the "Label" demo content — real usage hugs the text.

Installation Planned API

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:radio:1.0.0")
}
Property Mapping (proposed — after split)
Current FigmaProposedSwiftUICompose
size=default / largesize: EBRadioSize.controlSize(.small/.large)size=EBRadioSize.*
"- error" suffixisError: Bool.ebError(true)isError=true
selected: Boolselected: Boolselected: Boolean
label: Stringtitle: Stringlabel: String
disabled: Bool.disabled(true)enabled=false
onToggleonChange: (Bool) -> VoidonCheckedChange: (Boolean) -> Unit
SwiftUI
ios/Components/Radio/EBRadioButtonWithLabel.swift
Jetpack Compose
android/components/radio/EBRadioButtonWithLabel.kt
Usage Snippets Planned API
// Default row
EBRadioButtonWithLabel("Pay with GCash", selected: $selection == .gcash) {
    selection = .gcash
}

// Large row with error
EBRadioButtonWithLabel("Option A", selected: isSelected)
    .controlSize(.large)
    .ebError(true)

// Disabled
EBRadioButtonWithLabel("Not available", selected: false)
    .disabled(true)
// Default row
EBRadioButtonWithLabel(
    label = "Pay with GCash",
    selected = selection == Option.Gcash,
    onCheckedChange = { selection = Option.Gcash }
)

// Large row with error
EBRadioButtonWithLabel(
    label = "Option A",
    selected = isSelected,
    onCheckedChange = { isSelected = it },
    size = EBRadioSize.Large,
    isError = true
)

// Disabled
EBRadioButtonWithLabel(
    label = "Not available",
    selected = false,
    onCheckedChange = { },
    enabled = false
)
Accessibility
RequirementiOSAndroid
Whole-row tap targetWrap the row in a Button or tap gesture — label must be tappable, not just the radioApply Modifier.selectable(...) to the row
Group semanticsWrap options in .accessibilityElement(children: .contain)Use Modifier.selectableGroup() on parent
Role announcementRadio Button role inherited from the atomRole.RadioButton via selectable
Error announcementInclude error state in the option's accessibility labelUse semantics { error(...) }
Touch target sizeRow should be at least 44pt tall48dp minimum
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyicon-ofsset (typo — "offset"), text-container. Minor spelling issue.
C2Variant & Property NamingNeeds Fixsize values encode error state with space-hyphen-space strings.
C3Token CoverageReadyLabel colors, gap, padding all token-bound.
C4Native MappabilityReadyHStack / Row with radio + label.
C5Interaction State CoverageNeeds FixNo disabled, selected, or pressed variants.
C6Asset & Icon QualityPartialAlways uses small radio instance — large label doesn't scale the radio.
C7Code Connect LinkabilityPendingBlocked by C2 prop split.
Variants Inventory (4 total)
size (current)DecomposedNode ID
defaultsize=default, isError=false18482:35674
largesize=large, isError=false18482:35686
default - errorsize=default, isError=true18482:35680
large - errorsize=large, isError=true18482:35692

After the prop split, these 4 become 2 size × 2 isError=4 clean variants, with selected + disabled added as booleans (not variants).

1.0.0 — April 2026 Initial
Initial Assessment · node 18482:35673
Component assessed — 4 variants (default / large × error). Documented
Initial
size prop encodes state — "default - error" and "large - error" mix size + state. Should split. Open
C2 Open
Missing state variants — No disabled or selected. Open
C5 Open
Radio hardcoded to small — large label still uses 16 × 16 radio. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
Radio Button RestructureNeeds RefinementComponent link

A selection control for mutually exclusive choices in forms, surveys, and preference settings. 11 variants across three entangled properties: selected (unselected/selected/disabled/error), size (small/large), style (default/filled/checkmark). Matrix is sparse — style is only meaningful when a selection is present.

Split properties + rebuild as vector
Replace the sparse selected × style matrix with orthogonal props: selected: Bool + state: default/disabled/error. Retire the checkmark style (it's a checkbox affordance, not a radio). Rebuild the large radio with token-bound vector layers instead of raster SVG images. Rename .base/checkbox.base/radio. Add pressed + focused states.
In Context

Radio Buttons appear in Radio Button with Label groups — see the Radio Button with Label preview for the composed form row.

Live Preview

Toggle the three properties to see how the sparse matrix behaves — some combinations render the same as others.

Properties
selected
size
style
DS Health
Reusable
Pass
Used in forms, surveys, preference pickers. Two sizes cover 360px and 414px screen needs.
Self-contained
Warn
Small variants use token-bound borders + fills. Large variants export a pre-rendered SVG image for each state — tokens won't propagate to the large size. C3
Consistent
Warn
Two property-naming issues: selected mixes selection with modifiers (disabled/error), and style is conditional (only meaningful when selected is true). Sparse matrix with ~50% invalid combinations. C2
Composable
Warn
Internal frame is named .base/checkbox instead of .base/radio. Suggests checkbox primitives were reused here. Also the checkmark style is a checkbox affordance, not standard radio iconography. C6
Open Issues
  • Sparse variant matrix.selected × size × style=24 theoretical, ~11 valid. The style property is only meaningful when a selection is present, and selected conflates selection with modifier states. C2 · Variant & Property Naming
  • Large radio is raster-baked. Every large variant exports the ring+dot as a pre-rendered SVG image (imgContainer). Token changes won't propagate to the large size until this is rebuilt with layered vectors. C3 · Token Coverage
  • Internal frame named .base/checkbox. Misleading layer naming — the small radio nests a frame called .base/checkbox instead of .base/radio. C6 · Asset & Icon Quality
  • Checkmark style is not standard radio iconography. Radios use filled dots universally; checkmarks communicate "checked" — a checkbox affordance. The style=checkmark variant visually overlaps with Checkbox. C6 · Asset & Icon Quality
  • No pressed or focused states. Engineers must improvise these affordances. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until property split (selected/state/size) and large-radio vector rebuild land. C7 · Code Connect Linkability
Design Recommendations
  • Split properties into orthogonal axes:
    selected: Bool (true/false) — pure selection state
    state: default / disabled / error — modifier state (can combine with selected)
    size: small / large — unchanged
    Eliminates invalid combinations, maps to Swift Bool and native radio APIs. Property
  • Retire the checkmark style. Pick filled (blue dot) as the single visual style — it's the universally understood radio affordance. The checkmark variant is visually a checkbox and may cause user confusion when placed next to actual checkboxes. Rename
  • Rebuild the large radio as vector layers. Each variant today exports a flat SVG image; convert to a base ring + inner dot, both with token-bound fills. Matches the small radio's structure and lets tokens flow to both sizes. Asset
  • Rename internal frame.base/checkbox.base/radio. Minor but signals correct primitive ownership. Rename
  • Add pressed + focused states. Pressed=darker blue ring/fill; focused=outer 2px focus ring. Documents the interactive affordances native needs to render. State
Variants

Four logical states shown across both sizes. Style variations (filled vs. checkmark) are documented below but recommended for consolidation.

All states — large + small

Left column: large (20 × 20). Right column: small (16 × 16). Top to bottom: unselected, selected (filled), selected (checkmark), disabled, error, error-selected.

Colors by State
StateRoleTokenValue
Unselectedbordermain/radio-button/color/default/unselected/border#D7E0EF
Selectedbg (fill + ring)main/radio-button/color/default/selected/bg#005CE5
bordermain/radio-button/color/default/selected/border#005CE5
inner dot / checkmarkmain/radio-button/color/default/selected/icon#FFFFFF
Disabledbgmain/radio-button/color/disabled/selected/bg#C2CFE5
bordermain/radio-button/color/disabled/selected/border#C2CFE5
inner iconmain/radio-button/color/disabled/selected/icon#FFFFFF
Errorborder (unselected)main/radio-button/color/error/unselected/border#D61B2C
bg (selected)main/radio-button/color/error/selected/bg#D61B2C
border (selected)main/radio-button/color/error/selected/border#D61B2C

Large variants currently bypass these tokens — each state is a pre-rendered raster SVG. C3 fix requires rebuilding the large size with layered vectors that bind to these same tokens.

Layout
PropertyTokenValue
Large size20 × 20
Small size16 × 16
Ring border width2px
Inner dot (small filled)8 × 8 ellipse
Inner checkmark6 × 4 vector
Corner radiusspace/space-88px (fully round at 16×16)
Selected-filled wrapper paddingspace/space-44px (creates the "outer ring" illusion)
Installation Planned API

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:radio:1.0.0")
}
Property Mapping (proposed — after restructure)
Current FigmaProposedSwiftUICompose
selected=unselected/selectedselected: Boolselected: Boolselected: Boolean
selected=disabledstate=disabled (combine w/ selected).disabled(true)enabled=false
selected=errorstate=errorstate: .errorstate=EBRadioState.Error
size=small/largesize: EBRadioSize.controlSize(.small)size=EBRadioSize.Small
style=filled/checkmark(retire — pick filled only)
style=default (unselected/error)implicit from selected=false
onToggleonChange: (Bool) -> VoidonCheckedChange: (Boolean) -> Unit
SwiftUI
ios/Components/Radio/EBRadioButton.swift
Jetpack Compose
android/components/radio/EBRadioButton.kt
Usage Snippets Planned API
// Basic selection
EBRadioButton(selected: $isSelected)

// Large size with error state
EBRadioButton(selected: $isSelected, state: .error)
    .controlSize(.large)

// Disabled
EBRadioButton(selected: true)
    .disabled(true)
// Basic selection
EBRadioButton(selected = checked, onCheckedChange = { checked = it })

// Large size with error state
EBRadioButton(
    selected = checked,
    onCheckedChange = { checked = it },
    size = EBRadioSize.Large,
    state = EBRadioState.Error
)

// Disabled
EBRadioButton(
    selected = true,
    onCheckedChange = { },
    enabled = false
)
Accessibility
RequirementiOSAndroid
RoleInherit radio semantics via Toggle(isOn:) with radio styleUse Modifier.selectable(role=Role.RadioButton)
Selected state.accessibilityAddTraits(.isSelected)selected=true in semantics
Group labelWrap options in a .accessibilityElement(children: .contain) with group labelUse Modifier.selectableGroup() on parent
Tap targetRadio is 20px; wrap in 44pt hit areaWrap in 48dp hit area
Error announcementPair with a label and announce the error message after the labelUse semantics { error(...) }
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingPartialInternal frame named .base/checkbox instead of .base/radio.
C2Variant & Property NamingNeeds Fixselected mixes selection with modifiers; style is conditional.
C3Token CoverageNeeds FixLarge variants are raster — tokens don't flow to the large size.
C4Native MappabilityReadyMaps to Toggle / RadioButton with custom style.
C5Interaction State CoverageNeeds FixNo pressed or focused states.
C6Asset & Icon QualityNeeds FixCheckmark style conflicts with Checkbox visually; large radio is a pre-rendered image.
C7Code Connect LinkabilityPendingBlocked by C2. Clean mapping lands after prop split.
Variants Inventory (11 total)
selectedsizestyleNode ID
unselectedlargedefault18482:35699
unselectedsmalldefault18482:35702
selectedlargefilled18482:35715
selectedsmallfilled18482:35718
selectedlargecheckmark18482:35721
selectedsmallcheckmark18482:35724
disabledlargefilled18482:35704
disabledsmallfilled18482:35707
disabledlargecheckmark18482:35710
disabledsmallcheckmark18482:35713
errorlargedefault18482:35726

After the proposed restructure: 2 selected × 3 state × 2 size=12 well-formed orthogonal variants (no invalid combinations possible).

1.0.0 — April 2026 Initial
Initial Assessment · node 18482:35698
Component assessed — 11 variants across sparse selected × size × style matrix. Documented
Initial
Sparse matrix, mixed paradigmsselected mixes selection with modifier states; style is only meaningful when selected. Open
C2 Open
Large radio is raster-baked — tokens don't flow to the large size. Open
C3 Open
Internal frame named .base/checkbox + checkmark style overlaps with Checkbox. Open
C6 Open
No pressed/focused states. Open
C5 Open
Code Connect mappings — Not registered. Open
C7 Open
Recipient Field FixNeeds RefinementComponent link

A 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.

Fix required before handoff
Both trailing icons are non-swappable rectangles (C6). This blocks direct native property mapping for icon slots.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Send Money
Live Preview

Toggle state and fill to see the recipient field update in real time. Note the 56px height (taller than the standard 46px form field).

Properties
State
isFilled
DS Health
Reusable
Partial
Specific to recipient/contact entry in money transfer flows (Send Money, Pay Bills). Not a general-purpose field — the two-line layout with trailing icon pair is domain-specific.
Self-contained
Pass
Carries its own border, fill, label, and text styles per state. All 4 interaction states defined. 56px height, 6px corner radius. Disabled state has distinct background.
Consistent
Partial
isFilled now uses true/false (C2 fixed). Value layer renamed to #value (C1 fixed) — now consistent with sibling fields.
Composable
Warn
Trailing icons are icon-placeholder RECTANGLEs (C6) — not swappable icon instances. Cannot compose different icon actions (phonebook, scan QR) without editing the component.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesState=DefaultGray #D7E0EF border, white bg. Label + placeholder/value visible.
Active (Focused)YesYesState=ActiveBlue #005CE5 border.
ErrorYesYesState=ErrorRed #D61B2C border.
DisabledYesYesState=Disabled#EEF2F9 bg, border hidden. Muted label and text.
Resolved Issues
  • isFilled property renamed from Yes/No to true/false — now maps directly to Swift Bool / Kotlin BooleanC2 Fixed
  • Text layer renamed from #text-placeholder to #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
Open Issues
  • Code Connect mappings not registered. Structural work is complete — registration can proceed against the 8-variant State × isFilled schema. C7 · Code Connect Linkability
Design Recommendations
  • Replace icon placeholders with swappable icon instances. The two icon-placeholder RECTANGLEs block instance-swap — phonebook, scan-QR, and other trailing actions can't be dropped in without detaching the master. Slot
  • Document the 56px height rationale. Recipient Field is taller than the standard 46px form fields because of the two-line label + value layout. Call this out in the DS guidelines so it's not mistaken for a design error. Docs
  • Add an errorMessage slot below the field. Inline validation text keeps the field self-contained and matches the pattern proposed for the other form fields. Slot
States

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.

Default

Idle state with gray border. Two-line layout: small label above, value/placeholder below. Two trailing icon placeholders.

Properties
StateDefault
isFilled
Active (Focused)

Focused state with blue border indicating active input.

Properties
StateActive
isFilled
Error

Validation error state with red border.

Properties
StateError
isFilled
Disabled

Non-interactive state with gray background and hidden border. Muted label and text colors.

Properties
StateDisabled
isFilled
Colors by State

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.

RoleTokenDEFAULTACTIVEERRORDISABLED
Borderfield/border#D7E0EF#005CE5#D61B2Chidden
Backgroundfield/bg#FFFFFF#FFFFFF#FFFFFF#EEF2F9
Label textfield/label#0A2757#0A2757#0A2757#90A8D0
Value (filled)field/text/filled#0A2757#0A2757#0A2757#90A8D0
Value (empty)field/text/placeholder#90A8D0#90A8D0#90A8D0#C2CFE5
Icon placeholderfield/icon#C2C6CF#C2C6CF#C2C6CF#C2C6CF
Layout
Height56px (vs 46px standard)
Corner radius6px
Border1px stroke
Icon group68 × 32px
Icon size32 × 32px each
Icon radius41px (circular)
Typography
#label (top line)
FontHeyMeow Rnd Semibold
Size12px
Tracking0.5
#text-placeholder (bottom line)
FontHeyMeow Rnd Semibold
Size14px
Tracking0.25
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
isFilled (true/false)text: Binding<String>value: StringDerived from text content
#labellabel: Stringlabel: StringSmall top label (12px)
#text-placeholderplaceholder: Stringplaceholder: StringValue text or placeholder (14px)
icon-group (2x icons)trailingIcons: [Image]trailingIcons: @ComposableTwo action icon slots
State=DefaultDefault idle state
State=Active.focused()interactionSourceKeyboard active
State=Error.ebError(true)isError=trueValidation failed
State=Disabled.disabled(true)enabled=falseNon-interactive
SwiftUI
ios/Components/FormElements/EBRecipientField.swift
Jetpack Compose
android/components/form/EBRecipientField.kt
Usage Snippets Planned API
Default
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")
        }
    }
)
Error
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
)
Disabled
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
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 x 44 pt (56px field exceeds)48 x 48 dp (56px field exceeds)
Accessibility label.accessibilityLabel("Recipient")contentDescription
Error announcementVoiceOver reads error via .accessibilityValueTalkBack reads error via semantics { error() }
Trailing icon labels.accessibilityLabel("Contacts") per iconcontentDescription per icon button
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyLayer renamed to #value, now consistent with sibling fields.
C2Variant & Property NamingReadyisFilled=true/false — correctly uses native boolean values.
C3Token CoveragePartialColors appear correct but token binding not verified. Icon placeholders use hardcoded #C2C6CF.
C4Native MappabilityReadyMaps to custom EBRecipientField (SwiftUI + Compose). Two-line layout with trailing icon slots.
C5Interaction State CoverageReadyAll 4 states defined: Default, Active, Error, Disabled.
C6Asset & Icon QualityNeeds FixBoth trailing icons are icon-placeholder RECTANGLEs — not swappable icon instances.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Code Connect
AspectStatusNotes
Property namingReadyisFilled=true/false — boolean values map directly to native types
Layer namingBlocked#text-placeholder needs rename to #value
Icon slotsBlockedRECTANGLE placeholders — need swappable icon instances
State coverageReadyAll 4 states defined
Native component filePendingEBRecipientField.swift / EBRecipientField.kt not yet created
Variants Inventory (8 total)

4 State values × 2 isFilled values (true/false).

StateisFilledNode ID
Defaulttrue17758:3868
Defaultfalse17758:3875
Activetrue17758:3882
Activefalse17758:3889
Errortrue17758:3896
Errorfalse17758:3903
Disabledtrue17758:3910
Disabledfalse17758:3917
1.2.0 — March 2026 Fix
C1 Resolved · node 17758:3867
#text-placeholder renamed to #value — Value text layer renamed from #text-placeholder to #value. Now consistent with sibling fields (Input Field, Labeled Field) for direct native property mapping. C1 Fixed
C1 Resolved
1.1.0 — March 2026 Fix
C2 Resolved · node 17758:3867
isFilled renamed to true/false — Figma property isFilled updated from Yes/No to true/false. Now maps directly to Swift Bool and Kotlin Boolean for Code Connect. C2 Fixed
C2 Resolved
1.0.0 — March 2026 Initial
Initial Assessment · node 17758:3867
Component assessed — 8 variants documented across State (Default/Active/Error/Disabled) × isFilled (true/false). GCash-specific two-line recipient field with trailing action icons. 56px height (vs 46px standard). Documented
Initial
Boolean property uses Yes/NoisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Fixed in 1.1.0
C2 Fixed
Layer naming inconsistency — Value text layer is #text-placeholder instead of #value used by other Form Elements. Fixed in 1.2.0
C1 Fixed
Non-swappable icon placeholders — Both trailing icons are icon-placeholder RECTANGLEs, not component instances. Open
C6 Open
Code Connect mappings — No CLI mappings registered yet. Blocked by C1 and C6 issues. Open
C7 Open
Search Field RestructureRequires ReworkComponent link

A single-line text input tuned for search, with a leading search glyph and a trailing action slot. Currently ships 2 variants (state=default/filled) at 360×56px. Uses a banded top/bottom border rather than the rounded-rect stroke used by every other Form Element sibling.

Rework before handoff
Only Default and Filled exist — no focused, error, or disabled states. Search glyph is a raster img, trailing slot is an unresolved Placeholder circle, and the banded top/bottom border diverges from the rest of the Form Elements family. Consider composing from Input Field + leading/trailing icon slots instead of shipping a bespoke component.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Live Preview

Toggle state to see the search field update in real time.

Properties
state
DS Health
Reusable
Warn
Only usable when the surface tolerates a banded (top+bottom border) look. Won't compose into forms that use the standard rounded-rect field styling. No size variants, no dark mode.
Self-contained
Warn
Trailing icon-container holds an unresolved Placeholder wrapper with a raw icon-placeholder circle — consumers must instance-swap to get a usable clear/cancel affordance. Missing focused, error, and disabled tokens.
Consistent
Warn
Variant axis is state=default/filled — conflates "has content" with the four interaction states used by every sibling field (Default/Active/Error/Disabled). Token namespace (main/search/*) isolates this from the shared field/* tokens other siblings rely on.
Composable
Partial
Trailing icon-container is a real slot and accepts a swap (swapIcon). Leading icon is not slotted — locked to the bundled search glyph (which is raster, not vector).
Behavior
StateiOSAndroidFigma PropertyNotes
Default (empty)YesYesstate=defaultPlaceholder text at 50% opacity, trailing slot holds placeholder circle.
Filled (has query)YesYesstate=filledText at full opacity (#0A2757), identical container, trailing slot unchanged.
FocusedNoNoNo visible focused variant. Native focus ring cannot be approximated from DS.
ErrorNoNoNo error state defined.
DisabledNoNoNo disabled state defined.
Open Issues
  • State coverage is incomplete. Only default and filled are shipped; focused, error, and disabled are absent. Native TextField / SearchBar expect all four interaction states for focus rings, validation, and disabled styling. C5 · Interaction State Coverage
  • Leading search glyph is a raster asset. The search icon renders via <img src={imgShapeFull}> (raster PNG) rather than a vector instance from the icon library. Blocks token-based recoloring and fails crisp rendering on high-density displays. C6 · Asset & Icon Quality
  • Trailing slot ships a Placeholder wrapper with a raw circle.icon-container contains a Placeholder frame wrapping an icon-placeholder pink-circle shape. This is authoring scaffolding that should be replaced with a real clear/cancel icon (or removed) before the component leaves design. C1 · Layer Structure & Naming
  • state variant axis conflates content and interaction.state=default/filled is a derived content signal (has a value or not) — it shouldn't occupy the same axis that other Form Elements reserve for Default/Active/Error/Disabled. Also fails Code Connect's expectation of boolean-like or enum-of-states schemas. C2 · Variant & Property Naming
  • Banded border diverges from the Form Elements family. Container uses border-top + border-bottom only, no left/right, radius-0. Every sibling (Input, Labeled, Select, Recipient, View Only) uses a full rounded-rect stroke at 6px radius. No native primitive renders this shape by default — forces custom background work on both platforms. C4 · Native Mappability
  • Code Connect mappings not registered. Structural gaps (C1/C2/C5/C6) must be resolved before linking. No native file exists yet. C7 · Code Connect Linkability
Design Recommendations
  • Compose from Input Field instead of shipping a parallel primitive. Once Input Field gains leadingIcon / trailingIcon slots (already recommended in its assessment), a Search Field becomes Input Field + search glyph leading + clear-button trailing — no new component needed. Retires main/search/* tokens and inherits Default/Active/Error/Disabled for free. Composition
  • Swap the raster shape_full for the canonical search icon instance. Reference the same vector icon used elsewhere (24px Search Small) so it inherits main/{component}/color/icon-leading recoloring across modes. Asset
  • Replace the trailing Placeholder wrapper with a real Clear (X) icon instance. The current Placeholder > container > icon-placeholder path is authoring scaffolding. Bind to a 24px Close / Clear icon and expose it as an optional slot that hides when state=default. Slot
  • Add Active, Error, and Disabled variants, and split content-filled from interaction state. Adopt the sibling schema: State=Default | Active | Error | Disabled plus a boolean isFilled=true/false. That yields 8 variants and matches Input Field's axis model exactly. State
  • Align the border treatment with the Form Elements family. If Search Field remains a standalone component, switch to the shared 6px rounded-rect stroke — the banded top/bottom look is a screen-level pattern (section divider), not a field chrome treatment, and can be added by the surrounding layout. Family
  • Rename the main/search/color/default/* namespace to match the new schema. Either retire to field/* (if composed) or expand to main/search/color/{default|active|error|disabled}/* so tokens cover every state. Remove the /default/ sub-mode once other states exist. Token
  • Document search semantics for native handoff. iOS uses .searchable(text:) on a container (it is not a standalone view); Android uses Material 3 SearchBar (which expands into full-screen search) or a TextField with a leading search icon. Document both paths and the Enter-to-submit / Escape-to-clear keyboard contract. Docs
  • Add role="search" / search semantics and a labeled clear button. The clear-button slot needs its own accessibility label ("Clear search"). iOS VoiceOver and Android TalkBack must announce the field as a search input, not a generic text field. A11y
States

2 variants on a single axis: state=default | filled. Both share the same 360×56px container, banded top/bottom border, and trailing placeholder slot. State only toggles text color and opacity.

Default

Empty state. Placeholder label at 50% opacity (#90A8D0), leading search glyph at 80% opacity.

Properties
statedefault
Filled

State shown when a query has been entered. Text uses #0A2757 at full opacity.

Properties
statefilled
Colors by State

Only a single variable mode (default) is bound on main/search/*. Focused, error, and disabled tokens do not exist yet.

RoleTokenDEFAULTFILLED
Backgroundmain/search/color/default/bg#FFFFFF#FFFFFF
Border (top + bottom)main/search/color/default/border#F6F9FD (80%)#F6F9FD (80%)
Placeholdermain/search/color/default/placeholder#90A8D0 (50%)
Textmain/search/color/default/text#0A2757
Icon (leading)main/search/color/default/icon-leading#6780A9 (80%)#6780A9 (80%)
Icon (trailing)main/search/color/default/icon-trailing#6780A9#6780A9

Leading glyph is currently a raster img; the icon-leading token is declared but not applied to a vector fill.

Layout
PropertyValueToken
Container size360 × 56 px
Padding (horizontal)22 px left / 24 px right— / space/space-24
Padding (vertical)16 pxspace/space-16
Gap (icon ↔ text)8 pxspace/space-8
Gap (trailing slot)12 pxspace/space-12
Corner radius0radius/radius-0
Border1 px top + bottom only
Leading icon size24 × 24 px
Trailing slot size24 × 24 px
Typography
PropertyValue
DS text styleSecondary/Bold/Base
Font familyBarkAda
WeightSemibold (600)
Size14 px (font-size-20)
Line height20 px (leading-40)
Tracking0 (tracking-normal)
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
state=default / filledtext: Binding<String>query: StringFilled is derived — not a native parameter.
— (missing).focused() / @FocusStateinteractionSourceNo Figma variant for focused. Add Active state first.
— (missing).disabled(true)enabled=falseNo disabled variant.
swapIcon (trailing)trailingIcon: Image?trailingIcon: @ComposableCurrently holds a Placeholder circle — should be Clear (X).
labelprompt: Textplaceholder: StringPlaceholder copy ("Search").
SwiftUI
ios/Components/FormElements/EBSearchField.swift
Jetpack Compose
android/components/form/EBSearchField.kt
Usage Snippets Planned API
Standalone Search Field
EBSearchField("Search", text: $query,
    onSubmit: { runSearch(query) },
    onClear: { query = "" })
EBSearchField(
    query = query,
    onQueryChange = { query = it },
    onSearch = { runSearch(query) },
    placeholder = "Search"
)
Preferred: .searchable on a container

iOS treats search as a modifier on a navigation container, not a standalone view. Use .searchable when the search field drives a scene's content.

NavigationStack {
    List(results) { row in Text(row.title) }
}
.searchable(text: $query, prompt: "Search")
SearchBar(
    query = query,
    onQueryChange = { query = it },
    onSearch = { runSearch(query) },
    active = active,
    onActiveChange = { active = it },
    placeholder = { Text("Search") },
    leadingIcon = { Icon(Icons.Default.Search, null) }
) { /* results */ }
Alternative: compose from EBInputField

Once Input Field gains leading/trailing icon slots, Search Field can be a thin composition — this is the recommended path (see Design Recommendations).

EBInputField("Search", text: $query)
    .ebLeadingIcon(Image("search"))
    .ebTrailingIcon(query.isEmpty ? nil : Image("close")) { query = "" }
    .ebRole(.search)
EBInputField(
    value = query,
    onValueChange = { query = it },
    placeholder = "Search",
    leadingIcon = { Icon(Icons.Default.Search, null) },
    trailingIcon = if (query.isNotEmpty()) {
        { IconButton({ query = "" }) { Icon(Icons.Default.Close, "Clear search") } }
    } else null
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 × 44 pt (container is 56pt ✓)48 × 48 dp (container is 56dp ✓)
Search role / trait.searchable or .accessibilityAddTraits(.isSearchField)SearchBar sets role automatically, else semantics { role=Role.TextField; contentType=ContentType.SearchQuery }
Clear button label.accessibilityLabel("Clear search")contentDescription="Clear search"
Submit / Enter.submitLabel(.search) + onSubmitkeyboardOptions=KeyboardOptions(imeAction=ImeAction.Search)
Escape to clearHardware keyboard: handle in onKeyPress(.escape)Handle in onKeyEvent for keyboard users
Usage Guidelines

Do

Use Search Field for free-text query input that filters or retrieves results. Show the clear (X) button only when the field has content.

Don't

Use Search Field for destinations that don't actually filter or search. Use Input Field for generic text entry.

Do

Pair with a results region below the field and announce result counts to assistive tech when the query updates.

Don't

Ship the placeholder circle in the trailing slot — always swap to a real Clear / Cancel icon before publishing a screen.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkTrailing Placeholder > container > icon-placeholder chain is authoring scaffolding, not a named icon.
C2Variant & Property NamingReworkstate=default/filled conflates content with interaction. Diverges from sibling State axis (Default/Active/Error/Disabled).
C3Token CoveragePartialAll visible colors bound to main/search/color/default/*, but only a default sub-mode exists — no tokens for focused/error/disabled.
C4Native MappabilityReworkTop+bottom banded border isn't a native default. Missing role=search semantics in layer model.
C5Interaction State CoverageReworkOnly default/filled. No focused, error, or disabled variants.
C6Asset & Icon QualityReworkLeading search glyph is a raster img (shape_full), not a vector instance.
C7Code Connect LinkabilityNot MappedBlocked by C1/C2/C5/C6. No CLI mappings registered.
Code Connect
AspectStatusNotes
Property namingReworkstate=default/filled axis needs split into State (enum) + isFilled (bool)
State coverageReworkMissing Active / Error / Disabled
Icon qualityReworkRaster leading glyph + placeholder trailing slot
Native component fileNot CreatedEBSearchField.swift / EBSearchField.kt not yet created
Variants Inventory (2 total)

A single state axis with two values. Both variants are 360 × 56 px.

stateDimensionsNode ID
default360 × 5650:78118
filled360 × 5650:78126
1.0.0 — April 2026 Initial
Initial Assessment · node 18577:14520
Component assessed — 2 variants documented (state=default/filled). Part of Form Elements group. Verdict: Restructure / Requires Rework. Documented
Initial
State coverage incomplete — only default and filled; focused, error, disabled missing. Open
C5 Open
Leading glyph is rastershape_full rendered via img, not a vector instance. Open
C6 Open
Trailing slot ships Placeholder wrapper — unresolved Placeholder > icon-placeholder circle rather than a real Clear icon. Open
C1 Open
state axis conflates content and interactiondefault/filled is a derived content signal, not a state-machine value. Open
C2 Open
Banded border diverges from family — top+bottom only, radius-0. Siblings use full rounded-rect stroke at 6px. Open
C4 Open
Code Connect mappings — not registered. Blocked by C1/C2/C5/C6. Open
C7 Open
Select Field FixNeeds RefinementComponent link

A 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.

Fix required before handoff
Peso sign uses BOOLEAN_OPERATION instead of vector (C6). Flag uses raster IMAGE fill (C6). These block clean native mapping.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Send Money₱ Amount
Live Preview

Toggle state and fill to see the select field update in real time.

Properties
State
isFilled
DS Health
Reusable
Partial
Currency/amount selection pattern used across Send Money, Buy Load, Pay Bills, and other GCash flows. Tightly coupled to Philippine peso — not generalizable to other currencies without modification.
Self-contained
Pass
Carries its own border, fill, peso sign, flag, and chevron per state. All 4 interaction states defined with distinct visual treatment. Disabled state has separate background.
Consistent
Partial
isFilled now uses true/false (C2 fixed). Peso sign still uses shape_full BOOLEAN_OPERATION instead of a clean vector (C6).
Composable
Pass
Nests cleanly in form layouts alongside Input Field, Labeled Field, and Recipient Field. Chevron down signals tappable selection affordance.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesState=DefaultGray #D7E0EF border, white bg. Peso sign #183462.
Active (Focused)YesYesState=ActiveBlue #005CE5 border.
ErrorYesYesState=ErrorRed #D61B2C border.
DisabledYesYesState=Disabled#EEF2F9 bg, border hidden. Peso sign #7E96BE.
Resolved Issues
  • isFilled renamed from Yes/No to true/false for direct Swift Bool / Kotlin Boolean mapping C2 Fixed
  • Peso Sign shape_full BOOLEAN_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
Open Issues
  • Code Connect mappings not registered. Structural work is complete — registration can proceed against the 8-variant State × isFilled schema. C7 · Code Connect Linkability
Design Recommendations
  • Flatten the Peso Sign shape_full BOOLEAN_OPERATION. Boolean-operation paths render inconsistently across SVG export and native platforms. Replace with a single flattened vector path. Asset
  • Use a vector flag asset. The Philippine flag is currently a raster IMAGE fill. Swap to a vector from the DS icon library so it stays crisp across DPIs and platforms. Asset
  • Generalize to multi-currency. Expose currency symbol and flag as configurable slots so the field can support other currencies (USD, EUR, SGD) without creating a new component per currency. Property
  • Add a helperText slot. Error state has no accompanying text guidance today — add a slot for validation messages consistent with the other form fields. Slot
States

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).

Default

Idle state with gray border. Peso sign in dark navy, flag visible, chevron down affordance.

Properties
StateDefault
isFilled
Active (Focused)

Focused state with blue border indicating active selection.

Properties
StateActive
isFilled
Error

Validation error state with red border.

Properties
StateError
isFilled
Disabled

Non-interactive state with gray background, hidden border, and muted peso sign.

Properties
StateDisabled
isFilled
Colors by State

All states share the same container structure. Border color is the primary state indicator. Peso sign and text colors shift in disabled state.

RoleTokenDEFAULTACTIVEERRORDISABLED
Borderfield/border#D7E0EF#005CE5#D61B2Chidden
Backgroundfield/bg#FFFFFF#FFFFFF#FFFFFF#EEF2F9
Label textfield/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 signfield/icon/peso#183462#183462#183462#7E96BE
Layout
PropertyValue
Height46px
Corner radius6px
Peso sign size15 × 15
Flag size25 × 16
Flag corner radius2px
Chevron size32 × 32
Typography
LayerPropertyValue
#labelFontHeyMeow Rnd Semibold
Size16px
#valueFontHeyMeow Rnd
Size14px
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
isFilled (true/false)selection: Binding<String?>selectedValue: String?Derived from selection content
State=DefaultDefault idle state
State=Active.focused()interactionSourceSelection active
State=Error.ebError(true)isError=trueValidation failed
State=Disabled.disabled(true)enabled=falseNon-interactive
SwiftUI
ios/Components/FormElements/EBSelectField.swift
Jetpack Compose
android/components/form/EBSelectField.kt
Usage Snippets Planned API
Default
EBSelectField("Amount", selection: $amount)
EBSelectField(
    label = "Amount",
    selectedValue = amount,
    onValueChange = { amount = it }
)
Error
EBSelectField("Amount", selection: $amount)
    .ebError(true)
EBSelectField(
    label = "Amount",
    selectedValue = amount,
    onValueChange = { amount = it },
    isError = true
)
Disabled
EBSelectField("Amount", selection: $amount)
    .disabled(true)
EBSelectField(
    label = "Amount",
    selectedValue = amount,
    onValueChange = { amount = it },
    enabled = false
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 x 44 pt48 x 48 dp
Accessibility label.accessibilityLabel("Select amount")contentDescription
Role hint.accessibilityHint("Double tap to select")semantics { role=Role.DropdownList }
Error announcementVoiceOver reads error via .accessibilityValueTalkBack reads error via semantics { error() }
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic layer names: container, peso-sign, text-container, flag-container, Chevron Down.
C2Variant & Property NamingReadyisFilled=true/false — correct boolean convention for native mapping.
C3Token CoveragePartialColors appear correct but token binding not fully verified.
C4Native MappabilityReadyMaps to custom EBSelectField (SwiftUI) / EBSelectField (Compose).
C5Interaction State CoverageReadyAll 4 states defined: Default, Active, Error, Disabled.
C6Asset & Icon QualityNeeds FixPeso sign uses shape_full BOOLEAN_OPERATION (not a vector). Flag uses raster IMAGE fill.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Code Connect
AspectStatusNotes
Property namingReadyisFilled=true/false — boolean convention now correct for Code Connect mapping
Asset qualityBlockedPeso sign BOOLEAN_OPERATION and raster flag need replacement
State coverageReadyAll 4 states defined
Native component filePendingEBSelectField.swift / EBSelectField.kt not yet created
Variants Inventory (8 total)

4 State values × 2 isFilled values (true/false).

StateisFilledNode ID
Defaulttrue17758:3787
Defaultfalse17758:3797
Activetrue17758:3807
Activefalse17758:3817
Errortrue17758:3827
Errorfalse17758:3837
Disabledtrue17758:3847
Disabledfalse17758:3857
1.1.0 — March 2026 Update
C2 Fix — isFilled boolean naming · node 17758:3786
isFilled renamed from Yes/No to true/false — Figma component now uses correct boolean convention. Enables direct Swift Bool / Kotlin Boolean mapping for Code Connect. Fixed
C2 Fixed
1.0.0 — March 2026 Initial
Initial Assessment · node 17758:3786
Component assessed — 8 variants documented across State (Default/Active/Error/Disabled) × isFilled (true/false). Currency/amount selection field with peso sign, flag, and chevron. Part of Form Elements group. Documented
Initial
Boolean property uses Yes/NoisFilled=Yes/No instead of true/false. Incompatible with Swift Bool and Kotlin Boolean for Code Connect mapping. Fixed in 1.1.0
C2 Fixed
Peso sign uses BOOLEAN_OPERATIONshape_full is a BOOLEAN_OPERATION, not a clean vector path. May render inconsistently on native platforms. Open
C6 Open
Flag uses raster IMAGE fill — Philippine flag in flag-container uses a raster IMAGE fill instead of a vector. May degrade on high-density displays. Open
C6 Open
Code Connect mappings — No CLI mappings registered yet. Blocked by C6 (asset quality). Open
C7 Open
Stepper - Bullet RestructureReworkComponent link

A horizontal row of 8×8 dots — one filled in brand blue to indicate the current step, the rest in the light-blue track color. Today the family ships as 3 separate sibling components (Stepper - Bullet - 3 Steps, - 4 Steps, - 5 Steps), one per hardcoded step count. Inside each, a highlighted=1st | 2nd | … | Nth variant encodes which dot is active. There is no single Stepper component, no steps prop, and no current prop — both are baked into the variant name and sibling file.

Restructure — collapse 3 sibling components into one Stepper - Bullet with steps and current properties
Step count is a scalar, not a component axis. The current schema has 3 top-level components (3/4/5 steps) and inside each, an highlighted=1st … Nth ordinal enum for the active dot. Rebuild as a single component: steps: Int (3–10) and current: Int (1..steps). Replace the raster dot PNGs with a vector Ellipse whose fill is bound to main/stepper/color/bg (active) or main/stepper/color/bg-track (inactive). Native side maps to a custom EBStepperBullet(current:total:) rendered as an HStack / Row of Circle shapes. Better still: unify Dash + Bullet + Circular into one EBStepper(current:total:style: .bullet | .dash | .circular) API.
In Context

Stepper - Bullet appears at the top or bottom of multi-step flows — most commonly paginated onboarding, swipeable carousels of tutorial cards, and photo galleries where the user needs a minimal position indicator without numerical labels.

Live Preview

Adjust steps and current to see the proposed parameterized component. The active dot fills in brand blue; the rest show the light-blue track color.

Content (proposed)
steps
current
value2 of 4
DS Health
Reusable
Partial
Fits any low-information position indicator (carousel, onboarding), but the 3-sibling split forces consumers to swap components when step count changes instead of flipping a prop.
Self-contained
Warn
Each 8×8 dot renders as a raster <img>. Two PNG assets per sibling (filled + track) — six total for what should be one vector Ellipse with two token-bound fills.
Consistent
Fail
Step count modeled as 3 top-level components, same anti-pattern as Stepper - Circular (9 siblings). Every other scalar axis in the DS is a property. Breaks the naming hierarchy — "Stepper - Bullet" is three components, not one.
Composable
Partial
Small footprint (8px dots, 8px gaps) composes cleanly into carousel, modal, and onboarding layouts. But no connector line between dots means it reads as isolated markers rather than a progress rail.
Behavior
StateiOSAndroidFigma SpecNotes
Current stepYesYesfill=bgDot at current index fills in main/stepper/color/bg (#005CE5).
Other stepsYesYesfill=bg-trackAll other dots fill in main/stepper/color/bg-track (#D2E5FF). Note: the spec does not distinguish completed vs upcoming — both look identical.
Completed vs upcomingMissingMissingNot modeledBullet steppers in other systems often shade completed dots differently from upcoming ones. This family collapses both into the track color — direction of travel is lost.
Clickable / interactiveOptionalOptionalNot modeledUsed in carousels, some implementations let the user tap a dot to jump to that page. No pressed / focused state exists today.
Connector lineMissingMissingNot modeledClassic Material / iOS bullet steppers draw a thin line between dots tinted to match completed / upcoming. This family uses blank 8-px gaps instead.
Open Issues
  • Step count is modeled as 3 sibling components instead of a steps prop. The family ships as Stepper - Bullet - 3 Steps, - 4 Steps, - 5 Steps — three top-level components that differ only by hardcoded count. Same anti-pattern as Stepper - Circular (9 siblings). Should be a single Stepper - Bullet with steps: Int and current: Int — 3× maintenance collapses to 1×, and the component scales to 6, 7, 8+ without new files. C1 · Layer Structure & Naming
  • Variant axis highlighted=1st | 2nd | … | Nth uses ordinal enums instead of an integer. Each sibling has a nested symbol with highlighted=1st, 2nd, 3rd, 4th, 5th to mark the active dot. Ordinals don't compose — the 4-step sibling can't have a "5th" option, and there's no path to current=N+1. Promote to a top-level integer current: 1..steps. C2 · Variant & Property Naming
  • Every dot is a raster <img> PNG — 8×8 ellipse baked as an image. Two PNGs per sibling (filled + track) × 3 siblings=six raster assets for what is mathematically a filled circle. Blocks theming (can't retint), breaks at @3x, and ships bytes the native renderer doesn't need. An 8-px Circle() / Box(Modifier.clip(CircleShape)) with a token-bound fill is all that's required. C6 · Asset & Icon Quality
  • No native primitive matches — this needs a custom component. SwiftUI has no BulletStepper; Material 3 provides only LinearProgressIndicator and a linear Stepper. Both platforms need a custom EBStepperBullet built from an HStack/Row of Circle/Box shapes. The raster-baked dots make this worse — the dev can't reuse the asset. C4 · Native Mappability
  • No completed / upcoming distinction, no pressed / focused states, no connector line. The spec treats every non-current dot identically. Users can't see direction of travel. Tappable-to-jump behavior (common in carousels) has no pressed state modeled. And the 8-px blank gaps between dots would normally be a connector rail in classic bullet steppers. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the family collapses to one component and the raster dots are replaced with vector circles. Mapping 3 separate siblings would codify the anti-pattern into the tooling. C7 · Code Connect Linkability
Design Recommendations
  • Collapse all 3 siblings into one Stepper - Bullet with steps and current properties. Delete Stepper - Bullet - 3 Steps, - 4 Steps, - 5 Steps as separate components. Create one Stepper - Bullet with steps: 3 | 4 | … | 10 and current: 1 | 2 | … | 10. Variant math drops from 3 top-level × 3–5 highlighted=12 pre-baked variants to 1 component with runtime-computed fills. Native API: EBStepperBullet(current: Int, total: Int). Family
  • Unify Dash + Bullet + Circular under one EBStepper(current:total:style:) API. All three siblings share the same data shape (current + total) and the same token set (main/stepper/color/*). Collapse them into one native component with style: .dash | .bullet | .circular. Figma keeps three component symbols (different visual languages) but they share the same property schema — making migration / swap between styles trivial. Family
  • Rename the nested highlighted=1st | 2nd | … | Nth ordinal axis to an integer current. Ordinal enums don't scale and conflate position with presentation. Use current: Int at the top level and let each dot compute its own fill from index==current ? bg : bg-track. Property
  • Replace raster dot PNGs with vector Ellipse fills bound to tokens. Each dot is an 8×8 ellipse — the simplest possible vector. Two fills only: main/stepper/color/bg (active) and main/stepper/color/bg-track (inactive). No PNG assets, resolution-independent, theme-able. Asset
  • Add completed vs upcoming differentiation. Optional but common: completed dots use a muted brand tint; upcoming use the track. Model as status: completed | current | upcoming computed per-slot from current. Adds direction-of-travel cue without the user having to count. State
  • Spec a connector line between dots (optional variant). Classic bullet steppers draw a 1–2 px line between each dot pair, tinted to match completed (brand) vs upcoming (track). Today the 8-px blank gap reads as isolated markers. Add showConnector: Bool (default off for carousel-style use, on for wizard-style use). Property
  • Add an orientation property for vertical layouts. Dot steppers sometimes appear as a vertical list in sidebars or long-form onboarding. Add orientation: horizontal | vertical. Property
  • Build as a custom native component. Neither SwiftUI nor Material has a BulletStepper primitive. Ship EBStepperBullet: iOS uses HStack { ForEach(0..<total) { Circle().fill(index==current ? Color.stepperBg : Color.stepperBgTrack).frame(width: 8, height: 8) } }; Android uses Row { repeat(total) { Box(Modifier.size(8.dp).clip(CircleShape).background(if (it==current) EBTokens.stepperBg else EBTokens.stepperBgTrack)) } }. Composition
  • Announce "Step X of Y" to screen readers. The component is decorative by default — assistive tech reads nothing. Wrap in a semantic container that announces "Step \(current) of \(total)" (SwiftUI .accessibilityLabel, Compose Modifier.semantics { contentDescription=… }). Also: minimum touch target is 44×44 — if dots are tappable, wrap each in a padded hit area, don't make the 8-px dot itself the target. A11y
  • Document the canonical composition and retire the sibling names. Update the sticker sheet page to show one Stepper - Bullet with property controls; add a migration note pointing Stepper - Bullet - N Steps consumers at the new steps prop. Docs
Stepper - Bullet canonical node 27:48287 (5 Steps) · sibling frames 27:48235 (3 Steps), 27:48254 (4 Steps)

Horizontal row of N 8×8 dots with one dot filled in brand blue to indicate the current step. 3 hardcoded sibling frames today; target is one component with a steps prop.

Properties (today)
Top-level componentStepper - Bullet - N Steps (×3)
Inner varianthighlighted=1st | 2nd | … | Nth
Dot implementationraster <img>
Layouthorizontal only
Properties (proposed)
steps3 | 4 | … | 10
current1 | 2 | … | steps
orientationhorizontal | vertical
status (per slot)completed | current | upcoming
Colors by State
ROLETOKENVALUE
Dot (current)main/stepper/color/bg#005CE5
Dot (other)main/stepper/color/bg-track#D2E5FF

Both roles already use main/stepper/color/* tokens shared with Stepper - Circular and Stepper - Dash — token coverage is clean. The restructure work is schema and asset (raster → vector), not color.

Layout
Dot size8 × 8
Gap between dots8 (spacer width)
Outer frame padding0 horizontal, 4 vertical (space/space-4)
Container gap tokenspace/space-0
Corner radiusfull (circle)
Total width (N dots)8 × N + 8 × (N−1)=16N − 8
Height16
Typography
No text layers. Bullet stepper has no numbers or labels.
Screen readers should receive "Step X of Y" as a synthesized accessibility label — see Recommendations.
Installation Planned API

No native primitive matches the bullet stepper pattern — ship a custom EBStepperBullet component on both platforms. Consider shipping it as a style mode of a shared EBStepper along with dash and circular.

.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBStepperBullet
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
Stepper - Bullet - N Steps (×3 siblings)Stepper - Bullet (single component)EBStepperBulletEBStepperBullet
(implicit in sibling name)steps: 3…10total: Inttotal: Int
highlighted=1st | … | Nth (inner symbol)current: 1…stepscurrent: Intcurrent: Int
(horizontal only)orientation: horizontal | verticalorientation: Axis=.horizontalorientation: EBStepperOrientation=Horizontal
(single track color for all non-current)status: completed | current | upcoming (per slot)(derived internally from current)(derived internally from current)
(raster dot PNGs)Vector Ellipse fillsCircle().fill(...)Box(Modifier.clip(CircleShape).background(...))
Suggested file paths
  • ios/Components/Stepper/EBStepperBullet.swift — custom view, HStack of Circles
  • android/components/stepper/EBStepperBullet.kt — Row of Box composables with CircleShape background
  • Long-term: unify with Dash + Circular under ios/Components/Stepper/EBStepper.swift and android/components/stepper/EBStepper.kt exposing a style enum.
Usage
// Onboarding carousel — page 2 of 4
EBStepperBullet(current: 2, total: 4)

// Tutorial swipe — page 3 of 5
EBStepperBullet(current: 3, total: 5)

// With completed / upcoming distinction
EBStepperBullet(current: 2, total: 4, showProgress: true)

// Unified Stepper API (future)
EBStepper(current: 2, total: 4, style: .bullet)
// Onboarding carousel — page 2 of 4
EBStepperBullet(current = 2, total = 4)

// Tutorial swipe — page 3 of 5
EBStepperBullet(current = 3, total = 5)

// With completed / upcoming distinction
EBStepperBullet(current = 2, total = 4, showProgress = true)

// Unified Stepper API (future)
EBStepper(current = 2, total = 4, style = EBStepperStyle.Bullet)
Accessibility
RequirementiOSAndroid
Progress roleWrap the row in .accessibilityElement(children: .ignore) and expose one semantic label. Use .accessibilityValue("Step \(current) of \(total)").Merge descendants via Modifier.semantics(mergeDescendants=true) with contentDescription="Step $current of $total".
Value announcementVoiceOver reads "Step 2 of 4". Do not announce each dot individually — they're decorative at the unit level.TalkBack reads the merged label. Update stateDescription when current changes to trigger re-announcement.
Touch targets (if interactive)Dots are 8×8 — far below the 44×44 HIG minimum. If tappable, wrap each in a Button with an 18-px invisible hit-area padding on all sides.Same. Wrap each dot in Modifier.clickable().minimumInteractiveComponentSize() so the 48-dp target is enforced.
ContrastActive #005CE5 on white=5.3:1 ✓. Inactive #D2E5FF on white=1.2:1 — below the 3:1 non-text graphic threshold. Inactive dots rely on position + count to communicate, not contrast alone. Pair with announced label.Same ratios. Same concern.
Reduce motionIf animating between current values, honor UIAccessibility.isReduceMotionEnabled and snap instead of cross-fading.Honor Settings.Global.ANIMATOR_DURATION_SCALE and skip the animation when disabled.
Usage Guidelines
Do
  • Use for low-information position indicators — paginated onboarding, tutorial carousels, image galleries.
  • Keep total steps between 3 and 6 — more than that reads as "many dots" and loses its countability.
  • Pair with a screen title or page heading — the stepper alone doesn't tell the user what they're progressing through.
  • Center horizontally under the content it describes.
Don't
  • Don't use for numbered wizard flows — use Stepper - Circular (numbered) so users can see "Step 2 of 4" without counting dots.
  • Don't use for single-screen content — no position is being communicated.
  • Don't ship the 3 sibling components in product code — consume the parameterized version.
  • Don't make the 8-px dots the tap target — always pad the hit area to 44×44 (iOS) / 48×48 dp (Android).
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkStep count is modeled as 3 sibling components (Stepper - Bullet - 3/4/5 Steps) instead of a property. Collapse to a single Stepper - Bullet with steps + current.
C2Variant & Property NamingReworkNested highlighted=1st | 2nd | … | Nth ordinal axis should become a top-level integer current: 1..steps. Ordinals don't compose across step counts.
C3Token CoverageReadyFills bound to main/stepper/color/bg and main/stepper/color/bg-track. Padding bound to space/space-4, gap to space/space-0. Shared token set with Stepper - Circular and Stepper - Dash.
C4Native MappabilityReworkNo native primitive matches. Requires custom EBStepperBullet on both platforms. Unify with Dash + Circular under a shared EBStepper(style:) API.
C5Interaction State CoverageReworkNo completed / upcoming differentiation, no pressed / focused / tappable state, no connector line, no vertical orientation. Every non-current dot is identical.
C6Asset & Icon QualityReworkDots are raster <img> PNGs — two per sibling (filled + track). Replace with vector Ellipse with token-bound fills. Trivial vector; no reason to bake as raster.
C7Code Connect LinkabilityNot MappedBlocked until 3 siblings collapse to one component and raster dots are replaced with vectors. Consider mapping through a unified EBStepper(style: .bullet) API.
Variants Inventory (3 sibling components · 12 pre-baked highlighted variants)

Today: 3 sibling components, one per hardcoded step count, each with Nhighlighted variants (3+4+5=12 pre-baked variants). Target: 1 component with steps: 3…10 and current: 1…steps as runtime properties; no pre-baked variants needed.

#Sibling componentNodeFrame (w × h)Highlighted variants
1Stepper - Bullet - 3 Steps27:4823580 × 1283 (highlighted=1st, 2nd, 3rd)
2Stepper - Bullet - 4 Steps27:4825496 × 1644 (highlighted=1st, 2nd, 3rd, 4th)
3Stepper - Bullet - 5 Steps27:48287112 × 2005 (highlighted=1st, 2nd, 3rd, 4th, 5th)

Per-symbol node IDs: 3 Steps — 27:4823627:48248; 4 Steps — 27:4825527:48279; 5 Steps — 27:4828827:48328.

1.0.0 — April 2026 Initial
Initial Assessment · canonical node 27:48287 (5 Steps) + 2 siblings (27:48254 4 Steps, 27:48235 3 Steps)
Verdict: Restructure — Collapse 3 sibling components (Stepper - Bullet - 3/4/5 Steps) into one Stepper - Bullet with steps: Int and current: Int properties. Replace raster dot PNGs with vector ellipses. Long-term, unify with Dash + Circular under EBStepper(style:). Open
Schema
C1 — Family structure — 3 top-level components differ only by hardcoded step count. Collapse into one component with a steps property. Same anti-pattern as Stepper - Circular. Open
C1
C2 — Property shape — Nested highlighted=1st | 2nd | … | Nth ordinal axis should become a top-level integer current. Open
C2
C4 — Native mapping — No native primitive matches. Requires custom EBStepperBullet on both platforms built over HStack/Row of Circle shapes. Consider unifying with Dash + Circular under EBStepper(style:). Open
C4
C5 — Missing states — No completed / upcoming distinction, no pressed / focused states for interactive carousels, no connector line, no vertical orientation. Open
C5
C6 — Raster dots — Each 8×8 dot is a pre-baked PNG. Replace with vector Ellipse fills bound to main/stepper/color/bg and main/stepper/color/bg-track. Open
C6
C7 — Code Connect — Mappings pending restructure. Mapping 3 separate siblings would codify the anti-pattern. Open
C7
Stepper - Circular RestructureReworkComponent link

A horizontal row of numbered circles — each circle's ring shows how far through a multi-step flow the user is. Today the component ships as 9 separate sibling components (Stepper - Circular - 2 Steps through … - 10 Steps), one per hardcoded step count. Inside each one, every step circle is a 45×45 symbol (number=1…N) whose ring arc is a pre-rendered raster <img>. There is no single Stepper component, no steps prop, and no current prop — the "current" step is implicit in which variant the designer drops in.

Restructure — collapse 9 sibling components into one Stepper - Circular with steps and current properties
Step count is a scalar — not a component axis. The current schema has 9 top-level components (2..10 steps) and inside each, 10 symbols of pre-rendered ring arcs. Every step count ships ~N PNGs. Rebuild as a single component: steps: Int (range 2–10) and current: Int (1..steps). Redraw the ring as a stroked SVG arc derived from current / steps so the fill scales with math, not image swaps. Native side maps to a custom EBStepperCircular(current:total:) rendered over an HStack / Row of Circle shapes with .trim(from:to:) or drawArc.
In Context

Stepper - Circular appears at the top of multi-step flows (onboarding, KYC, form wizards) to show position and total count. Consumers pair it with a screen title and step-specific content.

Live Preview

Adjust steps and current to see the proposed parameterized component. The ring arc for the active step is derived from current / steps; completed steps show a full ring; upcoming steps show only the track.

Content (proposed)
steps
current
value2 of 4
DS Health
Reusable
Partial
Visually fits any multi-step flow, but the 9-sibling split forces consumers to swap components when step count changes, not just a prop.
Self-contained
Warn
Each step circle's ring arc renders via raster <img>. ~10 PNGs per sibling × 9 siblings ≈ 50+ assets for what should be one SVG arc with two parameters.
Consistent
Fail
Step count modelled as 9 top-level components. Every other scalar in the DS (Badge counter, Progress Bar target) is a property, not a component family. Breaks the naming hierarchy — "Stepper - Circular" is not a component, it's nine.
Composable
Partial
Fixed 45-px circles at 20-px gaps fit most screen widths for 2–6 steps; at 7+ steps the total width exceeds a typical mobile canvas (410+ px). No responsive shrink or wrap behavior.
Behavior
StateiOSAndroidFigma SpecNotes
Completed stepYesYesring=fullStep index < current. Full ring fill in brand blue, number in brand blue.
Current stepYesYesring=partial arcStep index=current. Arc fills current / steps of the ring; number in brand blue.
Upcoming stepYesYesring=track onlyStep index > current. Track-only ring in light blue; number still in brand blue.
Clickable / interactiveOptionalOptionalNot modeledSome wizards let the user tap a completed step to go back. No pressed / focused state exists today; add if interactive behavior is desired.
Vertical orientationMissingMissingNot modeledHorizontal only. Vertical steppers (common on narrow screens or list layouts) would need a second orientation mode or a sibling.
Open Issues
  • Step count is modeled as 9 sibling components instead of a steps prop. Today the family ships as Stepper - Circular - 2 Steps, … - 3 Steps, … … - 10 Steps — 9 top-level components that differ only by hardcoded count. Every other design system exposes one Stepper with steps: Int (or total: Int + current: Int). 9× maintenance, 9× Code Connect mapping, and no path to N=11+ without adding a 10th file. C1 · Layer Structure & Naming
  • Step ring arcs are pre-rendered raster <img> assets, one per step index. Each number=N symbol's ring is a baked PNG — the progressive fill is achieved by image swap, not by math. Blocks theming (can't retint), breaks at high DPI, and ships dozens of assets the native renderer doesn't need. Should be a stroked SVG arc with strokeDasharray (or Circle().trim(from:to:) on iOS, drawArc on Compose). C6 · Asset & Icon Quality
  • Variant axis number encodes position, not a property. The inner symbol uses number=1 | 2 | … | N which both labels the circle (the displayed digit) and implicitly sets the ring-arc raster. Two concerns collapsed into one enum. Should be: index: Int (the number shown) and status: completed | current | upcoming (the visual). C2 · Variant & Property Naming
  • No native primitive is a 1:1 match — this needs a custom component. SwiftUI has no built-in circular stepper; Material 3 shows only a linear Stepper. Both platforms require a custom EBStepperCircular built from a Row/HStack of Circle/Box shapes with arc-drawn progress. Today's raster baking makes this worse — the dev can't reuse the asset. C4 · Native Mappability
  • No pressed / focused / disabled state modeled. Wizards often allow tapping a completed step to return to it. With no interaction states in Figma, the developer has to invent hover / pressed treatment and the designer has no reference. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the family collapses to one component and the raster arcs are replaced with vector strokes. Mapping 9 separate siblings would codify the anti-pattern into the tooling. C7 · Code Connect Linkability
Design Recommendations
  • Collapse all 9 siblings into one Stepper - Circular with steps and current properties. Delete Stepper - Circular - 2 Steps through … - 10 Steps as separate components. Create one Stepper - Circular with steps: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 (enum) and current: 1 | 2 | … | 10 — or, if Figma supports integer ranges, a numeric property. Variant math drops from 9 top-level components × ~10 step symbols=~90 variant assets to 1 component with a runtime-computed arc. Native API: EBStepperCircular(current: Int, total: Int). Family
  • Split the inner symbol's number axis into index (digit shown) and status (ring treatment). Today one enum does both. Separate them: index: Int for the label text and status: completed | current | upcoming for the ring fill. The outer component then computes status per slot from current. Property
  • Replace raster ring arcs with stroked SVG / vector arcs. Each step circle's ring should be a 2-px stroked circle with stroke-dasharray (or two half-circles via trim). Colors bound to main/stepper/color/bg-track (unfilled) and main/stepper/color/bg (filled). Same visual output, theme-able, resolution-independent, no PNG assets. Asset
  • Add a completed visual state with a checkmark icon option. Many wizards replace the digit with a check once a step is done. Today there's no variant for this — everything shows a number. Add showCheckOnComplete: Bool (or model it into the status enum as completed-check). State
  • Add an orientation property for vertical layouts. Long flows (8+ steps) don't fit a phone-width row. Vertical stacks are common for checkout or document review wizards. Add orientation: horizontal | vertical and spec the connector line between circles for both axes. Property
  • Spec a connector line between circles. Today the 9 sibling frames space circles with a 20-px gap but no visible connector. Most steppers draw a line from the trailing edge of one circle to the leading edge of the next, tinted to match completed (brand) vs upcoming (track) states. Add this to the spec — it's the difference between "a row of circles" and "a stepper". Property
  • Rename layers step-containerRing (and the ring arc layer → Arc). "step-container" is a technical label; "Ring" is what a designer or developer searches for. Rename
  • Build as a custom native component, not a ProgressView wrapper. Neither SwiftUI ProgressView nor Material LinearProgressIndicator visually match this pattern. Ship a dedicated EBStepperCircular: iOS uses an HStack of ZStack { Circle().stroke(track); Circle().trim(from:0,to:progress).stroke(fill); Text(index) }; Android uses a Row of Box(Modifier.size(45.dp)) with Canvas drawing drawArc(sweepAngle=progress * 360f). Composition
  • Announce "Step X of Y" to screen readers. The component is decorative by default — assistive tech reads only the number. Wrap in a semantic container that announces "Step \(current) of \(total)" (SwiftUI .accessibilityLabel, Compose Modifier.semantics { contentDescription=… }). A11y
  • Document the canonical composition and retire the sibling names. Update the sticker sheet page to show one Stepper - Circular with property controls; add a migration note pointing Stepper - Circular - N Steps consumers to the new steps prop. Docs
Stepper - Circular canonical node 27:47768 (10 Steps) · sibling frames 27:47819…27:48036

Row of N 45×45 numbered circles, each with a ring that indicates position through the flow. 9 hardcoded sibling frames today; target is one component with a steps prop.

Properties (today)
Top-level componentStepper - Circular - N Steps (×9)
Inner symbolnumber=1 | 2 | … | N
Ring implementationraster <img>
Layouthorizontal only
Properties (proposed)
steps2 | 3 | … | 10
current1 | 2 | … | steps
orientationhorizontal | vertical
status (per slot)completed | current | upcoming
Colors by State
ROLETOKENVALUE
Ring (track / upcoming)main/stepper/color/bg-track#D2E5FF
Ring (fill / current + completed)main/stepper/color/bg#005CE5
Label (digit)main/stepper/color/label#005CE5

All three roles already use main/stepper/color/* tokens — token coverage is clean. The restructure work is schema, not color.

Layout
Step circle size45 × 45
Ring stroke width~2 (raster-baked)
Gap between circles20 (space/space-20)
Outer frame padding20 vertical, 20 horizontal
Total width (N steps)45 × N + 20 × (N−1) + 40
Height85
Typography
DS stylePrimary/Headlines/Block
FontProxima Soft Bold
Size18
Line height23
Tracking0.25
Digits are center-aligned within the 45×45 circle.
Installation Planned API

No native primitive matches the circular stepper pattern — ship a custom EBStepperCircular component on both platforms.

.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBStepperCircular
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
Stepper - Circular - N Steps (×9 siblings)Stepper - Circular (single component)EBStepperCircularEBStepperCircular
(implicit in sibling name)steps: 2…10total: Inttotal: Int
(implicit in number variant)current: 1…stepscurrent: Intcurrent: Int
number=1 | … | N (inner symbol)index: Int + status: completed | current | upcoming(derived internally from current)(derived internally from current)
(horizontal only)orientation: horizontal | verticalorientation: Axis=.horizontalorientation: EBStepperOrientation=Horizontal
(raster arc)Stroked SVG / Canvas arcCircle().trim(from: 0, to: progress).stroke(...)Canvas { drawArc(sweepAngle=progress * 360f) }
Suggested file paths
  • ios/Components/StepperCircular/EBStepperCircular.swift — custom view with HStack of ring + digit
  • android/components/stepper/EBStepperCircular.kt — Row of Canvas-drawn circular step indicators
  • No native primitive is a 1:1 match. A custom component is required on both platforms.
Usage
// KYC wizard — step 2 of 4
EBStepperCircular(current: 2, total: 4)

// Long form wizard — step 5 of 8
EBStepperCircular(current: 5, total: 8)

// Vertical layout for narrow columns
EBStepperCircular(current: 3, total: 6, orientation: .vertical)

// With check-on-complete — digit replaced by checkmark once done
EBStepperCircular(current: 4, total: 5, showCheckOnComplete: true)
// KYC wizard — step 2 of 4
EBStepperCircular(current = 2, total = 4)

// Long form wizard — step 5 of 8
EBStepperCircular(current = 5, total = 8)

// Vertical layout for narrow columns
EBStepperCircular(
    current = 3,
    total = 6,
    orientation = EBStepperOrientation.Vertical
)

// With check-on-complete — digit replaced by checkmark once done
EBStepperCircular(
    current = 4,
    total = 5,
    showCheckOnComplete = true
)
Accessibility
RequirementiOSAndroid
Progress roleWrap the row in .accessibilityElement(children: .ignore) and expose a single label. Set .accessibilityValue("Step \(current) of \(total)").Treat the row as a single semantic node via Modifier.semantics(mergeDescendants=true) with contentDescription="Step $current of $total".
Value announcementVoiceOver reads "Step 2 of 4". Avoid announcing each circle individually — it's noisy and position-less.TalkBack reads the merged label. Update stateDescription when current changes to trigger re-announcement.
Icon-only digitsThe digit itself is already semantic. Ensure contrast: #005CE5 on white=5.3:1 ✓Same. Digit color passes WCAG AA for non-text graphics.
Interactive stepsIf completed steps are tappable, wrap each in a Button with .accessibilityHint("Tap to return to step \(index)"). Pressed state inherits from the button.Use Modifier.clickable(onClickLabel="Return to step $index"). Ripple indication appears natively.
ContrastRing fill #005CE5 on track #D2E5FF=3.1:1 — passes 3:1 for non-text graphics (WCAG 1.4.11). OK.Same ratio.
Usage Guidelines
Do
  • Use for multi-step flows where the user needs to know position and total count (onboarding, KYC, checkout).
  • Place at the top of the screen or immediately under the title bar.
  • Pair with a step-specific heading ("Step 2: Verify your email") so the stepper + title form a progress pair.
  • Switch to vertical orientation when step count × 45 + gaps exceeds the column width.
Don't
  • Don't use for flows with fewer than 3 steps — a simpler "Step 1 of 2" text label is clearer.
  • Don't use for linear progress where total steps are unknown — use Progress Bar (indeterminate) instead.
  • Don't ship the 9 sibling components in product code — consume the parameterized version.
  • Don't let total steps exceed 10 horizontally on mobile — wrap, shrink, or switch to vertical.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkStep count is modeled as 9 sibling components instead of a property. Collapse to a single Stepper - Circular. Also rename step-containerRing.
C2Variant & Property NamingReworkInner number axis conflates digit label and ring-arc rendering. Split into index + status (and lift current / steps to the top-level component).
C3Token CoverageReadyAll colors bound to main/stepper/color/{bg, bg-track, label}. Typography bound to Primary/Headlines/Block. Gap bound to space/space-20.
C4Native MappabilityReworkNo native primitive matches. Requires a custom EBStepperCircular. Today's raster arcs block direct primitive composition.
C5Interaction State CoverageReworkNo pressed / focused / disabled / tappable-completed state modeled. Missing vertical orientation and check-on-complete variants.
C6Asset & Icon QualityReworkRing arcs are raster <img> — one PNG per step index per sibling component. Replace with stroked SVG / Canvas arcs.
C7Code Connect LinkabilityNot MappedBlocked until 9 siblings collapse into one component and raster arcs are replaced with vector strokes.
Variants Inventory (9 sibling components · ~63 total step symbols)

Today: 9 sibling components, one per hardcoded step count. Each sibling contains N step symbols (number=1 … N) with baked raster ring arcs. Sum of step symbols: 2+3+4+5+6+7+8+9+10=54 step symbols (+ 9 outer frames=~63 nodes). Target: 1 component with steps: 2…10 and current: 1…steps as runtime properties; ring arcs drawn by math, not asset.

#Sibling componentNodeFrame widthStep symbols
1Stepper - Circular - 2 Steps27:480361502 (number=1, 2)
2Stepper - Circular - 3 Steps27:480202153
3Stepper - Circular - 4 Steps27:479992804
4Stepper - Circular - 5 Steps27:479733455
5Stepper - Circular - 6 Steps27:479424106
6Stepper - Circular - 7 Steps27:479064757
7Stepper - Circular - 8 Steps27:478655408
8Stepper - Circular - 9 Steps27:478196059
9Stepper - Circular - 10 Steps27:4776867010 (number=1…10)
1.0.0 — April 2026 Initial
Initial Assessment · canonical node 27:47768 (10 Steps) + 8 siblings
Verdict: Restructure — Collapse 9 sibling components (Stepper - Circular - 2…10 Steps) into one Stepper - Circular with steps: Int and current: Int properties. Replace raster ring arcs with stroked SVG / Canvas. Open
Schema
C1 — Family structure — 9 top-level components differ only by hardcoded step count. Collapse into a single component with a steps property. Rename step-container layer → Ring. Open
C1
C2 — Property shape — Inner number=1…N conflates digit label and arc-fill rendering. Split into index (label) + status (ring treatment). Lift current and steps to the parent component. Open
C2
C4 — Native mapping — No native primitive matches. Requires custom EBStepperCircular on both platforms built over HStack/Row of stroked circles. Open
C4
C5 — Missing states — Add pressed / focused for interactive steps, vertical orientation, and check-on-complete option. Open
C5
C6 — Raster arcs — Each step's ring is a pre-baked PNG. Replace with stroked SVG circles bound to main/stepper/color/bg-track and main/stepper/color/bg. Open
C6
C7 — Code Connect — Mappings pending restructure. Mapping 9 separate siblings would codify the anti-pattern. Open
C7
Stepper - Dash RestructureReworkComponent link

A flat, segmented progress indicator — a horizontal row of equal-width rounded dashes where the current-and-earlier steps fill brand blue and the remaining steps render in the light-blue track color. Ships as one component set with highlighted=1st | 2nd | … | 10th (10 variants) and ten boolean propNStepper props used to hide/show each slot. All dashes are real vector rectangles bound to the main/stepper/color/* tokens — no raster assets — which puts Dash well ahead of the Circular and Bullet siblings on asset quality.

Restructure — collapse the boolean slot flags and ordinal enum into current: Int + total: Int
Visually the component is already in great shape — vector dashes, token-bound fills, clean rounded geometry. The structural problem is the schema: highlighted is an ordinal enum (1st…10th) when it should be a number, and the 10 propNStepper booleans emulate what should be a single total scalar. Rebuild as one variant with current: 1…10 and total: 2…10, and fix the duplicate 6th layer name that covers positions 7–10. Native side stays a custom component (no platform primitive renders "N equal-width dashes" out of the box).
In Context

Stepper - Dash sits above multi-step flows to tell the user where they are — KYC wizards, onboarding carousels, checkout funnels. The dashed format reads as "position in a sequence" rather than "percent done".

Live Preview

Adjust total to set the number of dashes and current to set which step is active. The proposed API collapses today's 10 propNStepper booleans and the highlighted=1st…10th enum into two integer properties.

Properties (proposed)
total
current
Figma (today)
highlighted2nd
visible slotsprop1–prop4=true
DS Health
Reusable
Partial
Applies to any 2–10 step linear flow. Hard-capped at 10 (the ordinal enum only goes up to 10th), so longer flows need a different component.
Self-contained
Pass
Pure vector — rounded rectangles bound to main/stepper/color/bg and main/stepper/color/bg-track. No raster assets, no external dependencies.
Consistent
Warn
Ordinal enum (1st…10th) where a number belongs; 10 boolean visibility flags where a scalar total belongs; layer names duplicate 6th across positions 7–10.
Composable
Partial
Frame is 268 px wide with fill-container dashes, so it stretches inside a parent. Family-wise, Dash, Bullet, and Circular each ship independently; a unified EBStepper(style:) API would compose better.
Behavior
StateiOSAndroidFigma SpecNotes
Default (current)YesYeshighlighted=1st…10thDashes 1…current fill #005CE5; dashes current+1…total fill #D2E5FF.
CompletedMissingMissingNot modeledNo distinct "all done" state. Once current===total the final dash just reuses the same brand blue — consider a positive-green variant when the whole flow finishes successfully.
ErrorMissingMissingNot modeledFlows that can fail mid-way (KYC rejected, upload failed) have no way to indicate which step errored. Add a per-dash status or a component-level error state.
Tappable stepN/AN/ANot modeledDisplay-only today — no pressed or focused variant. If designers want to allow tapping a completed dash to return, that interaction needs spec'ing.
Open Issues
  • Total step count encoded as 10 boolean props instead of an integer. Ships prop1Stepper…prop10Steppers — ten toggles used as visibility flags for "how many dashes to render". To spec a 4-dash stepper a consumer flips four toggles on and six off; to change to 5 dashes they flip one more. Should be a single total: Int (or total: 2 | 3 | … | 10 enum). C2 · Variant & Property Naming
  • highlighted is ordinal, not numeric.highlighted=1st | 2nd | … | 10th reads as a label, not a position. Native APIs and product code want an integer they can feed a current / total calculation — rename to current and switch to a numeric range. C2 · Variant & Property Naming
  • Duplicate 6th layer name across positions 7–10. In the highlighted=1st variant, dash layers are labelled 1st, 2nd, 3rd, 4th, 5th, 6th, 6th, 6th, 6th, 6th. Looks like a copy-paste oversight when the component was extended from 6 to 10 slots. Rename to 7th10th. C1 · Layer Structure & Naming
  • No native primitive matches. Neither SwiftUI nor Material 3 ships a "segmented dash" progress indicator. Implementation is a custom HStack/Row of rounded rectangles — manageable, but the DS must own the composable and its theming. C4 · Native Mappability
  • No completed, error, or interactive state modeled. Only the default two-tone (brand / track) rendering exists. Add variants for completed-successfully (green), error-at-step-N (red), and optionally pressed/focused if steps are tappable. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the ordinal enum and boolean visibility props collapse into current and total. Mapping today's schema would codify the anti-pattern. C7 · Code Connect Linkability
Design Recommendations
  • Replace the 10 propNStepper booleans with a single total property. Today a designer builds "4-dash stepper" by toggling prop1Stepper=true, prop2=true, prop3=true, prop4=true, prop5..10=false. Replace with total: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10. Variant math drops from (10 ordinal × 2^10 boolean combos, nominally) to 9 × 10=90, and more importantly the schema becomes legible — "4 dashes, step 2 active" instead of "highlighted=2nd + 4 booleans on, 6 booleans off". Property
  • Rename highlighted=1st…10th to current: 1…10. The property name reads as a Boolean ("is highlighted?") and the values read as rank labels. Both are wrong — the value is a numeric index. Rename to current and switch values from ordinal strings to integers so consumers can do current / total math. Rename
  • Fix duplicated 6th layer names on slots 7–10. Rename the last four inner dash layers to 7th, 8th, 9th, 10th so the inspector and dev handoff read cleanly. Low-effort cleanup. Rename
  • Add status states for completed, error, and loading. Introduce an optional component-level state: default | success | error that repaints the whole row (green when the flow finishes, red when the current step errors). Indeterminate / loading is optional — a subtle pulsing animation on the current dash while an async step resolves. Tokens: main/stepper/color/bg-positive, main/stepper/color/bg-negative. State
  • Unify Stepper - Dash, Stepper - Bullet, and Stepper - Circular into one EBStepper API. All three render the same underlying data (current, total) and differ only in the visual treatment of each slot. On the native side, ship one EBStepper(current:total:style:) with style: .dash | .bullet | .circular instead of three separate components — the state, accessibility, and layout logic are identical across styles. Figma can keep three sibling records for the sticker sheet, but the API is one surface. Family
  • Document a canonical "dash height × total" sizing chart. The single frame is 268 px wide with 4-px dash height and 4-px gap. As total grows the individual dash width shrinks — at total=10 each dash is ~22 px. Document the minimum sensible per-dash width and the component's fill behavior inside narrower parents. Docs
  • Announce "Step X of Y" to screen readers. Dashes are purely decorative — assistive tech sees nothing today. Wrap in a container with accessibilityLabel="Step \(current) of \(total)" on iOS and Modifier.semantics { contentDescription=… } on Android. A11y
  • Spec a vertical orientation for long flows. With 10 dashes in a narrow column, individual dashes get uncomfortably short. Add orientation: horizontal | vertical so flows can stack top-to-bottom when horizontal width is constrained. Property
Stepper - Dash component set 18649:5223 · variants 18649:5224…18649:5323

Horizontal row of equal-width rounded dashes. Dashes 1…current render in brand blue (bg); dashes current+1total render in track blue (bg-track). All fills are real vector rectangles bound to stepper tokens.

Properties (today)
highlighted1st | 2nd | … | 10th
prop1Stepper…prop10Steppersboolean × 10
Dash implementationvector rect (rounded)
Frame width268 (inner) · 308 (outer)
Properties (proposed)
current1 | 2 | … | 10 (Int)
total2 | 3 | … | 10 (Int)
statusdefault | success | error
orientationhorizontal | vertical
Colors by State
ROLETOKENVALUE
Dash (current & earlier)main/stepper/color/bg#005CE5
Dash (later)main/stepper/color/bg-track#D2E5FF
Success (status=success) — proposedmain/stepper/color/bg-positive#12AF80
Error (status=error) — proposedmain/stepper/color/bg-negative#D81E1E
Layout
Outer frame308 × 260 (canvas)
Row width268 (fill-container)
Dash height4 (space/space-4)
Gap between dashes4 (space/space-4)
Row padding0 (space/space-0)
Corner radius100 (pill)
Dash widthflex-1 (equal share of row)
Typography
No text
Label pairingexternal
Stepper - Dash is a graphic-only primitive; any "Step X of Y" label is the consumer's responsibility and should live in the accessible name.
Installation Planned API

No native primitive matches. EBStepperDash is a small custom composable built from a row of rounded rectangles.

.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBStepperDash
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
highlighted: 1st | … | 10thcurrent: Int (1…10)current: Intcurrent: Int
prop1Stepper…prop10Steppers (Bool × 10)total: Int (2…10)total: Inttotal: Int
(not modeled)status: default | success | errorstatus: EBStepperStatusstatus: EBStepperStatus
(not modeled)orientation: horizontal | verticalaxis: Axisorientation: Orientation
bg tokenunchangedEBColors.stepperBgEBColors.stepperBg
bg-track tokenunchangedEBColors.stepperTrackEBColors.stepperTrack
Suggested file paths
  • ios/Components/Stepper/EBStepperDash.swiftHStack of Capsule fills
  • android/components/stepper/EBStepperDash.ktRow of Box(Modifier.weight(1f).height(4.dp).clip(RoundedCornerShape(100)).background(…))
  • Unified API option: ship EBStepper(current:total:style:) with style: .dash | .bullet | .circular — see the Family recommendation. Individual EBStepperDash becomes a thin alias.
Usage
// Step 2 of 4
EBStepperDash(current: 2, total: 4)

// Unified API — dash style of the shared stepper
EBStepper(current: 2, total: 4, style: .dash)

// Success — flow completed
EBStepperDash(current: 4, total: 4, status: .success)

// Error — step 3 failed
EBStepperDash(current: 3, total: 5, status: .error)

// Accessible wrapper
EBStepperDash(current: 2, total: 4)
    .accessibilityElement()
    .accessibilityLabel("Step 2 of 4")
// Step 2 of 4
EBStepperDash(current = 2, total = 4)

// Unified API — dash style of the shared stepper
EBStepper(current = 2, total = 4, style = EBStepperStyle.Dash)

// Success — flow completed
EBStepperDash(current = 4, total = 4, status = EBStepperStatus.Success)

// Error — step 3 failed
EBStepperDash(current = 3, total = 5, status = EBStepperStatus.Error)

// Accessible wrapper
EBStepperDash(
    current = 2,
    total = 4,
    modifier = Modifier.semantics { contentDescription = "Step 2 of 4" }
)
Accessibility
RequirementiOSAndroid
Progress roleExpose as a single .accessibilityElement() with the .progressbar trait, value Double(current) / Double(total), label "Step 2 of 4".Wrap in Modifier.semantics { role=Role.ProgressBar; progressBarRangeInfo=ProgressBarRangeInfo(current.toFloat(), 0f..total.toFloat()) }.
Label announcementVoiceOver should read "Step 2 of 4, 50%". Localize the substitutions.TalkBack same — use stateDescription for the "Step X of Y" phrasing.
Non-decorative colorsBrand blue on track blue is a 3.1:1 non-text contrast ratio — passes WCAG 1.4.11. OK.Same ratio.
GroupingUse .accessibilityElement(children: .ignore) so VoiceOver hears one unified announcement, not N dash descriptions.Use Modifier.semantics(mergeDescendants=true).
Dynamic Type / text scaleStepper has no text, so Dynamic Type doesn't apply to the component itself. Labels paired above/below should scale.Same — font scale applies to paired labels, not dashes.
Usage Guidelines
Do
  • Use Dash when the number of steps is small, fixed, and known up front (KYC wizard, onboarding carousel).
  • Pair with a label above the dashes — "Step 2 of 4 · Personal details".
  • Let the component fill the column width so each dash reads as a proportional segment.
  • Keep total between 2 and 10 — beyond that, dashes become visually too small to distinguish.
Don't
  • Don't use Dash for continuous progress (percent complete of a single task) — use Progress Bar.
  • Don't use Dash when steps have labels or numbers — use Stepper - Circular so each step is individually identifiable.
  • Don't mix Dash and Bullet inside the same flow — pick one style per product area.
  • Don't animate the fill from dash to dash on every mount — only animate on real step transitions.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingNeeds RefinementInner dash layers on slots 7–10 are all named 6th (copy-paste bug). Otherwise clean.
C2Variant & Property NamingReworkhighlighted=1st…10th should be numeric current; 10 propNStepper booleans should collapse to total: Int.
C3Token CoverageReadyAll fills bound to main/stepper/color/bg and main/stepper/color/bg-track. Gap uses space/space-4. Add bg-positive / bg-negative when status states land.
C4Native MappabilityReworkNo native primitive. Custom HStack/Row of rounded rectangles. Small composable, but must be owned by the DS.
C5Interaction State CoverageReworkOnly default two-tone. Missing completed/success, error, and (optional) tappable-step pressed/focused.
C6Asset & Icon QualityReadyPure vector rectangles — no raster assets. Resolution-independent, theme-able. Best-in-class for the Stepper family.
C7Code Connect LinkabilityNot MappedBlocked until the ordinal enum and boolean slot flags collapse into current + total.
Variants Inventory (10 total)

A single highlighted axis with 10 ordinal values (1st, 2nd, …, 10th)=10 variants. Each variant additionally exposes 10 boolean propNStepper visibility flags (not counted in the variant total — they're instance props). The proposed architecture replaces all of them with current: Int + total: Int, producing 9 × 10=90 representable states with no boolean juggling.

#NodehighlightedDimensionsNotes
118649:52241st268 × 4Dash 1 bg; 2–10 bg-track
218649:52352nd268 × 4Dashes 1–2 bg; 3–10 bg-track
318649:52463rd268 × 4Dashes 1–3 bg; 4–10 bg-track
418649:52574th268 × 4Dashes 1–4 bg; 5–10 bg-track
518649:52685th268 × 4Dashes 1–5 bg; 6–10 bg-track
618649:52796th268 × 4Dashes 1–6 bg; 7–10 bg-track
718649:52907th268 × 4Dashes 1–7 bg; 8–10 bg-track · inner layer mislabel 6th
818649:53018th268 × 4Dashes 1–8 bg; 9–10 bg-track · inner layer mislabel 6th
918649:53129th268 × 4Dashes 1–9 bg; 10 bg-track · inner layer mislabel 6th
1018649:532310th268 × 4All 10 dashes bg · inner layer mislabel 6th
1.0.0 — April 2026 Initial
Initial Assessment · node 18649:5223
Verdict: Restructure — Collapse highlighted=1st…10th + 10 propNStepper booleans into current: Int + total: Int. Asset quality already clean (pure vector). Open
Schema
C1 — Layer naming — Dash layers at positions 7–10 are all labelled 6th (copy-paste bug). Rename to 7th10th. Open
C1
C2 — Ordinal enum + boolean flagshighlighted is ordinal (1st…10th) where a number belongs; 10 propNStepper booleans emulate what should be total: Int. Rename and collapse. Open
C2
C4 — Native mapping — No platform primitive matches. Requires custom EBStepperDash on both platforms. Consider unified EBStepper(style:) shared with Bullet and Circular. Open
C4
C5 — Missing states — No completed-success, error, or tappable-step variants. Add status: default | success | error and optional pressed/focused. Open
C5
C6 — Asset quality — Pure vector rectangles bound to main/stepper/color/* tokens. No raster. Ready. Ready
C6
C7 — Code Connect — Blocked until the schema collapses to current + total. Open
C7
Subtext Message RestructureRequires ReworkComponent link

Helper / success / error message shown below form fields. 6 variants across Variant (Primary/Success/Error) × Size (Base/Small). Icon appears only for Success and Error. Currently a standalone DS primitive — composed in by consumers beneath Input Field, Labeled Field, Select Field, Recipient Field, Dropdown, etc.

Restructure before native handoff
Asymmetric anatomy (Primary has no icon, Success/Error hardcode icons), misnamed leadingLabel boolean, generic shape_full icon layers, no disabled state. Decide: keep as standalone primitive or fold into form-field supportingText slot.
In Context

Appears directly beneath form fields — Input, Labeled, Select, Recipient, Dropdown — to communicate helper hints, success confirmation, or validation errors.

Live Preview

Toggle variant, size, leading label, and trailing icon to see the subtext update in real time.

Properties
Variant
Size
leadingLabel
trailingIcon
DS Health
Reusable
Partial
Works under any form field. But it's only ever rendered beneath a field — in practice it's a field-composition concern, not a standalone primitive. No disabled variant to mirror field disabled state.
Self-contained
Partial
Carries own type, color, spacing, and icon per variant. However, Success / Error icons are drawn shapes named shape_full — not instance-swapped from the DS Icon library.
Consistent
Warn
Anatomy diverges by variant — Primary has no icon slot, Success / Error hardcode icons. Boolean leadingLabel is misnamed (the Label text renders trailing in the flex row).
Composable
Partial
Instance-placed under form fields today. Native equivalents (SwiftUI convention, Compose supportingText) treat this as a field slot, not a peer component — suggesting it should be folded in.
Behavior
VariantiOSAndroidFigma PropertyNotes
Primary (helper)YesYesVariant=PrimaryNeutral weaker text #6780A9. No icon.
SuccessYesYesVariant=SuccessGreen text #048570, filled check icon #12AF80.
ErrorYesYesVariant=ErrorRed text + icon #D61B2C.
DisabledNo disabled variant today. When parent field is disabled, there's no matched subtext state.
Open Issues
  • Anatomy diverges by variant. Primary has no icon, Success / Error hardcode specific icons (Checkmark Circular / Close). The "leading icon" is not a uniform slot — it's an if-branch on Variant. Consumers can't override the Success / Error icon without detaching. C4 · Native Mappability
  • leadingLabel boolean is misnamed. The "Label" text actually renders on the trailing side of the flex row (after the message content), not leading. Naming contradicts rendered position and will mislead SwiftUI / Compose param names downstream. C2 · Variant & Property Naming
  • Icon layer named shape_full. The inner 12×12 shape inside the Checkmark / Close containers carries a generic, flattened-style name. Suggests a raster image fill or BOOLEAN_OPERATION rather than a proper vector Icon instance swappable from the DS Icon library. C6 · Asset & Icon Quality
  • Container layers named generically. Nested frames labeled container and content don't describe role — Code Connect slot inference relies on semantic names like #leading-icon, #message, #label. C1 · Layer Structure & Naming
  • No Disabled variant. When the parent field is disabled, the subtext has no paired state — consumers either hide it or rely on manual opacity adjustments. Every sibling form field carries a Disabled state; the subtext should too. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked behind C1 / C2 / C4 / C6. Also depends on the family-level decision below — whether this stays a standalone component or folds into the form-field supportingText slot. C7 · Code Connect Linkability
Design Recommendations
  • Fold Subtext Message into each form field as a supportingText slot. This is the native convention on both platforms — Material 3 TextField exposes supportingText, and SwiftUI pairs a Text under the field using the same validation state. Folding it in removes the C4 / C5 gaps (field already has Disabled + Error states) and lets consumers drive the message by passing supportingText: String? + deriving color from isError. The standalone component can stay as an annotation helper for designers but is no longer the canonical consumer integration path. Family
  • Rename leadingLabeltrailingLabel. The "Label" text renders at the end of the flex row — the property name must match rendered position so SwiftUI / Compose params don't surprise developers. If the intent is to actually make the Label leading, swap the layer order in Figma and keep the current name. Rename
  • Normalize anatomy with a real leading-icon slot across all variants. Rebuild so every variant shares the same structure: optional #leading-icon (Icon instance) → #message (text) → optional #trailing-label (Label text). Primary keeps icon=off by default; Success / Error default icon=on. This lets one Variant enum + one boolean icon slot + one boolean label slot cover all six variants uniformly. Property
  • Replace shape_full with DS Icon library instances. Swap the Success checkmark and Error close for Icon=Checkmark Circular and Icon=Close instance-swaps bound to main/subtext-message/*/icon tokens. Flattened boolean shapes can't be retinted, recolored, or resized cleanly and they block Code Connect 1:1 icon param mapping. Asset
  • Add a Disabled variant. Match the 4-state contract of every sibling form field (Default / Active / Error / Disabled). When the parent field is disabled, the subtext needs a paired muted color token (e.g. main/subtext-message/disabled/label). State
  • Rename container layers.container#leading-icon-slot, content#content, inner shape → #icon-glyph. Semantic names drive Code Connect slot inference. Rename
Variants

6 variants across 2 axes: Variant (Primary / Success / Error) × Size (Base / Small). Base uses 12/18 caption type, Small uses 10/15 small caption.

Primary (helper)

Neutral helper text. No icon. Used for hints, formatting examples, or ambient guidance under a field.

Properties
VariantPrimary
Size
Success

Valid input confirmation. Green text with filled circular checkmark.

Properties
VariantSuccess
Size
Error

Validation error. Red text with filled circular close icon.

Properties
VariantError
Size
Colors by Variant

Each variant binds its own label (and icon, where applicable) token. No appearance modes. No disabled state.

VARIANTROLETOKENVALUE
Primarylabelmain/subtext-message/primary/label#6780A9
Successlabelmain/subtext-message/success/label#048570
Successiconmain/subtext-message/success/icon#12AF80
Errorlabelmain/subtext-message/error/label#D61B2C
Erroriconmain/subtext-message/error/icon#D61B2C
Disabled— (missing)
Typography by Size

Both sizes use the Secondary (BarkAda Semibold) type scale.

SIZETEXT STYLEFONTSIZELINE HEIGHTWEIGHT
BaseSecondary/Bold/CaptionBarkAda12 px18 pxSemibold (600)
SmallSecondary/Bold/Small CaptionBarkAda10 px15 pxSemibold (600)
Layout
PROPERTYVALUETOKEN
Padding left2 pxspace/space-2
Padding top4 pxspace/space-4
Gap (icon ↔ content)4 pxspace/space-4
Icon frame16 × 16 px
Icon glyph12 × 12 px
Installation Planned API

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.

Property Mapping

Assumes the recommended architecture: supportingText slot on each form field (preferred), with this standalone component as a secondary annotation helper.

Figma PropertySwiftUI ParamCompose ParamNotes
Variant=Primary.ebSubtextStyle(.primary)style=EBSubtextStyle.PrimaryNeutral helper text. When used via field supportingText, this is the default style.
Variant=Success.ebSubtextStyle(.success)style=EBSubtextStyle.SuccessGreen with checkmark icon.
Variant=Error.ebSubtextStyle(.error)style=EBSubtextStyle.ErrorRed with close icon. Field-side: derived from isError.
Size=Base / Small.controlSize(.regular / .small)size=EBSubtextSize.Base / SmallTypography scale only.
leadingLabel (Yes/No)trailingLabel: String?trailingLabel: String?=nullNeeds rename — property is misnamed (renders trailing, not leading).
trailingIcon (Yes/No)leadingIcon: Image?leadingIcon: @Composable (() -> Unit)?Slot is actually leading. Default icon per variant, or pass custom.
SwiftUI
ios/Components/FormElements/EBSubtextMessage.swift
Jetpack Compose
android/components/form/EBSubtextMessage.kt
Usage Snippets Planned API
Preferred: via form-field supportingText
EBInputField("Email", text: $email)
    .ebError(!isValid)
    .ebSupportingText("Enter a valid email address")
EBInputField(
    value = email,
    onValueChange = { email = it },
    placeholder = "Email",
    isError = !isValid,
    supportingText = { Text("Enter a valid email address") }
)
Standalone primitive
EBSubtextMessage("Valid message content")
    .ebSubtextStyle(.success)
    .controlSize(.small)
EBSubtextMessage(
    text = "Valid message content",
    style = EBSubtextStyle.Success,
    size = EBSubtextSize.Small
)
Accessibility
RequirementiOSAndroid
Error announcementWire to field .accessibilityValue so VoiceOver reads the error with the field value.Use semantics { error(msg) } on the field, not a standalone live region.
Icon is decorativeMark the leading icon .accessibilityHidden(true) — the text carries the meaning.Icon contentDescription=null; semantics go on the text.
Dynamic Type / font scalingCaption type must scale with Dynamic Type. Don't hard-lock font size.Use sp units and respect fontScale.
Color-only meaningPair red with the close icon so red isn't the sole error cue.Same — both a color and an icon are required for error/success.
Usage Guidelines

Do

Pass the message via the parent field's supportingText slot. This keeps validation state and message colocated.

Don't

Render this beneath a field as a separate sibling component — the parent field can't coordinate disabled / error state with an external peer.

Do

Keep error messages specific and actionable ("Enter 11 digits, starting with 09"). Use Primary for ambient hints, Success for confirmation.

Don't

Use Success as a decorative "looks good!" under every valid field — reserve it for meaningful post-validation confirmation.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkGeneric container, content, shape_full layers. No semantic slot names.
C2Variant & Property NamingReworkleadingLabel renders trailing — name contradicts position. Booleans already true/false (good).
C3Token CoverageReadyDedicated main/subtext-message/* tokens for label + icon. Spacing uses space/*.
C4Native MappabilityReworkAnatomy diverges by variant. Natively this is a field slot (supportingText), not a peer component.
C5Interaction State CoverageReworkNo Disabled variant. Sibling fields have 4 states; subtext has 3 variants.
C6Asset & Icon QualityReworkIcons are shape_full layers — likely flattened / boolean shapes, not vector Icon instances.
C7Code Connect LinkabilityNot MappedBlocked until family decision + C1 / C2 / C4 / C6 resolved.
Code Connect
AspectStatusNotes
Property namingReworkleadingLabel must be renamed to match rendered position
Slot inferenceReworkGeneric layer names block slot detection
State coverageReworkMissing Disabled variant
Native component fileNot MappedDepends on family decision: standalone EBSubtextMessage or field supportingText slot
Variants Inventory (6 total)

3 Variant values × 2 Size values. The leadingLabel and trailingIcon booleans are not part of the 6 — they toggle at the instance level.

VariantSizeNode ID
PrimaryBase11855:8764
PrimarySmall11855:8767
SuccessBase11855:8770
SuccessSmall11855:8776
ErrorBase11855:8782
ErrorSmall11855:8788
1.0.0 — April 2026 Initial
Initial Assessment · node 18687:71133
Component assessed — 6 variants documented (3 Variant × 2 Size). Primary / Success / Error with Base and Small sizes. Used as helper / validation message beneath Form Elements. Documented
Initial
Anatomy diverges by variant — Primary has no icon slot; Success / Error hardcode specific icons. Not a uniform slot contract. Open
C4 Open
leadingLabel misnamed — The "Label" text renders on the trailing side of the flex row. Property name contradicts rendered position. Open
C2 Open
Icon layer named shape_full — Inner 12×12 glyph carries a generic, flattened-style name. Suggests raster fill or boolean op rather than a proper vector Icon instance. Open
C6 Open
Container layers named genericallycontainer / content don't describe role. Open
C1 Open
No Disabled variant — Sibling form fields all carry Disabled. Subtext doesn't. Open
C5 Open
Code Connect mappings — Not registered. Blocked by family decision (fold into field supportingText slot vs keep standalone) + C1 / C2 / C4 / C6. Open
C7 Open
Tab Item FixNeeds RefinementComponent link

Atom composed by the Tabs container. 8 variants across isActive? (Yes/No) × orientation (vertical/horizontal) × size (small/large). Horizontal variants also expose hasLeadingIcon, hasCounter, and hasRedDot booleans — vertical variants always render an icon.

Prop and asset cleanup needed
Rename isActive?selected (true/false). Unify the leading-icon slot across orientations. Replace the hardcoded counter (and its raw hex colors) with an instance of the canonical Badge. Replace the placeholder circle with a swappable Icon slot. C2C3C6
In Context

Tab Items appear inside the Tabs container. See the Tabs in-context preview for the full screen layout.

Live Preview

Toggle state, orientation, size, and slots to see every combination.

Properties
isActive?
orientation
size
hasLeadingIcon
hasCounter
hasRedDot
DS Health
Reusable
Pass
Used by Tabs for all tab surfaces. Four orientation/size combinations cover 360px and 414px screens.
Self-contained
Warn
Carries layout, border, typography. But the counter pill uses hardcoded colors (#ECF1FA, #0F3390) instead of tokens. C3
Consistent
Warn
Property isActive? has a trailing ? and uses Yes/No. The leading-icon slot behaves differently across orientations: vertical always renders one, horizontal exposes hasLeadingIcon boolean. C2
Composable
Warn
Counter pill is drawn locally rather than instancing the canonical Badge component — changes to Badge won't propagate. Icon is a hardcoded gray circle icon-placeholder instead of a swappable Icon slot. C6
Behavior
StateiOSAndroidFigma PropertyNotes
ActiveYesYesisActive?=YesBlue label, blue bottom border
InactiveYesYesisActive?=NoGray label, light gray bottom border
Pressed / DisabledN/AN/ANot defined. C5
Has counterYesYeshasCounter=true (horizontal only)Counter pill. Should instance Badge, not duplicate colors. C6
Has red dotYesYeshasRedDot=true6px red dot in top-right corner
Open Issues
  • Property isActive? has a ? in its name. And uses Yes/No values. Rename to selected with true/false to match Checkbox/Radio conventions. C2 · Variant & Property Naming
  • Leading-icon slot is inconsistent across orientations. Vertical always renders an icon; horizontal gates it on hasLeadingIcon. Unify to a single leading=none | icon prop that behaves identically in both orientations. C2 · Variant & Property Naming
  • Counter pill uses hardcoded hex values.#ECF1FA bg, #0F3390 label — not bound to tokens, so theme changes won't propagate. C3 · Token Coverage
  • Counter is drawn locally instead of instancing Badge. Breaks compositional inheritance — future Badge updates (color, sizing, overflow) won't reach Tab Item. C6 · Asset & Icon Quality
  • Icon is a hardcoded placeholder circle. 32px (vertical) / 24px (horizontal) gray icon-placeholder — should be a swappable Icon slot via instance swap. C6 · Asset & Icon Quality
  • No pressed / disabled states documented. Tap feedback and non-interactive state are critical for a tab atom — engineers are improvising them today. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until property rename, slot adoption, and state coverage land. C7 · Code Connect Linkability
Design Recommendations
  • Rename isActive?selected with true/false values. Matches Swift Bool / Kotlin Boolean for Code Connect. Rename
  • Unify the leading-icon slot — replace the always-on vertical icon and the hasLeadingIcon boolean with a single leading=none/icon slot that behaves identically across orientations. Property
  • Replace the local counter pill with a Badge instance. Badge already ships tokenized colors and will inherit any future token changes. Same pattern used when Avatar Group was repointed to the canonical Avatar. Composition
  • Replace icon-placeholder with a swappable Icon slot (instance-swap). Lets product teams drop in any icon without editing the master. Slot
  • Add pressed and disabled states as explicit variants so engineers don't have to improvise tap feedback. State
  • Add tokens for the counter under main/tab/counter/{bg,label} if keeping it standalone, or adopt Badge's existing tokens. Token
Variants

8 variants grouped by orientation (vertical/horizontal) × size (small/large) × isActive (Yes/No). Horizontal variants expose optional slots; vertical always renders an icon.

Vertical · small — 360px screen

Icon above label. 32px icon, 16/16 label (Primary/Label/Base). Active + inactive shown side-by-side.

Vertical · large — 414px screen

Icon above label. 32px icon, 18/18 label (Primary/Label/Large). Optimized for 414px screens.

Horizontal · small — label only + optional slots

Label-first row. Optional leading icon (24px) and trailing counter (18px pill). Red dot anchored to top-right.

Horizontal · large

Same anatomy as horizontal small but with 18/18 label and 112px cell width.

Colors by State

Two states: active and inactive. Counter colors are currently hardcoded (flagged as C3).

RoleTokenActiveInactive
Labelmain/tab/color/{active|inactive}/label#005CE5#6780A9
Bottom bordermain/tab/color/{active|inactive}/border#005CE5#E5EBF4
Counter bg— (hardcoded)#ECF1FA#ECF1FA
Counter label— (hardcoded)#0F3390#0F3390
Red dot— (raster)Red 6×6px indicator

Counter colors should be tokenized as main/tab/counter/{bg,label} or replaced by instancing the canonical Badge component.

Layout
PropertyValue
Vertical padding (top)12px
Vertical padding (bottom)16px
Horizontal padding (left/right)12px
Icon size (vertical)32 × 32
Icon size (horizontal leading)24 × 24
Icon → label gap (vertical)12px
Counter size18px h × padding 6h / 4v
Counter radius99999px (pill)
Red dot size6 × 6 (top-right)
Bottom border2px solid
Horizontal small width105px
Horizontal large width112px
Typography
SizeDS text styleSpec
SmallPrimary/Label/BaseHeyMeow Rnd Bold · 16 / 16 · +0.25
LargePrimary/Label/LargeHeyMeow Rnd Bold · 18 / 18 · +0.25
Counter— (hardcoded)HeyMeow Rnd Bold · 12 / 12 · +0.5
Installation Planned API

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:tabs:1.0.0")
}
Property Mapping (proposed — after cleanup)
Current FigmaProposed APISwiftUICompose
isActive?=Yes/Noselected: Boolselected: Boolselected: Boolean
orientation=vertical/horizontalorientation: EBTabOrientation.orientation(.vertical)orientation=Vertical
size=small/largesize: EBTabSize.size(.small)size=Small
hasLeadingIcon + vertical always-on iconleading: Icon?leading: Image?leading: Painter?
hasCounter=true/falsecounter: Int?counter: Int?counter: Int?
hasRedDot=true/falseshowBadge: BoolshowBadge: BoolshowBadge: Boolean
label: Stringtitle: Stringlabel: String
SwiftUI
ios/Components/Tabs/EBTabItem.swift
Jetpack Compose
android/components/tabs/EBTabItem.kt
Usage Snippets Planned API
// Vertical tab with icon
EBTabItem(label: "Overview", leading: Image("overview"))
    .selected(true)

// Horizontal tab with counter
EBTabItem(label: "Inbox", counter: 12)
    .orientation(.horizontal)
    .selected(false)

// Horizontal tab with red-dot indicator
EBTabItem(label: "Alerts", showBadge: true)
    .orientation(.horizontal)
// Vertical tab with icon
EBTabItem(
    label = "Overview",
    leading = painterResource(R.drawable.overview),
    selected = true
)

// Horizontal tab with counter
EBTabItem(
    label = "Inbox",
    orientation = EBTabOrientation.Horizontal,
    counter = 12,
    selected = false
)

// Horizontal tab with red-dot indicator
EBTabItem(
    label = "Alerts",
    orientation = EBTabOrientation.Horizontal,
    showBadge = true
)
Accessibility
RequirementiOSAndroid
Selected state.accessibilityAddTraits(.isSelected)selected=true in semantics
Counter value.accessibilityValue("\(count) unread")contentDescription includes the count
Red dot.accessibilityLabel("New") or append to labelAppend to contentDescription
Tap targetIcon-only cells need 44pt hit areaCells meet 48dp with padding
Usage Guidelines

Do

Use vertical orientation when tabs have icons + short labels (iconic nav). Use horizontal when labels are longer or icons aren't semantically needed.

Don't

Mix orientations within the same Tabs container.

Do

Use counter for numeric indicators (unread counts, items). Use showBadge for "new / unseen" indicators where the count is unimportant.

Don't

Stack counter + red-dot on the same tab — the counter already communicates "there's something here."

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic: container, icon-label, icon, label, counter-container, red-dot.
C2Variant & Property NamingNeeds FixisActive? Yes/No + inconsistent leading-icon slot across orientations.
C3Token CoverageNeeds FixCounter bg #ECF1FA and label #0F3390 are hardcoded.
C4Native MappabilityReadyCustom cell on iOS, Material Tab content on Android.
C5Interaction State CoverageNeeds FixPressed and disabled states not defined.
C6Asset & Icon QualityNeeds FixPlaceholder circle instead of Icon slot; counter is duplicated, not a Badge instance.
C7Code Connect LinkabilityPendingNot mapped.
Variants Inventory (8 total)
isActive?orientationsizeNode ID
Yesverticalsmall18482:33263
Noverticalsmall18482:33270
Yesverticallarge18482:33277
Noverticallarge18482:33284
Yeshorizontalsmall18482:33291
Nohorizontalsmall18482:33300
Yeshorizontallarge18482:33309
Nohorizontallarge18482:33318
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:33262
Component assessed — 8 variants across isActive × orientation × size. Horizontal variants expose optional leading icon, counter, and red-dot slots. Documented
Initial
Property naming issuesisActive? has a ? and uses Yes/No. Leading-icon slot behaves differently across orientations. Open
C2 Open
Counter colors hardcoded#ECF1FA bg and #0F3390 label are raw hex. Should use tokens or instance the Badge component. Open
C3 Open
No pressed/disabled states — State coverage limited to active/inactive. Open
C5 Open
Icon placeholder + duplicated counter — Icon should be a swappable slot; counter should instance the canonical Badge. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
Table - Scheduling ConsolidateRequires ReworkComponent link

A scheduled-payment row: a top date-amount line (date label on the left, peso-prefixed primary amount on the right) and an optional grid of nested label / value detail pairs below. Component description reads "Use to present Scheduled Payment information clearly to user." Published as 3 variants selected by the type enum: no display amount (date + amount only), 2 amounts display (one row of 2 detail pairs), 4 amounts display (two rows of 2 detail pairs=4 total). Third parallel record in the Table family alongside Table and Table - Transaction.

Fold into the Table consolidation; compose from Inline Text, not re-author
Table - Scheduling is the third feature-specific composition in the Table family (after Table - Transaction). It re-creates a date + peso-amount primary line and a grid of label/value pairs — layouts the DS already covers with Inline Text stacked inside a Generic Transaction Card or native List cell. Three records for three product surfaces (generic table, transaction limits, scheduled payments) is a family-level smell: the base pattern is "label / value rows with optional peso prefix," and every sibling duplicates it with narrower coverage. Recommend removing Table - Scheduling from core DS and documenting a "scheduled payment" recipe on the Table page that composes EBInlineText rows — same guidance as Table - Transaction. Mobile scheduling surfaces (auto-debit plans, installment schedules) already render as vertical lists on iOS and Android; no phone surface needs this fixed 360px grid.
In Context

Scheduled payments screen (auto-debit, installment plans, standing orders): a list of upcoming payment rows stamped with a date, the total debit amount, and — where relevant — a breakdown of principal / interest / fee components.

Payment ScheduleMAY 10, 2026₱1,250.00PrincipalPHP 1,100InterestPHP 150JUN 10, 2026₱1,250.00PrincipalPHP 1,110InterestPHP 140JUL 10, 2026₱1,250.00Manage Schedule
Live Preview

Toggle the type enum to cycle through the three variants: date + amount only, + 2 detail cells, or + 4 detail cells (two rows).

Properties
type
DS Health
Reusable
Warn
Locked to date + peso-prefixed amount + optional 2/4-cell label/value grid. Any scheduling surface that needs 1, 3, or N detail cells, a status chip, or a non-peso currency has to detach.
Self-contained
Fail
Primary amount renders a raster Peso Sign - Proxima image fill (same remote figma.com/api/mcp/asset/* URL as Table - Transaction). Detail cells prefix amounts with the literal string "PHP", mixing glyph and text prefixes inside one component.
Consistent
Fail
Does not reuse Table's type × no. of columns × icon schema — introduces a brand-new type enum whose values embed the detail count in a sentence ("2 amounts display"). Third family member, third shape.
Composable
Warn
Not built from Table, Inline Text, or Generic Transaction Card — re-implements the label / value cell inline. A true composition would nest EBInlineText instances for each detail pair.
Open Issues
  • Third parallel Table-family record with a new schema. After Table (type × no. of columns × icon) and Table - Transaction (type × no. of columns × icon, peso content), Scheduling introduces another axis shape: a single type enum that gates a fixed 0 / 2 / 4 detail count. Three records, three different schemas for the same underlying "rows of label/value pairs" idea. C1 · Layer Structure & Naming
  • type values embed the detail count in a sentence. Values are "no display amount", "2 amounts display", "4 amounts display" — natural-language strings that bake the count into the property. Should be an integer detailCount: 0 | 2 | 4, or — better — replaced by a data-driven details: [Pair] array that accepts any length. C2 · Variant & Property Naming
  • No native mobile primitive matches the layout. Payment schedules on iOS and Android render as list cells (List / LazyColumn) — a date header, an accessory amount, and optional secondary label/value rows. The 360px fixed grid is a desktop pattern; on phone width the two-cell detail row already crowds at the 12px type scale used here. C4 · Native Mappability
  • No tap, pressed, or disabled states. A scheduled payment row is typically tappable — to view, edit, or cancel the scheduled entry — and can be visually disabled (past / cancelled / skipped). None of that coverage exists. C5 · Interaction State Coverage
  • Peso sign is a raster image asset. Primary amount's currency prefix is an <img> pointing at a Figma-hosted bitmap, same remote URL as Table - Transaction. Detail amounts, meanwhile, use the literal string "PHP" — two different currency-prefix treatments in one component. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until the family consolidation decision lands — Code Connect for Scheduling would just duplicate the Inline Text mapping. C7 · Code Connect Linkability
Design Recommendations
  • Remove Table - Scheduling from core DS; publish as a recipe on Table's page composing Inline Text rows. Same guidance as Table - Transaction. Recipe reads: "For scheduled-payment lists (auto-debit, installments, standing orders), compose a date / primary-amount header row with EBInlineText, then stack additional EBInlineText rows for breakdown details inside a Generic Transaction Card or native List cell." Eliminates 3 variants, a raster peso asset, and the mixed /PHP prefix inconsistency. Family
  • Collapse the Table family (Table + Table - Transaction + Table - Scheduling) into one data-driven row primitive. If the family is retained for wider surfaces, publish a single EBTableRow with role: .header | .content and a columns: [Column] array where each column carries its own format: .text | .amount | .date. Scheduling's date-header + amount + detail-grid becomes: one row with two columns (date / amount), followed by N rows with detail pairs. Collapses 18 family variants into 2 role variants × data. Family
  • Rename type values to integers, or drop the property. If Scheduling is kept, rename to detailCount: 0 | 2 | 4 (no sentence fragments in property values). If merged into Inline Text / Table's data-driven row, drop the property entirely — the count is inferred from the details array. Rename
  • Unify the currency prefix treatment. Primary amount uses a raster peso glyph; detail amounts use the literal string "PHP". Pick one. Preferred: the Unicode glyph (U+20B1) everywhere, rendered in the row's type style — drop the raster asset and drop the "PHP" literal. Alternative: ship a vector peso icon (icon/currency/peso-sm) for the primary line and an optional "PHP" prefix for breakdown values, wired up as real tokens / slots. Asset
  • Compose each detail cell from Inline Text. The component already matches Inline Text's label + value shape (main/table/color/label-preamble on top, main/table/color/label below). Instance-swap to Inline Text so the tokens consolidate under main/inline-text/* and state / a11y improvements flow through to every consumer. Composition
  • Add row interaction states if Scheduling stays. A scheduled-payment row is tappable — add State=Pressed and State=Disabled variants (disabled=past / cancelled with muted labels and a strike or tag). Without these, every consumer re-invents the tap target. State
  • Document scheduling semantics. If Scheduling is kept, add guidance on which amounts belong on the primary line (total debit) vs. breakdown cells (principal / interest / fee / tax). Prevents the component from being used as a generic 3-amount row on non-scheduling surfaces. Docs
Variants

3 variants selected by the type enum. All three share the top date-amount row; types 2 amounts display and 4 amounts display append a details grid of label / value pairs below.

type=no display amount — 50.5px tall

Minimum variant. A single date-amount row: a 108px left-aligned date label (MMM DD, YYYY) and a right-aligned peso-prefixed primary amount. No detail rows.

type=2 amounts display — 89.5px tall

Date-amount row + one detail row. Details row carries a 111px "row-item" leading label and two equal-width cells in the remaining space, each a Label / PHP X,XXX.XX pair.

type=4 amounts display — 132.5px tall

Date-amount row + two detail rows (4 cells total). Second detail row shares the same 111px gutter and two label/value cells as the first, with 12px gap between detail rows.

Colors by State
RoleTokenDefaultPressedDisabled
Row bgmain/table/color/bg#FFFFFF
Date labelmain/table/color/label#0A2757
Primary amountmain/table/color/label-amount#005CE5
Currency glyph (peso, primary)main/table/color/icon-currency-primary#005CE5
Detail preamble labelmain/table/color/label-preamble#6780A9
Detail valuemain/table/color/label#0A2757

No interaction states. The primary-amount peso glyph is a raster fill despite the token main/table/color/icon-currency-primary existing for its color. Detail amounts use a literal "PHP" string prefix with no token indirection. C6

Layout
PropertyTokenValue
Row width (fixed)360px
Height — no display amount50.5px
Height — 2 amounts display89.5px
Height — 4 amounts display132.5px
Horizontal padding24px
Vertical paddingspace/space-1616px
Date column width108px
Detail leading column width111px
Date-row → details-row gap8px
Detail row → detail row gap12px
Detail label → value gap4px
Detail cell gap8px
Peso glyph size15 × 15px (raster)
Peso → amount gap2px
Typography
ElementDS text styleSpec
Date labelPrimary/Label/Light/FineProxima Soft Semibold · 12 / 12 · +0.5
Primary amountPrimary/Label/SmallProxima Soft Bold · 14 / 14 · +0.25
Detail preamble labelPrimary/Multi-line Label/Light/FineProxima Soft Semibold · 12 / 14 · +0.5
Detail value (PHP X,XXX.XX)Primary/Label/Light/FineProxima Soft Semibold · 12 / 12 · +0.5
Installation Planned API

Table - Scheduling is recommended for removal. The API shown below assumes the preferred mobile path: compose EBInlineText rows inside EBGenericTransactionCard or a plain VStack/Column. If Scheduling is retained, it re-uses Table's installation (eb-ds-ios, com.eastblue.ds:table).

Property Mapping (proposed — route to primitives)
FigmaSwiftUIComposeNotes
type (drop)details: [EBInlineText]details: List<EBInlineText>Count is inferred from the details array — no property needed. Accepts 0, 2, 4, or any N.
Date label (MMM DD, YYYY)EBInlineText(label:, value:)EBInlineText(label=, value=)Row header is a two-column Inline Text: date on the left, primary amount on the right.
Primary amount (peso + X,XXX.XX)Text("\u{20B1}" + amount).ebAmountStyle(.primary)Text("₱$amount", style=EBAmountStyle.Primary)Unicode peso glyph in the row's type style. Drop the raster asset.
Detail Label / PHP X,XXX.XXEBInlineText(label:, value:)EBInlineText(label=, value=)Each detail cell collapses into an Inline Text pair. "PHP" prefix is part of the value string.
Row tap target.onTapGesture { … } / ButtonModifier.clickable { … }Add if the surface supports viewing / editing a scheduled entry.
SwiftUI (if retained)
ios/Components/Table/EBTableSchedulingRow.swift
Jetpack Compose (if retained)
android/components/table/EBTableSchedulingRow.kt
Usage Snippets Planned API
// Recommended — compose with Inline Text (phone-width)
EBGenericTransactionCard {
    EBInlineText(label: "MAY 10, 2026", value: "₱1,250.00")
        .ebAmountStyle(.primary)
    HStack {
        EBInlineText(label: "Principal", value: "PHP 1,100.00")
        EBInlineText(label: "Interest",  value: "PHP 150.00")
    }
}

// Option B — if Scheduling row is retained
EBTableSchedulingRow(
    date: Date(),
    amount: 1250.00,
    details: [
        .init("Principal", "PHP 1,100.00"),
        .init("Interest",  "PHP 150.00")
    ]
)
// Recommended — compose with Inline Text (phone-width)
EBGenericTransactionCard {
    EBInlineText(label = "MAY 10, 2026", value = "₱1,250.00",
                 style = EBAmountStyle.Primary)
    Row {
        EBInlineText(label = "Principal", value = "PHP 1,100.00")
        EBInlineText(label = "Interest",  value = "PHP 150.00")
    }
}

// Option B — if Scheduling row is retained
EBTableSchedulingRow(
    date = LocalDate.now(),
    amount = 1250.00,
    details = listOf(
        SchedulingDetail("Principal", "PHP 1,100.00"),
        SchedulingDetail("Interest",  "PHP 150.00")
    )
)
Accessibility
RequirementiOSAndroid
Row semanticsIf tappable, wrap as Button with .accessibilityLabel("Scheduled payment, May 10 2026, 1,250 pesos") — combine the date and amount into one spoken phrase.Wrap in Modifier.clickable + Modifier.semantics(mergeDescendants=true); set contentDescription to a full spoken phrase.
Currency glyph fallbackUse Unicode \u{20B1} inline — avoid a raster image that won't scale with Dynamic Type.Use Unicode inline — avoid a bitmap that won't respect font-scale settings.
Date formattingUse Date.FormatStyle with the user's locale; don't hardcode MMM DD, YYYY.Use DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); don't hardcode format string.
Detail label / value pairingGroup each detail cell so VoiceOver reads "Principal, 1,100 pesos" as one element, not two.Group each detail cell so TalkBack reads "Principal, 1,100 pesos" as one element, not two.
Disabled / past rowsPast or cancelled schedules: .accessibilityHint("Past payment") + muted label tokens.Past or cancelled schedules: Modifier.semantics { stateDescription="Past payment" } + muted label tokens.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkThird parallel record in the Table family with yet another schema. Should fold in, not stand alone.
C2Variant & Property NamingReworktype values embed the detail count in sentence fragments ("2 amounts display").
C3Token CoveragePartialbg / label / label-amount / label-preamble / icon-currency-primary bound. The literal "PHP" string prefix on detail amounts has no token indirection.
C4Native MappabilityReworkScheduled payments are a List / LazyColumn pattern on mobile, not a fixed-width grid.
C5Interaction State CoverageReworkNo tap, pressed, or disabled states — rows are typically tappable (edit / cancel) or disabled (past / cancelled).
C6Asset & Icon QualityReworkPeso glyph is a raster image; detail amounts use a literal "PHP" prefix. Two currency-prefix treatments in one component.
C7Code Connect LinkabilityNot MappedBlocked until the family consolidation decision lands.
Variants Inventory (3 total)

A single type axis with 3 values. No cross-axis matrix — detail count is the only variant driver.

typeDetail cellsHeightNode ID
no display amount050.5px47:324362
2 amounts display2 (1 row × 2 cells)89.5px47:324363
4 amounts display4 (2 rows × 2 cells)132.5px47:324364

Detail layout is locked to pairs of 2 cells. No 1-cell, 3-cell, or N-cell variants exist — a data-driven details array would cover all shapes in one record.

1.0.0 — April 2026 Initial
Initial Assessment · node 47:324365
Family assessed — 3 variants selected by a single type enum. Third parallel Table-family record (Table, Table - Transaction, Table - Scheduling). Documented
Initial
Third family schema divergence — Introduces a new type enum instead of reusing Table's type × no. of columns × icon or Transaction's schema. Recommend folding into Inline Text / Table data-driven row. Open
C1 Open
Sentence-fragment enum values"no display amount" / "2 amounts display" / "4 amounts display" bake the detail count into natural-language strings. Open
C2 Open
No native mobile primitive — Scheduled payments are a List / LazyColumn pattern on mobile, not a fixed 360px grid. Open
C4 Open
No interaction / disabled states — Scheduling rows are typically tappable or visually muted for past / cancelled entries. Open
C5 Open
Mixed currency prefix treatments — Primary amount ships as a raster peso glyph; detail amounts use a literal "PHP" string. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
Table - Transaction ConsolidateRequires ReworkComponent link

A transaction-themed fork of Table: header row with 2 or 3 column labels (optional icon placeholder per column) and a content row with peso-prefixed amount columns. Component description reads "Use to present user's transactions and related details" — per-variant descriptions specifically cite account limits. Published as 6 variants (type: header/content · no. of columns: 2/3 · icon: yes/no — icon only on header). Overlaps heavily with Table, Generic Transaction Card, and Inline Text.

Fold into Table as a recipe, not a separate primitive
Table - Transaction is a feature-specific composition of existing DS primitives — a header row (which Table already ships) and a content row that is effectively an Inline Text with a peso-prefixed amount. Publishing it as its own component duplicates the Table variant matrix and introduces a raster peso asset to DS surface. On mobile, "transaction details" render as vertical label / amount stacks — exactly what Generic Transaction Card and Inline Text already cover. Recommend removing from core DS and documenting the pattern as a recipe on Table's page: "For transaction totals, compose an EBInlineText stack; use Table only for multi-column tabular history on wider surfaces."
In Context

Per-variant descriptions cite account limits — e.g. daily / monthly send caps showing used vs. remaining peso amounts in aligned columns. Other GCash surfaces like transaction history and receipts use the Generic Transaction Card vertical stack, not this tabular layout.

Account LimitsUSEDREMAININGTOTALDaily₱3,500₱6,500₱10,000Monthly₱42,000₱58,000₱100,000Request Increase
Live Preview

Toggle type (header / content), column count (2 / 3), and icon (header only). Content rows show peso-prefixed placeholder amounts.

Properties
type
no. of columns
icon (header only)
DS Health
Reusable
Warn
Locked to the peso-prefixed amount shape. Any transaction surface that needs a date column, a badge, a status chip, or a non-peso currency has to detach or reach for a different component.
Self-contained
Fail
Content row ships a raster Peso Sign - Proxima image fill, not a DS icon instance or text glyph. Handoff code references a remote figma.com/api/mcp/asset/* URL — not deliverable to native.
Consistent
Fail
Reuses Table's no. of columns string-with-period naming, but drops the 4-column option and the content icon variant — schema diverges from parent for no apparent reason. Label token main/table/color/label-preamble appears only here.
Composable
Warn
Not built from Table, Inline Text, or Generic Transaction Card — re-implements the label and amount stack directly. A true composition would reuse those primitives as nested instances.
Open Issues
  • Duplicate of Table's variant matrix with narrower coverage. Table already ships header / content rows with 2–4 columns and the same icon=yes/no axis. Table - Transaction re-creates that axis but only for 2 and 3 columns, then specializes the content row to peso amounts. The overlap is a maintenance drag and the layer tree is a separate sibling instead of a reuse. C1 · Layer Structure & Naming
  • no. of columns inherits Table's period-in-name string enum. Should be an integer columnCount — or, better, removed altogether in favor of a data-driven columns array on the parent Table. The Transaction fork inherits the anti-pattern without fixing it. C2 · Variant & Property Naming
  • No native mobile primitive matches this layout. Same mobile problem as Table: iOS SwiftUI Table is macOS/iPad only, Material Compose has no Table primitive. Phone-width transaction totals render as stacked EBInlineText rows (label + peso amount) — not a 360px fixed three-column table. C4 · Native Mappability
  • No interaction or semantic-amount states. Content row has no pressed / disabled states and amount cells don't distinguish positive, negative, or zero amounts despite being a transaction surface. C5 · Interaction State Coverage
  • Peso sign ships as a raster image asset. The content row's currency prefix is an <img> pointing at a Figma-hosted bitmap (figma.com/api/mcp/asset/...), not an inline glyph, SF Symbol, or vector icon. Header icon placeholder is still a hardcoded #C2C6CF circle as in Table. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until the family consolidation decision lands — Code Connect for Table - Transaction would just duplicate Table's mapping. C7 · Code Connect Linkability
Design Recommendations
  • Remove Table - Transaction from core DS; publish as a Table recipe and a Generic Transaction Card / Inline Text pattern. The component is a feature-specific composition, not a primitive. Document two ready-made paths in the Table page: (a) "For aligned multi-column amount totals on wider surfaces, use Table with amount-formatted column content"; (b) "For phone-width transaction details (receipt, limits, fees), compose EBInlineText rows inside Generic Transaction Card." Eliminates 6 duplicate variants and the raster peso asset from DS surface. Family
  • If Transaction stays, consolidate with Table's row primitive and add an amountFormat column flag. Merge into Table's data-driven row with a per-column format: .text | .amount. Amount formatting would auto-prefix the currency glyph. Collapses Table + Table - Transaction from 3 records / 15 variants into 1 record / 2 variants × data. Composition
  • Replace the raster peso sign with a text glyph or vector icon. Currency prefix should be the native Unicode glyph (U+20B1) rendered in the row's type style, or — if visual weight needs to match Proxima's peso — a vector SVG shipped as a DS icon (icon/currency/peso-sm). Drop the Figma-hosted bitmap reference. Asset
  • Rename or drop no. of columns. Same fix as Table. If the component is kept, rename to an integer columnCount. If merged into Table's data-driven row, drop it entirely — column count is inferred from the columns array. Rename
  • Align amount label token with Inline Text. The content-row label uses main/table/color/label-preamble (#6780A9) while the amount value uses main/table/color/label (#0A2757). Inline Text covers the same semantic pair with label / value tokens. Merge into a shared token set (main/inline-text/color/label, main/inline-text/color/value) to prevent drift. Token
  • Document amount-sign semantics. If Table - Transaction is kept for account limits, add explicit guidance that amounts are always positive totals (used / remaining / cap). For signed transaction flows (income / expense), direct consumers to Generic Transaction Card. Prevents the component from being mis-used on flows it wasn't designed for. Docs
Variants

6 variants split across type (header / content) × no. of columns (2 / 3) × icon (yes / no — header only; content always has icon=no).

Header row — 36 / 62px tall

Subtle-bg row with bottom border. Repeats a 10px Proxima Soft Semibold "Column Label" N times across equal-width flex columns. icon=yes grows height from 36 to 62px and stacks a 24px placeholder circle above each label.

Content row — 72.5px tall

White bg, bottom border. Two-line layout: a 14px preamble label on top, then N equal-width amount cells below. Each amount cell renders a 15px peso-sign raster + a 14px Proxima Soft Bold numeric value (X,XXX.XX).

Colors by State
RoleTokenDefaultPressedDisabled
Header bgmain/table/color/bg-subtle#F6F9FD
Content bgmain/table/color/bg#FFFFFF
Row bordermain/table/color/border#E5EBF4
Header column labelmain/table/color/label#0A2757
Content preamble labelmain/table/color/label-preamble#6780A9
Amount valuemain/table/color/label#0A2757
Currency icon (peso)main/table/color/icon-currency-secondary#183462
Header icon placeholder— (hardcoded)#C2C6CF

No interaction states defined. Peso sign is a raster fill despite the token existing for its color. Header icon placeholder is hardcoded hex. C6

Layout
PropertyTokenValue
Row width (fixed)360px
Header height (icon=no)36px
Header height (icon=yes)62px
Content row height72.5px
Header horizontal paddingspace/space-2424px
Header pt / pb12 / space/space-1212 / 12px
Content horizontal padding24px
Content pyspace/space-1616px
Header column gap8px
Content label → amounts gap8px
Amount column gap16px
Header icon → label gap2px
Header icon size24 × 24px
Peso glyph size15 × 15px (raster)
Peso → amount gap2px
Typography
ElementDS text styleSpec
Header column labelPrimary/Multi-line Label/Light/TinyProxima Soft Semibold · 10 / 12 · +0.25
Content preamble labelPrimary/Label/Light/SmallProxima Soft Semibold · 14 / 14 · +0.25
Amount valuePrimary/Label/SmallProxima Soft Bold · 14 / 14 · +0.25
Installation Planned API

Table - Transaction is recommended for removal. The API shown below assumes the preferred mobile path: compose EBInlineText rows inside EBGenericTransactionCard or a plain VStack/Column. If Table - Transaction is retained, it re-uses Table's installation (eb-ds-ios, com.eastblue.ds:table).

Property Mapping (proposed — route to primitives)
FigmaSwiftUIComposeNotes
type=headerEBTableRow(role: .header, …)EBTableRow(role=Header, …)Delegate to Table's row primitive — same bg-subtle styling, same label typography.
type=content (peso amounts)VStack { EBInlineText(…) }Column { EBInlineText(…) }On mobile, render as stacked label / value rows. The 3-column peso layout is a desktop/tablet pattern.
no. of columns (drop)columns: [Column]columns: List<Column>Inferred from the data array — same as Table.
icon=yes (header slot)leadingIcon: Image?leadingIcon: @Composable (() -> Unit)?Per column. Same slot pattern as Table header.
Peso Sign - Proxima (raster)Text("\u{20B1}" + amount)Text("₱$amount")Prefix the value with the Unicode peso glyph. Drop the bitmap asset.
Label (preamble) / X,XXX.XXEBInlineText(label:, value:)EBInlineText(label=, value=)Each amount column collapses into an Inline Text pair.
SwiftUI (if retained)
ios/Components/Table/EBTableTransactionRow.swift
Jetpack Compose (if retained)
android/components/table/EBTableTransactionRow.kt
Usage Snippets Planned API
// Recommended — compose with Inline Text (phone-width)
EBGenericTransactionCard {
    EBInlineText(label: "Daily used",      value: "₱3,500.00")
    EBInlineText(label: "Daily remaining", value: "₱6,500.00")
    EBInlineText(label: "Daily cap",       value: "₱10,000.00")
}

// Option B — if multi-column Table row is retained (tablet / wider)
VStack(spacing: 0) {
    EBTableRow(
        role: .header,
        columns: ["Used", "Remaining", "Cap"]
    )
    EBTableRow(
        role: .content,
        label: "Daily",
        columns: [
            .amount("3,500.00"),
            .amount("6,500.00"),
            .amount("10,000.00")
        ]
    )
}
// Recommended — compose with Inline Text (phone-width)
EBGenericTransactionCard {
    EBInlineText(label = "Daily used",      value = "₱3,500.00")
    EBInlineText(label = "Daily remaining", value = "₱6,500.00")
    EBInlineText(label = "Daily cap",       value = "₱10,000.00")
}

// Option B — if multi-column Table row is retained (tablet / wider)
Column {
    EBTableRow(
        role = EBTableRowRole.Header,
        columns = listOf("Used", "Remaining", "Cap")
    )
    EBTableRow(
        role = EBTableRowRole.Content,
        label = "Daily",
        columns = listOf(
            TableColumn.Amount("3,500.00"),
            TableColumn.Amount("6,500.00"),
            TableColumn.Amount("10,000.00")
        )
    )
}
Accessibility
RequirementiOSAndroid
Amount semanticsEach amount cell should read as a single element — wrap peso + value and use .accessibilityLabel("Three thousand five hundred pesos").Merge peso + value with Modifier.semantics(mergeDescendants=true); set contentDescription to a spoken phrase, not the raw glyph.
Currency glyph fallbackUse Unicode \u{20B1} inline — avoid a raster image that won't scale with Dynamic Type.Use Unicode inline — avoid a bitmap that won't respect font-scale settings.
Header vs content rowHeader rows carry .accessibilityAddTraits(.isHeader).Header rows use Modifier.semantics { heading() }.
Column label / amount pairingGroup the preamble label with each amount column so VoiceOver reads "Daily, three thousand five hundred pesos".Group the preamble label with each amount column so TalkBack reads "Daily, three thousand five hundred pesos".
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkDuplicate sibling to Table with narrower column coverage. Should fold in, not stand alone.
C2Variant & Property NamingReworkInherits Table's no. of columns string-with-period anti-pattern unchanged.
C3Token CoveragePartialbg / border / label / label-preamble / icon-currency-secondary all bound. Header icon placeholder uses hardcoded #C2C6CF.
C4Native MappabilityReworkNo native mobile Table primitive; transaction totals render as Inline Text stacks.
C5Interaction State CoverageReworkNo pressed / disabled states; no semantic handling of positive / negative amounts.
C6Asset & Icon QualityReworkPeso sign ships as a raster <img>; header icon is a hardcoded placeholder circle.
C7Code Connect LinkabilityNot MappedBlocked until the family consolidation decision lands.
Variants Inventory (6 total)

A 2 type × 2 no. of columns × 2 icon matrix, pruned: icon only applies to header rows, so content × icon=yes doesn't exist. Total 6 variants.

typeno. of columnsiconHeightNode ID
header2no36px47:324707
header3no36px47:324703
header2yes62px47:324705
header3yes62px47:324706
content2no72.5px47:324704
content3no72.5px47:324708

Column-count coverage (2, 3) is narrower than Table's parent (2, 3, 4). No 4-column Transaction variant exists despite the parent supporting it.

1.0.0 — April 2026 Initial
Initial Assessment · node 47:324709
Family assessed — 6 variants (2 type × 2 columns × 2 icon, pruned). Only used for account-limit surfaces in the sticker sheet. Documented
Initial
Duplicate of Table — Reuses Table's variant schema with narrower column coverage and a peso-specialised content row. Recommend folding into Table as a recipe. Open
C1 Open
no. of columns inherited naming — String enum with a period in the property name, carried over from Table. Open
C2 Open
No native mobile primitive — Same mobile problem as Table; transaction totals should be Inline Text stacks. Open
C4 Open
No interaction or amount-sign states — No pressed / disabled, no positive / negative amount differentiation. Open
C5 Open
Peso sign is a raster asset — Content row currency prefix is an image fill, not a text glyph or vector. Header icon placeholder is still hardcoded #C2C6CF. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
Table RestructureRequires ReworkComponent link

A tabular row primitive rendered as a 360px-wide strip with a label column and 1–3 right-aligned description columns. Published as a 9-variant component matrix (type: header/content · no. of columns: 2/3/4 · icon: yes/no — icon only applies to headers). Two orphan sub-components Table - Item and Table - Label are defined but never placed directly in screens.

Collapse the family and rethink Table on mobile
The 3-component setup (Table + Table - Item + Table - Label) hardcodes a column-count variant matrix that doesn't scale. On mobile, tabular data almost always renders as a vertical stack of label/value pairs — which is exactly what the existing Inline Text component already does. Evaluate whether Table should ship as a DS primitive at all, or be reserved for true data-dense desktop contexts while mobile screens compose List + Inline Text rows instead. If Table stays, collapse the matrix into a single data-driven row (columns: [Column]) with named leading / trailing slots and optional per-row icon.
In Context

Sticker sheet shows Table instances stacked on a Template Screen to build a static 6-row pattern — 1 header row + 5 content rows. No scroll, no sort, no selection.

TitleHeaderColumnColumnColumnLabelDescDescDescLabelDescDescDescLabelDescDescDescLabelDescDescDescLabel
Live Preview

Toggle type (header / content), column count, and icon (header only). Labels and descriptions use placeholder copy.

Properties
type
no. of columns
icon (header only)
DS Health
Reusable
Warn
Works for the narrow case of 2–4 equal-width columns with a left label and right descriptions. Breaks for mixed widths, amounts, badges, or sortable columns — all common table use cases.
Self-contained
Warn
Header row carries bg, border, label/column typography; content row carries label + description. But column count is locked at build time — consumers can't add or remove columns without detaching.
Consistent
Fail
no. of columns is a string enum with a period in the name — collides with C2 naming conventions. Three orphan sibling components (Table, Table - Item, Table - Label) when one data-driven row would suffice. C2
Composable
Warn
Items and Labels are declared as reusable atoms but the parent Table doesn't slot them — it re-implements the label and description directly. No real composition.
Open Issues
  • Three orphan components instead of one data-driven row. Table, Table - Item, and Table - Label are published as separate components but only Table is consumed — Item and Label are never placed directly. Should collapse into a single row primitive that accepts a columns array. C1 · Layer Structure & Naming
  • no. of columns uses string enum with a period in the property name. Should be an integer columnCount — the period breaks code-friendly naming and the string "2"/"3"/"4" can't interpolate to a data-driven row count. C2 · Variant & Property Naming
  • No native mobile primitive matches this layout. iOS SwiftUI Table is macOS/iPad-only; Material Compose has no Table primitive. On phones, tabular data is a vertical stack of label/value pairs (Inline Text) or a horizontally-scrollable list. Current 360px-fixed rows don't adapt. C4 · Native Mappability
  • No interaction state coverage. Rows have no hover, pressed, focused, selected, or disabled states. If rows are ever tappable (drill-down into a row detail), there's no visual affordance. C5 · Interaction State Coverage
  • Header icon is a raw placeholder circle.icon=yes variants render a hardcoded #C2C6CF 24px circle with no instance swap or named slot. Consumers have no documented way to set the icon. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until the family consolidates and native decision lands. C7 · Code Connect Linkability
Design Recommendations
  • Reconsider whether Table belongs in a mobile-first DS. GCash ships on phones where tables almost never render as true HTML-style tables. The sticker sheet pattern screen is doing what Inline Text already does — a stack of label / value rows. Two cleaner paths: (a) remove Table from core DS, publish usage guidance pointing to List + Inline Text for label/value data; (b) keep Table but scope it to genuine multi-column data (transaction history, scheduled payments, etc.) and rebuild it as a data-driven row. Family
  • Collapse the three-component family into one row primitive. Merge Table, Table - Item, and Table - Label into a single Table Row component with a columns slot array and a role variant (header / content). Eliminates the no. of columns variant explosion — 9 records collapse into 2 + slot content. Published node count drops from 3 components / 14 variants to 1 component / 2 variants. Property
  • Rename no. of columns to an integer (or drop it entirely). The period in the property name breaks C2 naming conventions. If the data-driven restructure lands, column count is inferred from the columns array and the prop disappears. Otherwise rename to columnCount with integer values. Rename
  • Replace the header icon placeholder with a named slot. Declare an icon Slot on the header-role row so consumers drop in any 24px icon component. Drop the icon=yes/no boolean — slot presence signals intent. Maps cleanly to @ViewBuilder / @Composable slot. Slot
  • Add row interaction states. If rows are ever tappable (drill-down row detail, sortable columns), publish default / pressed / selected / disabled state variants. If rows stay display-only, document that explicitly so native devs wrap in a non-interactive container. State
  • Introduce shared label/value token set with Inline Text. Table label and Inline Text label serve the same semantic role — "a thing and its value". Aligning tokens (main/table/color/label with main/inline-text/*) reduces drift and supports cross-component theming. Token
Variants

9 variants split across type (header / content) × no. of columns (2 / 3 / 4) × icon (yes / no — header only; content always has icon=no).

Header row — 37 / 65px tall

Subtle-bg row with bottom border. Primary-bold label on the left, semibold columns on the right. icon=yes grows height from 37 to 65px and adds a 24px placeholder circle above each column.

Content row — 56px tall

White bg. Bold 12px label on the left, 10px BarkAda Semibold description columns on the right (1, 2, or 3 of them).

Colors by State
RoleTokenDefaultPressedDisabled
Header bgmain/table/color/bg-subtle#F6F9FD
Content bgmain/table/color/bg#FFFFFF
Row bordermain/table/color/border#E5EBF4
Label / column textmain/table/color/label#0A2757
Description textmain/table/color/description#6780A9
Header icon placeholder— (hardcoded)#C2C6CF

No interaction states today — no hover / pressed / selected / disabled tokens are defined. Header icon placeholder is hardcoded hex, not a token. C6

Layout
PropertyTokenValue
Row width (fixed)360px
Header height (icon=no)37px
Header height (icon=yes)65px
Content row height56px
Horizontal paddingspace/space-2424px
Header pt / pbspace/space-8, space/space-128 / 12px
Content py12px
Column gap16px
Label width99px min
Header icon size24 × 24px
Icon → column gapspace/space-22px
Typography
ElementDS text styleSpec
Header labelPrimary/Label/SmallProxima Soft Bold · 14 / 14 · +0.25
Header columnPrimary/Multi-line Label/Light/FineProxima Soft Semibold · 12 / 14 · +0.5
Content labelPrimary/Label/FineProxima Soft Bold · 12 / 12 · +0.5
Content descriptionSecondary/Bold/Small CaptionBarkAda Semibold · 10 / 15 · 0
Installation Planned API

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:table:1.0.0")
}
Property Mapping (proposed — data-driven)
FigmaSwiftUIComposeNotes
type=header/contentrole: .header / .contentrole=EBTableRowRole.Header / ContentHeader styles bg-subtle + bottom border; content is plain white.
no. of columns (drop)columns: [Column]columns: List<Column>Column count inferred from the data array — no separate prop.
icon (slot)leadingIcon: Image?leadingIcon: @Composable (() -> Unit)?Header-only. Drop icon=yes/no boolean — slot presence signals intent.
Label here textlabel: Stringlabel: StringLeft-aligned primary label.
Description text (xN)columns: [Column]columns: List<Column>Each Column has its own text / alignment / optional secondary value.
SwiftUI
ios/Components/Table/EBTableRow.swift
Jetpack Compose
android/components/table/EBTableRow.kt
Usage Snippets Planned API
// Option A — dedicated row primitive (if Table stays in DS)
VStack(spacing: 0) {
    EBTableRow(
        role: .header,
        label: "Header",
        columns: ["Column", "Column", "Column"]
    )
    ForEach(rows) { row in
        EBTableRow(
            role: .content,
            label: row.label,
            columns: row.values
        )
    }
}

// Option B — compose with Inline Text (recommended for mobile)
VStack(spacing: 12) {
    EBInlineText(label: "Amount",      value: "₱1,000.00")
    EBInlineText(label: "Reference",   value: "0000 0123 4567")
    EBInlineText(label: "Date",        value: "Apr 22, 2026")
}
// Option A — dedicated row primitive (if Table stays in DS)
Column {
    EBTableRow(
        role = EBTableRowRole.Header,
        label = "Header",
        columns = listOf("Column", "Column", "Column")
    )
    rows.forEach { row ->
        EBTableRow(
            role = EBTableRowRole.Content,
            label = row.label,
            columns = row.values
        )
    }
}

// Option B — compose with Inline Text (recommended for mobile)
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
    EBInlineText(label = "Amount",      value = "₱1,000.00")
    EBInlineText(label = "Reference",   value = "0000 0123 4567")
    EBInlineText(label = "Date",        value = "Apr 22, 2026")
}
Accessibility
RequirementiOSAndroid
Tabular semanticsOn iPad/macOS use Table; on iPhone use accessibilityElement(children: .combine) per row so each reads as "Label, value, value".Wrap in Modifier.semantics { collectionInfo=CollectionInfo(rowCount, columnCount) }.
Header vs content rowUse .accessibilityAddTraits(.isHeader) on header rows.Use Modifier.semantics { heading() } on header rows.
Column headers without visible textIf icon-only header columns exist, provide .accessibilityLabel.Set contentDescription on the icon slot.
Row-level tapIf rows become tappable, wrap row in Button with .accessibilityHint.Wrap in Modifier.clickable with role=Role.Button.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRework3 orphan components (Table, Table - Item, Table - Label) where 1 data-driven row would suffice.
C2Variant & Property NamingReworkno. of columns uses string enum with a period; should be integer or dropped.
C3Token CoveragePartialLabel / bg / border bound to main/table/*. Header icon placeholder uses hardcoded #C2C6CF.
C4Native MappabilityReworkNo native iOS/Android mobile primitive. Needs a mobile rethink — list of label/value pairs vs true desktop table.
C5Interaction State CoverageReworkNo hover / pressed / selected / disabled states.
C6Asset & Icon QualityReworkHeader icon is a raw placeholder circle with no slot or instance swap.
C7Code Connect LinkabilityNot MappedBlocked until family consolidation and native decision land.
Variants Inventory (9 total across the family root)

Table ships 9 variants. Family siblings Table - Item (3 variants) and Table - Label (2 variants) are declared but never placed directly in screens — only Table is consumed.

typeno. of columnsiconHeightNode ID
header2no37px47:323220
header3no37px47:323222
header4no37px47:323224
header2yes65px47:323221
header3yes65px47:323223
header4yes65px47:323225
content2no56px47:325867
content3no56px47:325868
content4no56px47:325869

Family siblings: Table - Item 27:322607 (3 variants — description / icon / empty), Table - Label 27:322640 (2 variants — description=no / yes). Both would fold into the proposed data-driven columns slot.

1.0.0 — April 2026 Initial
Initial Assessment · node 47:326260
Family assessed — 3 published components (Table / Table - Item / Table - Label) with 9 + 3 + 2 variants. Only Table is consumed in screens. Documented
Initial
Three orphan components — Table - Item and Table - Label exist but are never placed directly. Recommend collapsing into one data-driven row. Open
C1 Open
no. of columns naming — String enum with a period in the property name. Open
C2 Open
No native mobile primitive — Mobile tables are almost always stacks of label/value pairs (Inline Text) — reconsider whether Table belongs in the DS at all. Open
C4 Open
No interaction states — No hover / pressed / selected / disabled state variants. Open
C5 Open
Header icon is a raw placeholder — Hardcoded #C2C6CF circle with no slot or instance swap. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
Tabs FixNeeds RefinementComponent link

The Tabs container composes a row of Tab Item atoms and manages the active index. Currently exposes 3 variants split by tabsCount (2 / 3 / 4). Figma component is currently named singular "Tab" — should be renamed "Tabs".

Rename + drop count variant
Rename the Figma component from "Tab" → "Tabs" (plural) to disambiguate from the Tab Item atom. Drop tabsCount — native tabs accept a list of items, not a fixed count variant. The container becomes one flexible component instead of 3 rigid variants.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. Tabs sit below a Title Bar to switch between screen sections.

TitleOverviewDetailsHistory
Live Preview

Toggle the tab count to see 2 / 3 / 4-tab layouts. Each cell is a Tab Item in the vertical small configuration.

Properties
tabsCount
active
DS Health
Reusable
Pass
Used anywhere a screen needs to switch between sections — Transactions, Vouchers, Profile tabs, category filters. Container adapts to 2–4 tabs via variant.
Self-contained
Pass
Carries its own width, shadow (Depth/D4), and flex layout. Composes Tab Item children cleanly.
Consistent
Warn
Component named "Tab" (singular) while it's actually the container — confusing alongside the Tab Item atom. tabsCount uses string values ("2"/"3"/"4") instead of integer or dropping the variant. C2
Composable
Pass
Every cell is an instance of the canonical Tab Item (27:89110). Changes to Tab Item propagate here.
Behavior
StateiOSAndroidFigma PropertyNotes
2 tabsYesYestabsCount="2"Width 124px
3 tabsYesYestabsCount="3"Width 186px
4 tabsYesYestabsCount="4"Width 248px
5+ tabs / scrollableN/AN/ANot documented. Native needs a scrollable variant for overflow. C5
Open Issues
  • Component name is singular ("Tab"). Should be plural ("Tabs") to match the container semantics and disambiguate from the Tab Item atom. C2 · Variant & Property Naming
  • tabsCount is a variant with string values."2"/"3"/"4" — native tabs take a list; the count should not be a discrete Figma variant. Drop the property and let the container accept a collection. C2 · Variant & Property Naming
  • No scrollable / overflow variant documented. For 5+ tabs, native uses ScrollableTabRow on Android and horizontal scroll on iOS — DS has no pattern, so engineers improvise. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the plural rename and tabsCount drop land. C7 · Code Connect Linkability
Design Recommendations
  • Rename "Tab" → "Tabs" (plural). Matches the atom/container pattern already used by Avatar + Avatar Group and Menu Grid + Service Item. Rename
  • Drop the tabsCount variant. The container should be a single flexible component that accepts an array of Tab Items. Collapses 3 variants → 1. Property
  • Add a scrollable variant (or document that 5+ tabs should switch to a ScrollableTabRow pattern). Prevents engineers from improvising overflow behavior. State
Variants

Three variants by tab count. Each cell is an instance of Tab Item (vertical, small). First tab is active, remaining are inactive.

4 tabs — default

4 Tab Items in an equal-width flex row. 248px total width.

3 tabs

3 Tab Items in an equal-width flex row. 186px total width.

2 tabs

2 Tab Items in an equal-width flex row. 124px total width.

Layout
PropertyValue
Total width (2 tabs)124px
Total width (3 tabs)186px
Total width (4 tabs)248px
Per-tab width62px (flex 1 0 0)
Gap between tabs0 (shared border-bottom)
ShadowDepth/D4 — 0 0 8px #73819A1A

Color, typography, and state tokens are defined on the Tab Item atom. See Tab Item → Style for the full color reference.

Installation Planned API

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:tabs:1.0.0")
}
Property Mapping (proposed — after dropping tabsCount)
Proposed APISwiftUIComposeNotes
itemsitems: [EBTabItem]items: List<EBTabItem>Array of Tab Items — replaces tabsCount variant
selectedIndex@Binding selection: IntselectedIndex: IntActive tab index
onSelectonSelect: (Int) -> VoidonSelect: (Int) -> UnitTab tap callback
scrollable.scrollable(true)scrollable: BooleanFor 5+ tabs — not yet defined in Figma
SwiftUI
ios/Components/Tabs/EBTabs.swift
Jetpack Compose
android/components/tabs/EBTabs.kt
Usage Snippets Planned API
EBTabs(
    items: [
        EBTabItem(label: "Overview", icon: .image(Image("overview"))),
        EBTabItem(label: "Details"),
        EBTabItem(label: "History")
    ],
    selection: $selectedIndex
)
EBTabs(
    items = listOf(
        EBTabItem(label = "Overview", icon = painterResource(R.drawable.overview)),
        EBTabItem(label = "Details"),
        EBTabItem(label = "History")
    ),
    selectedIndex = state,
    onSelect = { state = it }
)
Accessibility
RequirementiOSAndroid
Tab list roleAutomatic via TabViewAutomatic via TabRow (Material semantics)
Selected state announced.accessibilityAddTraits(.isSelected) on active tabselected=true in semantics
Keyboard / focus navigationiOS handles via focus traitsCompose handles via focus semantics
Usage Guidelines

Do

Use 2–4 tabs for primary navigation within a screen. Tabs should represent peer sections of equal importance.

Don't

Use tabs for sequential flows (Step 1 / Step 2 / Step 3) — use a Stepper component instead.

Do

Keep tab labels short (one or two words). If labels exceed 12 characters, use icons only or switch to vertical orientation.

Don't

Nest Tabs inside Tabs — creates navigation ambiguity and accessibility issues.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyTab 1, Tab 2, ... container, icon-label. Semantic.
C2Variant & Property NamingNeeds FixComponent named singular "Tab"; tabsCount uses string values and shouldn't be a variant at all.
C3Token CoverageReadyShadow and spacing bound to tokens. Colors live on the Tab Item atom.
C4Native MappabilityReadyMaps cleanly to TabView / TabRow.
C5Interaction State CoveragePartialActive/inactive covered via Tab Item. No scrollable pattern for 5+ tabs.
C6Asset & Icon QualityReadyChildren are Tab Item instances.
C7Code Connect LinkabilityPendingNot mapped.
Variants Inventory (3 total)
tabsCountWidthNode ID
2124px18482:33259
3186px18482:33255
4248px18482:33250

After the recommended restructure these 3 variants collapse to 1 flexible container accepting a list.

1.0.0 — April 2026 Initial
Initial Assessment · node 18482:33249
Component assessed — 3 variants (tabsCount 2/3/4). Container composing Tab Item children. Recommended rename "Tab" → "Tabs" and dropping the count variant. Documented
Initial
Component named singular — "Tab" should be "Tabs" (plural) to disambiguate from the Tab Item atom. Open
C2 Open
tabsCount is a variant property — Should be removed; the container should accept a list of Tab Items instead of exposing a fixed count enum. Open
C2 Open
No scrollable variant — 5+ tabs have no documented overflow pattern. Open
C5 Open
Code Connect mappings — Not registered. Open
C7 Open
Terms & Conditions Accordion RemoveN/AComponent link

Two variants (expanded=yes, expanded=no) of the canonical Accordion with the title hardcoded to "Terms & Conditions" and the expanded body hardcoded to a fixed list of four voucher rules. It carries no unique schema, no unique tokens, and no unique behavior — every color references main/accordion/* tokens directly from the canonical Accordion.

Remove — use the canonical Accordion
The DS already ships one Accordion primitive with a title prop and a content slot. Every time a product needs an accordion with a different title or body, the answer is to author a new instance — not to ship a new sibling. Replace this component with a documented usage example: EBAccordion(title: "Terms & Conditions", content: [...]).
Current Figma

Two variants — collapsed and expanded. Both reuse the canonical Accordion's tokens (main/accordion/color/*) for header, label, chevron, and border; the expanded body packs four list rows bound to main/list-item/color/default/*. No tokens or structure are unique to this component.

DS Health
Reusable
Fail
Not reusable — the title is hardcoded to "Terms & Conditions" and the body is a fixed list of four voucher rules. Any other use case must detach and rebuild.
Self-contained
Warn
Structure is correct, but every token it carries belongs to the canonical Accordion. This component adds no styles or logic of its own.
Consistent
Fail
Breaks the DS pattern where components are primitives with props. This is a composition (a filled-in instance) disguised as a component, and it would set a precedent for a sibling per hardcoded title.
Composable
Pass
The composition itself is clean — it uses the canonical Accordion tokens and embeds list rows correctly. That's exactly why it should live in product code, not as a DS atom.
Native equivalent Planned API

Use the canonical EBAccordion with a product-defined title and body. No dedicated "Terms & Conditions" component is needed on either platform.

// Just use the canonical Accordion
EBAccordion(title: "Terms & Conditions") {
    VStack(alignment: .leading, spacing: 8) {
        ForEach(rules) { rule in
            EBListItem(rule.text)
        }
    }
}
// Just use the canonical Accordion
EBAccordion(title = "Terms & Conditions") {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        rules.forEach { rule ->
            EBListItem(content = rule.text)
        }
    }
}
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyLayers match the canonical Accordion.
C2Variant & Property NamingReadyOnly an expanded boolean — identical to the canonical Accordion's state axis.
C3Token CoverageReadyAll colors bound to main/accordion/* and main/list-item/* tokens from the canonical components.
C4Native MappabilityN/ADo not map. Use EBAccordion with a product-defined title and body.
C5Interaction State CoverageReadyInherits expanded/collapsed from the canonical Accordion.
C6Asset & Icon QualityReadyChevron and check icons are vector instances.
C7Code Connect LinkabilityN/ANot linkable — no native counterpart. Consumers call EBAccordion directly.
1.0.0 — April 2026 Initial
Initial Assessment · node 5119:5447
Assessed as a product composition — Instance of the canonical Accordion (16870:9288) with hardcoded title and body. No unique schema or tokens. Documented
Initial
Recommendation: Remove — Delete from the sticker sheet and publish as a usage example of EBAccordion. Sets the precedent that product-specific compositions do not become DS siblings. Open
Design Decision
Text Area ConsolidateRequires ReworkComponent link

Multi-line text input with placeholder, filled, focus, error, and disabled states. 8 variants across State × isFilled. Mirrors Input Field's schema exactly — differs only in line count and a desktop resize-handle glyph. Native platforms treat multi-line as a single TextField modifier, not a distinct component.

Consolidate into Input Field
Text Area duplicates Input Field's State × isFilled schema with identical tokens (renamed under main/text-area/*). SwiftUI exposes multi-line via axis: .vertical on TextField; Compose exposes it via singleLine=false. Fold into Input Field with a multiline / lineLimit prop so the DS maps 1:1 to the native primitive.
In Context

Typical mobile contexts: feedback forms, message composers, notes, support request descriptions.

Live Preview

Toggle state and fill to see the text area update in real time.

Properties
state
isFilled
isExpandable
DS Health
Reusable
Warn
Works anywhere multi-line text is needed, but duplicates Input Field's State × isFilled schema. Reuse is better served by extending Input Field with a multiline flag than by shipping a parallel component.
Self-contained
Partial
Carries its own border, fill, and text styles per state. No label slot, no helperText/error-message slot, no characterCount slot — validation and labeling are pushed onto every consuming screen.
Consistent
Warn
isFilled=yes/no (C2, same anti-pattern Input Field already fixed). state=active instead of focused matches Input Field but diverges from the broader DS verb set. Tokens duplicated under main/text-area/* with identical values to main/input-field/*.
Composable
Partial
Nests in form layouts, but has no sibling wrapper (no Labeled Text Area) and no slot contract for icons or character count. The expand-icon frame holds a fixed raster glyph rather than a swappable node.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesstate=default1px #D7E0EF border, white bg. Resize glyph shown.
Active (Focused)YesYesstate=active2px #005CE5 border. Should rename to focused to match platform vocabulary.
ErrorYesYesstate=error2px #D61B2C border. No inline error-message slot.
DisabledYesYesstate=disabled#EEF2F9 bg, border hidden, text #C2CFE5.
Open Issues
  • Boolean property uses Yes/No.isFilled=yes/no cannot map to Swift Bool or Kotlin Boolean without a translation layer. Input Field already fixed this — Text Area should follow. C2 · Variant & Property Naming
  • Desktop resize-handle glyph baked into a mobile component. The text-area icon (12×12px, bottom-right) mirrors the browser-only resize: both affordance. Native TextField(axis: .vertical) / OutlinedTextField(maxLines=n) auto-grow without a user-facing handle — the glyph has no native equivalent and should not ship in mobile variants. C4 · Native Mappability
  • Resize handle is a raster PNG, once per state. Four separate PNG assets are referenced for the same 12×12px glyph. Even if the handle survives, it should be a single vector instance tinted by main/text-area/color/{state}/icon-resizer. C6 · Asset & Icon Quality
  • No label or helperText slot. Labels and validation messages are handled by the consumer, so every screen re-implements the anatomy. Native multi-line text fields expose both as first-class parameters. C5 · Interaction State Coverage
  • No characterCount / limit affordance. Multi-line entry is the canonical use case for character limits (comments, reviews, message composers). There is no count slot and no maxLength hook — the DS cannot represent limit state today. C5 · Interaction State Coverage
  • Tokens duplicated under main/text-area/*. Every value in main/text-area/color/* mirrors main/input-field/color/* exactly. If Text Area folds into Input Field, this namespace collapses; if it stays, the two token sets should alias a shared main/field/* collection to prevent drift. C1 · Layer Structure & Naming
  • Code Connect mappings not registered. Blocked by property naming and the multi-line-vs-Input Field decision. Cannot register until the family shape is finalized. C7 · Code Connect Linkability
Design Recommendations
  • Fold Text Area into Input Field as a multiline / lineLimit prop. SwiftUI models this as TextField(text:, axis: .vertical).lineLimit(3...6); Compose models it as OutlinedTextField(singleLine=false, maxLines=6). Both are the same primitive with a single flag. Mirroring that in Figma collapses 8 variants into 0 net new variants on Input Field (add a multiline boolean to the existing 8-variant matrix) and removes the duplicated token namespace. Family
  • Rename isFilled to use true/false. Same fix Input Field already shipped in 1.1.0. Required for Swift Bool / Kotlin Boolean mapping. Rename
  • Rename state=active to state=focused. Matches SwiftUI @FocusState and Compose FocusRequester vocabulary. Apply to Input Field and the whole Form Elements family at once so the rename lands once. Rename
  • Drop the desktop resize handle on mobile variants. If Text Area survives as a sibling, remove the expand-icon frame — it has no native equivalent on iOS/Android. Keep it only if a web/desktop DS variant is in scope, gated behind a platform axis. Asset
  • Add a helperText slot and a characterCount slot beneath the field. Multi-line is the canonical character-count surface. Expose a supporting-text row that can host either error copy, hint copy, or a count — matching Material 3's supportingText / counter pattern. If Text Area folds into Input Field, this slot lives on Input Field and serves both single- and multi-line. Slot
  • Alias main/text-area/* to a shared main/field/* collection. If the family stays split for any reason, the two token sets must reference a single source so border/bg/placeholder/disabled colors never drift. Preferable outcome: delete main/text-area/* outright after consolidation. Token
States

8 variants across 2 axes: state (default/active/error/disabled) × isFilled (yes/no). All share a 328px-wide container with 6px corner radius. Filled variants are 62px tall (2 lines of content); empty variants are 46px tall.

Default

Idle state with gray border. Resize-handle glyph sits in the bottom-right regardless of fill.

Properties
statedefault
isFilled
Active (Focused)

Focused state with 2px blue border. Rename target: focused.

Properties
stateactive
isFilled
Error

Validation error state with 2px red border. No inline error-message slot — copy is the consumer's responsibility.

Properties
stateerror
isFilled
Disabled

Non-interactive state with gray fill and muted text. Border hidden.

Properties
statedisabled
isFilled
Colors by State

All four color roles are bound to main/text-area/color/{state}/* tokens. Every value mirrors the equivalent main/input-field/color/* token — motivation for consolidation.

RoleTokenDEFAULTACTIVEERRORDISABLED
Bordertext-area/color/{state}/border#D7E0EF#005CE5#D61B2Chidden
Backgroundtext-area/color/{state}/bg#FFFFFF#FFFFFF#FFFFFF#EEF2F9
Text (filled)text-area/color/{state}/text#0A2757#0A2757#0A2757#C2CFE5
Placeholdertext-area/color/{state}/placeholder#C2CFE5#C2CFE5#C2CFE5#C2CFE5
Resize glyphtext-area/color/{state}/icon-resizer#D7E0EF#D7E0EF#D7E0EF#D7E0EF
Layout & Typography
PropertyValueToken
Width328px (fill)
Height (empty)46px
Height (filled, 2 lines)62px
Padding top10pxspace/space-10
Padding bottom8pxspace/space-8
Padding left12pxspace/space-12
Padding right8pxspace/space-8
Corner radius6pxradius/radius-2
Border (default)1px
Border (active/error)2px
Resize glyph12 × 12px
Text stylePrimary/Multi-line Label/Light/Small
FontProxima Soft Semiboldfont-family/font-primary
Size14pxfont-size/font-size-20
Line-height16pxline-height/leading-25
Tracking0.25letter-spacing/tracking-wide
Installation Planned API

Text Area is proposed to consolidate into EBInputField via a lineLimit / maxLines parameter. The install path below is shared with Input Field.

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.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
(text content)text: Binding<String>value: StringDerived from text content
isFilled (yes/no)Derived from text.isEmpty; not a native param
(multi-line default)axis: .verticalsingleLine=falseThe trait that makes this a "text area"
(auto-grow range).lineLimit(3...6)maxLines=6Replaces the desktop resize handle
state=defaultDefault idle state
state=active@FocusStateinteractionSourceKeyboard active
state=error.ebError(true)isError=trueValidation failed
state=disabled.disabled(true)enabled=falseNon-interactive
isExpandableDesktop-only resize handle; no native equivalent
SwiftUI (proposed consolidation)
ios/Components/FormElements/EBInputField.swift
Jetpack Compose (proposed consolidation)
android/components/form/EBInputField.kt
Usage Snippets Planned API
Multi-line (default)
EBInputField("Tell us more", text: $value, axis: .vertical)
    .lineLimit(3...6)
EBInputField(
    value = text,
    onValueChange = { text = it },
    placeholder = "Tell us more",
    singleLine = false,
    maxLines = 6
)
Error
EBInputField("Tell us more", text: $value, axis: .vertical)
    .lineLimit(3...6)
    .ebError(true)
EBInputField(
    value = text,
    onValueChange = { text = it },
    placeholder = "Tell us more",
    singleLine = false,
    maxLines = 6,
    isError = true
)
Disabled
EBInputField("Tell us more", text: $value, axis: .vertical)
    .lineLimit(3...6)
    .disabled(true)
EBInputField(
    value = text,
    onValueChange = { text = it },
    placeholder = "Tell us more",
    singleLine = false,
    maxLines = 6,
    enabled = false
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 x 44 pt (per-line height 22pt, container ≥44pt)48 x 48 dp
Accessibility label.accessibilityLabel("Comment")contentDescription
Error announcementVoiceOver reads error via .accessibilityValueTalkBack reads error via semantics { error() }
Character-count announcementAnnounce remaining via .accessibilityValue when a limit is setExpose via supportingText semantics
Usage Guidelines

Do

Use for free-form responses expected to exceed one line — feedback, comments, messages, notes.

Don't

Use for short structured inputs (name, phone, code) — Input Field's single-line default is more appropriate and faster to fill.

Do

Pair with a visible label above the field and a helper-text row below for character counts or format hints.

Don't

Rely on the desktop resize handle on mobile — mobile fields auto-grow within lineLimit/maxLines and the handle has no native behavior.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingPartialDuplicate token namespace main/text-area/* mirrors main/input-field/* exactly. Text layer structure is clean.
C2Variant & Property NamingFailisFilled=yes/no (same anti-pattern Input Field already resolved). state=active should be focused.
C3Token CoverageReadyAll colors bound to main/text-area/color/*. Spacing and radius tokens resolved.
C4Native MappabilityReworkExists as a distinct component but native platforms treat multi-line as a single TextField with axis: .vertical / singleLine=false. Desktop resize handle has no native equivalent.
C5Interaction State CoveragePartialAll 4 interaction states present. Missing slots: label, helper/error text, character count.
C6Asset & Icon QualityFailResize glyph is a raster PNG referenced four times (once per state) instead of a single vector instance.
C7Code Connect LinkabilityNot MappedBlocked by the consolidation decision and property renames.
Code Connect
AspectStatusNotes
Property namingFailisFilled=yes/no cannot map to native booleans
Component identityFailNative platforms have no standalone TextArea primitive; consolidation into Input Field is required first
Native component filePendingProposed target: EBInputField with multi-line flag
Variants Inventory (8 total)

4 state values × 2 isFilled values.

stateisFilledHeightNode ID
defaultyes62px3070:21242
defaultno46px3070:21239
activeyes62px3070:21243
activeno46px3070:21238
erroryes62px3070:21244
errorno46px3070:21240
disabledyes62px3070:21241
disabledno46px3070:21237
1.0.0 — April 2026 Initial
Initial Assessment · node 3070:21245
Component assessed — 8 variants documented across state (default/active/error/disabled) × isFilled (yes/no). Multi-line sibling of Input Field within the Form Elements group. Documented
Initial
Consolidation proposed — Fold Text Area into Input Field via a multiline / lineLimit prop to match SwiftUI axis: .vertical and Compose singleLine=false. Open
Family
Boolean property uses Yes/NoisFilled=yes/no instead of true/false. Same anti-pattern Input Field already resolved. Open
C2 Open
Desktop resize-handle glyph — 12×12px raster PNG in the bottom-right has no native mobile equivalent; iOS/Android auto-grow without a user-facing handle. Open
C4, C6 Open
Duplicate token namespacemain/text-area/color/* values mirror main/input-field/color/* exactly; candidate for aliasing or deletion after consolidation. Open
C1 Open
Code Connect mappings — No CLI mappings registered yet. Blocked by consolidation decision and property renames. Open
C7 Open
Title Bar Component link

An 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.

In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Send Money
Live Preview

Toggle properties to see the title bar update in real time.

Properties
leading icon
trailing icon
leading control
subtext
title block
DS Health
Reusable
Pass
Navigation title bar used across every screen in the app. Boolean property toggles cover all common configurations: back arrow, trailing action, subtext URL, CTA control, and large header block.
Self-contained
Pass
Carries its own status bar stub, title row, icon slots, subtext, and optional header block. Background color and all text/icon colors are token-bound. No external dependencies.
Consistent
Partial
Boolean properties use 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.
Composable
Pass
Fits as the top element on any screen. Nests below the system status bar. Content area sits directly below the title bar. Title block expands naturally when toggled on.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYes5 boolean propertiesNavigation 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.

Open Issues
  • Boolean properties use yes/no strings.leading icon, trailing icon, leading control, subtext, title block — all incompatible with Swift Bool / Kotlin Boolean for Code Connect. C2 · Variant & Property Naming
  • Trailing icon is a placeholder RECTANGLE. The 24×24 icon-placeholder blocks instance swap — native icon slot mapping can't be wired until this becomes a swappable Icon instance. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until boolean values and placeholder rectangles are fixed. C7 · Code Connect Linkability
Design Recommendations
  • Rename boolean values from yes/no to true/false. Direct native boolean mapping eliminates the string-to-bool layer when wiring Code Connect. Rename
  • Replace trailing icon placeholder with a real Icon instance. Drop in an actual icon from the DS library (e.g. more/ellipsis, share, search) so product teams can instance-swap without editing the master. Slot
  • Make leading control dependency explicit. Today it requires leading icon=yes and trailing icon=no — document it in the spec or promote it to a separate property so the constraint is enforced, not assumed. Docs
  • Add a dark / transparent variant. For screens with hero images or gradient backgrounds (home, campaign pages, onboarding), the current opaque bar is wrong — a transparent variant lets the hero breathe through. Property
  • Document the showAsset property. Currently only available when title block=yes; its purpose (background image behind the title) should be explicitly described in the component spec. Docs
Configurations

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
DESDEV

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.

Properties
leading icon
trailing icon
leading control
subtext
Properties
leading iconyes
trailing iconno
leading controlno
subtextno
title blockno
With Title Block
DESDEV

Title bar with expanded header block (72px) below the title row. Used for screens with prominent section headers. Adds "Header" text at 26px Semibold.

Properties
leading icon
trailing icon
leading control
subtext
Properties
leading iconyes
trailing iconno
leading controlno
subtextno
title blockyes
Colors by State

Single color scheme -- no appearance modes. All colors bound to main/title-bar/color/ tokens. Display/navigation component with no state-driven color changes.

RoleTokenValue
Backgroundmain/title-bar/color/bg#1972F9
Title labelmain/title-bar/color/label-title#FFFFFF
Header labelmain/title-bar/color/label-header#FFFFFF
Subtext / URLmain/title-bar/color/label-url#F6F9FDCC (80% opacity)
CTA textmain/title-bar/color/label-cta#FFFFFF
Iconmain/title-bar/color/icon#FFFFFF
Layout
PropertyValue
Status bar height44px
Title row padding H20px
Title row padding V12px
Leading icon size24 x 24
Trailing icon size24 x 24
Title block height72px
Title block padding H24px
Total height (no subtext, no block)~84px
Total height (with subtext, no block)~100px
Total height (with block)~156--172px
Typography
LayerText StyleFontSizeTrackingLine-height
TitlePrimary/Label/Light/BaseHeyMeow Rnd Semibold16px0.25px16px
SubtextPrimary/Label/Light/FineHeyMeow Rnd Semibold12px0.5px12px
HeaderPrimary/Headlines/Light/AreaHeyMeow Rnd Semibold26px0.85px31px
CTA (control)Primary/Label/Light/SmallHeyMeow Rnd Semibold14px0.25px14px
Installation Planned API

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.

Property Mapping
Figma PropertySwiftUIComposeNotes
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
SwiftUI
ios/Components/TitleBar/EBTitleBar.swift
Jetpack Compose
android/components/titlebar/EBTitleBar.kt
Usage Snippets Planned API
Basic (title only)
EBTitleBar("Send Money")
EBTitleBar(
    title = "Send Money"
)
With back arrow
EBTitleBar("Send Money")
    .ebLeadingIcon(Image(systemName: "arrow.left"))
EBTitleBar(
    title = "Send Money",
    leadingIcon = { Icon(Icons.Default.ArrowBack, "Back") }
)
Full configuration
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"
)
With leading control
EBTitleBar("Edit Profile")
    .ebLeadingIcon(Image(systemName: "arrow.left"))
    .ebLeadingControl("Done")
EBTitleBar(
    title = "Edit Profile",
    leadingIcon = { Icon(Icons.Default.ArrowBack, "Back") },
    leadingControlText = "Done"
)
Accessibility
RequirementiOSAndroid
Minimum touch target44 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 titlesemantics { heading() } on title
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic layer names: title, Title Bar, title-block, Leading Icon, Placeholder.
C2Variant & Property NamingNeeds RefinementAll 5 boolean properties use yes/no instead of true/false. leading control has implicit dependency on other properties.
C3Token CoverageReadyAll 6 color roles bound to main/title-bar/color/ tokens.
C4Native MappabilityReadyMaps to NavigationBar (iOS) / TopAppBar (Android, Material 3).
C5Interaction State CoverageReadyNavigation bar -- no interaction states needed beyond individual tap targets on icons and control.
C6Asset & Icon QualityNeeds RefinementTrailing icon uses icon-placeholder RECTANGLE instead of a swappable icon instance.
C7Code Connect LinkabilityNeeds RefinementNo CLI mappings registered yet.
Code Connect
AspectStatusNotes
Property namingNeeds FixAll booleans use yes/no -- must be renamed to true/false before Code Connect mapping
Asset qualityNeeds FixTrailing icon placeholder RECTANGLE needs replacement with icon instance
State coverageReadyNavigation bar -- no interaction states needed
Native component filePendingEBTitleBar.swift / EBTitleBar.kt not yet created
Variants Inventory (20 total)

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 blockCombinations coveredCount
no10 combos of leading/trailing icon + leading control + subtext10
yesSame 10 combos with title block enabled10
View full property combination breakdown (20 rows)
leading icontrailing iconleading controlsubtexttitle blockNode ID
nonononono23:175149
nononoyesno23:175365
noyesnonono23:175415
noyesnoyesno23:175427
yesyesnonono23:175377
yesyesnoyesno23:175389
yesnononono23:175487
yesnonoyesno23:175499
yesnoyesnono23:175449
yesnoyesyesno23:175461
nonononoyes23:175159
nononoyesyes23:175169
noyesnonoyes23:175179
noyesnoyesyes23:175189
yesyesnonoyes23:175199
yesyesnoyesyes23:175209
yesnononoyes23:175219
yesnonoyesyes23:175229
yesnoyesnoyes23:175239
yesnoyesyesyes23:175249
1.0.0 -- April 2026 Initial
Initial Assessment -- node 23:175148
Component assessed -- 20 variants documented across 5 boolean properties: leading icon, trailing icon, leading control, subtext, title block. App navigation title bar with brand blue background and white text/icons. All colors bound to main/title-bar/color/ tokens. Documented
Initial
Boolean properties use yes/no -- All 5 boolean properties (leading 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. Open
C2 Open
Trailing icon uses placeholder RECTANGLE -- icon-placeholder is a 24x24 RECTANGLE instead of a swappable icon instance from the DS icon library. Blocks native icon slot mapping. Open
C6 Open
Code Connect mappings -- No CLI mappings registered yet. Blocked by C2 (boolean naming) and C6 (placeholder icon). Open
C7 Open
Toast - With Button ConsolidateReworkComponent link

A transient overlay notification with an inline action button. 4 variants across Type (Default / Light) × Description (yes / no). Modeled as a sibling of the base Toast solely to add the button — which should be an optional action slot on Toast itself. Width 330 (not 312 like base Toast), action surface uses the deprecated Button - Small/XS instance.

Consolidate — fold into the base Toast
Remove this component from the family. Base Toast picks up action?: EBToastAction (label + callback) and supportingText?: String (the 10/15 BarkAda second line). Align width with base Toast (312 vs. 330 today) and swap the deprecated Button - Small/XS for Button - XSmall. Covers the "Undo / Retry / View" use cases and collapses two components into one.
In Context

The actionable toast appears after reversible operations — "Transfer sent · Undo", "Message failed · Retry", "Photo uploaded · View". The action button sits right-aligned, tappable without dismissing the toast. Auto-dismiss is suppressed while an action is present.

Live Preview

The description toggle switches between a two-line toast (label + supporting text, 12 px vertical padding) and a single-line toast (label only, 8 px vertical padding). Action button appearance flips with type: white-on-navy for default, blue-on-white for light.

Content
label
description
action
Properties
Type
Description
DS Health
Reusable
Partial
Drops into reversible-action moments (Undo, Retry, View). But the narrow axis set (no error, no pending, no icon) means it can't replace the base Toast for most feedback moments — consumers pick the wrong component half the time.
Self-contained
Warn
Embeds the .[DEPRECATED] Button - Small/XS instance (scheduled for deletion). When that source is removed, this component breaks. Owns its surface tokens, but its action surface is borrowed from a deprecated source.
Consistent
Fail
Exists as a parallel component for what should be a property on Toast. type=default|light here vs. base Toast's type=default|pending|error and theme=default|light|dark — same axis name, incompatible value sets. Width is 330; base Toast is 312.
Composable
Warn
The action is baked in as a fixed Button instance — consumers can't swap it for a text-only link, an icon button, or disable/load it. A real slot would let consumers compose the action they need.
Behavior
BehavioriOSAndroidFigma SpecNotes
Action tapYesYesButton instance embeddedThe deprecated Button - Small/XS is the action surface. Only one fixed appearance; consumers can't disable or show a loading spinner on the action.
Whole-container tapAmbiguousAmbiguousRoot is a <button> in some variantsThe default, description=no variant wraps the whole toast in a button element, overlapping the inner action — tap target is unclear.
Auto-dismiss when action presentMissingMissingNot annotatedToasts with actions should stay visible until the action is taken or explicitly dismissed. Not spec'd.
Action loading / disabled stateMissingMissingNot modeled"Retry" actions often need a loading state after tap. No provision in the component.
A11y — action labelGenericGenericText "Label"The default text in the instance is literally "Label". Needs a content contract and an accessibility label override.
Open Issues
  • Separate component for a button slot that should be a prop on the base Toast. Material's Snackbar, SwiftUI's .alert(actions:), and every other mature DS handle this as an optional action parameter — not a sibling record. Maintaining two components doubles the surface area of every future change and invites drift (different widths, different type sets). C1 · Layer Structure & Naming
  • Axis names + values drift from the base Toast. Base Toast exposes type=default | pending | error + theme=default | light | dark. This sibling exposes type=default | light — same axis name, narrower value set, collides on meaning. Consumers wiring Code Connect can't treat them as the same prop. C2 · Variant & Property Naming
  • description=yes|no is a slot + sizing flag bundled into a string. When description is present, vertical padding grows from 8 to 12 and a 4 px gap is inserted below the label. The trigger should be the presence of supporting-text content (supportingText?: String), not a hard-coded variant. C2 · Variant & Property Naming
  • Action surface uses the deprecated Button - Small/XS. The embedded Button instance (node 21:164490) is marked DEPRECATED in Figma, slated for deletion. When that source is removed, every variant of this toast breaks. Must re-link to Button - XSmall. C4 · Native Mappability
  • No icon axis at all. Base Toast has With Icon=yes | no. This sibling drops the axis entirely — you can't have an actionable toast with a leading checkmark or error glyph. Merging into Toast recovers the icon automatically. C6 · Asset & Icon Quality
  • Whole-container tap overlaps the action button. The default, description=no variant wraps the entire toast in a <button> element, while the inner action is also a button — two conflicting tap targets stacked. Behavior is undefined when the user taps the non-action area. C5 · Interaction State Coverage
  • Action has no states. No pressed, disabled, or loading state on the action. "Retry" actions commonly need a spinner after tap; destructive "Undo" often needs to grey out during processing. Not modeled. C5 · Interaction State Coverage
  • Width 330 vs. base Toast 312. Inconsistent with the base Toast's fixed width. A single consolidated component picks one width (recommend 312, matching the rest of the family). C1 · Layer Structure & Naming
  • Code Connect mappings not registered. Blocked on consolidation — there should be no separate Code Connect entry; the action slot maps to the base Toast's action parameter. C7 · Code Connect Linkability
Design Recommendations
  • Consolidate into the base Toast. Remove Toast - With Button from the family. Base Toast picks up two new optional slots: supportingText?: String (the 10/15 BarkAda second line) and action?: EBToastAction (label + callback, with optional loading and disabled states). One component, covers every use case today and the ones this sibling misses (actionable error, actionable with icon). Family
  • Migrate the action surface to Button - XSmall. The embedded Button - Small/XS is marked deprecated in Figma. Rebind the action instance to the canonical Button - XSmall before the deprecated source is deleted — otherwise every variant breaks. Composition
  • Replace description=yes|no with a supporting-text slot. Promote the second text line to an optional content slot. The padding and gap changes follow from the slot being populated — no duplicate variants required. Slot
  • Normalize width to 312. Match the base Toast. One width across the family. Property
  • Define the action's state contract. Spec pressed / disabled / loading states on the action slot so "Retry" and "Undo" flows can reflect processing state. State
  • Resolve the whole-container tap conflict. Decide: either the toast is dismiss-on-tap (drop the action as the only interactive surface), or the action owns the only tappable region. Pick one and drop the root <button> wrapper from the other variants. State
  • Document auto-dismiss suppression when an action is present. A toast with an action stays visible until the user taps the action or explicitly dismisses. Call this out as a usage note. Docs
  • Document the A11y announcement mapping. Action label feeds accessibilityLabel (iOS) / contentDescription (Android). Live region polite for neutral, assertive if consolidated into a destructive Toast. A11y
Default — with description node 813:31117

The two-line dark toast. Label (14 px bold) above supporting text (10 px BarkAda Medium, 80% white), action button right-aligned and bottom-aligned. Used for reversible success moments that need more context — "Transfer sent · to Juan Dela Cruz".

Properties
Typedefault
Descriptionyes
ActionButton - Small/XS (deprecated)
Colors by Type
TYPEROLETOKENVALUE
Defaultbgdefault/bg#0A2757
labeldefault/label#FFFFFF
descriptiondefault/description#F6F9FDCC
borderdefault/border#E5EBF4
Lightbglight/bg#FFFFFF
labellight/label#0A2757
descriptionlight/description#445C85
borderlight/border#E5EBF4
Action (both)button bg (default/light)button-v1/default/background#005CE5
button bg (on dark)button-v1/default/background-primary#FFFFFF
Layout
Width330
Padding H16
Padding V (description=yes)12
Padding V (description=no)8
Corner radius8
Border1 solid token
Gap (text ↔ button)24
Gap (label ↔ description)4
Button height24
Button padding16 × 8/7
Button radius99 (pill)
Typography
Label stylePrimary/Multi-line Label/Small
Label fontProxima Soft Bold
Label size14 / 16 · +0.25
Description styleSecondary/Default/Small Caption
Description fontBarkAda Medium
Description size10 / 15 · +0
Button label stylePrimary/Label/Small
Button fontProxima Soft Bold
Button size14 / 14 · +0.25
Light — with description node 27:53213

Same two-line layout, inverted surface. White bg, navy label, slate supporting text, blue-on-white action button. Used on dark backgrounds or when the surrounding screen is already high-contrast.

Properties
Typelight
Descriptionyes
Action buttonvariant=default (blue)
Layout overrides
Width330
Padding16 × 12
Border1 · #E5EBF4
Action fg/bg#FFFFFF / #005CE5
Default — no description node 813:31125

The compact single-line toast with action. 8 px vertical padding, label only. Used for short reversible actions — "Copied · Undo".

Properties
Typedefault
Descriptionno
Root element<button> (whole container)
Layout overrides
Width330
Padding16 × 8
Gap (text ↔ button)24
Action offset66 px fixed slot
Light — no description node 27:53225

The compact light-surface toast. Single-line label, blue action button, white bg.

Properties
Typelight
Descriptionno
Root element<div> (non-interactive outer)
Typography
Label stylePrimary/Multi-line Label/Small
Label fontProxima Soft Bold
Label size14 / 16 · +0.25
Installation Planned API

After consolidation this component is absorbed by the base Toast. The install + import remain the same; the action becomes a parameter.

.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBToast
import com.gcash.designsystem.components.EBToastAction
Property Mapping (Proposed — via Base Toast)

This component collapses into the base Toast. Figma properties map to the consolidated Toast API, not to a standalone EBToastWithButton.

Figma (today)Figma (proposed — on Toast)SwiftUICompose
Type: default | lighttheme: light | dark (shared with Toast).ebToastTheme(.dark)theme: EBToastTheme
Description: yes | nosupportingText?: String (slot)supportingText: String?supportingText: String?
(embedded Button - Small/XS — deprecated)action?: ToastAction (slot)action: EBToastAction?action: @Composable (() -> Unit)?
(implicit label text)message: Stringmessage: Stringmessage: String
(no icon axis)leadingIcon?: Icon (inherited from Toast)leadingIcon: Image?leadingIcon: @Composable (() -> Unit)?
Suggested file paths
  • ios/Components/Toast/EBToast.swift — absorbs the action slot
  • ios/Components/Toast/EBToastAction.swift — action model (label + onTap + optional state)
  • android/components/toast/EBToast.kt — absorbs the action slot (wraps Material 3 Snackbar)

No dedicated EBToastWithButton file — the component is removed from the family after consolidation.

Usage
// Undo pattern — single-line, dark surface
EBToast(
    message: "Message deleted",
    action: EBToastAction("Undo", onTap: undo)
)
.ebToastTheme(.dark)

// Retry pattern — with supporting text, light surface
EBToast(
    message: "Couldn't send money",
    supportingText: "Check your connection and try again.",
    action: EBToastAction("Retry", onTap: retry)
)
.ebToastTheme(.light)

// Action with loading state (after tap)
EBToast(
    message: "Couldn't send money",
    action: EBToastAction("Retry", isLoading: isRetrying, onTap: retry)
)
// Undo pattern — single-line, dark surface
EBToast(
    message = "Message deleted",
    theme = EBToastTheme.Dark,
    action = { EBToastAction("Undo", onClick = onUndo) }
)

// Retry pattern — with supporting text, light surface
EBToast(
    message = "Couldn't send money",
    supportingText = "Check your connection and try again.",
    theme = EBToastTheme.Light,
    action = { EBToastAction("Retry", onClick = onRetry) }
)

// Action with loading state
EBToast(
    message = "Couldn't send money",
    action = {
        EBToastAction(
            "Retry",
            isLoading = isRetrying,
            onClick = onRetry
        )
    }
)
Accessibility
RequirementiOSAndroid
Action labelAction passes accessibilityLabel through to the inner Button. Default: the visible label.Action passes contentDescription to the inner Button. Default: the visible label.
Live regionPolite announcement; use the Toast's appearance to decide (destructive → assertive).LiveRegionMode.Polite by default; assertive for destructive.
Suppress auto-dismissWhen action is non-nil, host overlay keeps the toast on screen until the action is tapped or the user swipes.SnackbarDuration.Indefinite when an action is present.
Action loading stateSwap label for a ProgressView; keep the button reachable for VoiceOver (don't disable mid-announcement).Swap label for a CircularProgressIndicator; set enabled=false after the state is announced.
Tap targetAction button must have a ≥ 44 × 44 hit area; the 24 px pill extends via .contentShape if visual size is smaller.Action button must have a ≥ 48 dp touch target; use Modifier.minimumInteractiveComponentSize().
Usage Guidelines
Do
  • Use the action slot for reversible operations: Undo, Retry, View.
  • Keep the action label to one word where possible (≤ 10 characters).
  • Suppress auto-dismiss while an action is visible — let the user decide.
  • Show a loading state on the action button during async retries.
Don't
  • Don't use an action toast for destructive operations that can't be reversed.
  • Don't stack more than one action in the slot — use a Modal if you need multiple.
  • Don't make the entire toast tappable while an action is present — conflicts with the action's hit target.
  • Don't use the deprecated Button - Small/XS instance. Rebind to Button - XSmall before consolidation.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkSeparate component for a property slot. Consolidate into base Toast; normalize width from 330 to 312.
C2Variant & Property NamingReworktype values drift from base Toast; description=yes|no should be a content slot.
C3Token CoverageReadySurface, label, description, border tokens all bound via main/toast/color/{default|light}/*. Action uses comp/button-v1/default/*.
C4Native MappabilityReworkAction surface uses the deprecated Button - Small/XS; maps cleanly to Snackbar's action slot once migrated.
C5Interaction State CoverageReworkNo action states (pressed / disabled / loading); whole-container tap overlaps the inner action.
C6Asset & Icon QualityReworkNo leading-icon axis at all — consolidation recovers it from base Toast.
C7Code Connect LinkabilityNot MappedBlocked on consolidation — no standalone Code Connect entry; action maps to base Toast's action parameter.
Variants Inventory (4 total)

Axes: Type (2) × Description (2)=4 variants. Flat matrix — no collapsed axes, no illegal combinations. Every variant embeds the deprecated .[DEPRECATED] Button - Small/XS instance.

#NodeTypeDescriptionDimensionsNotes
1813:31117defaultyes330 × 74Root is a <button> element
227:53213lightyes330 × 74Root is a <div> element
3813:31125defaultno330 × 41Root is a <button> element (tap conflict)
427:53225lightno330 × 41Root is a <div> element
1.0.0 — April 2026 Initial
Initial Assessment · node 27:53205
Verdict: Consolidate — Fold into the base Toast. Action becomes an optional slot; supporting text becomes an optional content slot. Remove this sibling from the family. Open
Family
C1 — Duplicate component — Exists to add an action slot to Toast. Collapse into base Toast with action?: EBToastAction. Width drifts from base (330 vs. 312). Open
C1
C2 — Axis drifttype=default|light here vs. type=default|pending|error + theme=default|light|dark on base Toast. description=yes|no should be supportingText?: String. Open
C2
C4 — Deprecated Button embedded — Action surface uses .[DEPRECATED] Button - Small/XS (node 21:164490), slated for deletion Aug 22, 2025. Rebind to Button - XSmall before migration. Open
C4
C5 — Tap conflict + missing action statesdescription=no variants wrap the root in a <button> overlapping the inner action. No pressed/disabled/loading states for the action. Open
C5
C6 — No icon axis — Silently drops the With Icon axis that base Toast exposes. Consolidation recovers it. Open
C6
C7 — Code Connect — Blocked on family consolidation. No standalone entry expected. Open
C7
Toast RestructureReworkComponent link

A transient overlay notification. 16 variants spanning Type (Default / Pending / Error) × Theme (Default / Light / Dark) × With Icon × Large Label. Floats above the UI to acknowledge an action, surface an error, or indicate pending work — auto-dismisses after a short duration. Sibling component Toast - With Button models the same primitive with an inline action; the two should consolidate.

Restructure — collapse the family and clean up the axes
Merge Toast + Toast - With Button into one component with an optional action slot. Split the overloaded theme axis into appearance=neutral | destructive | pending + theme=light | dark. Replace Large Label with size=small | base. Replace the Pending placeholder circle with a real spinner instance. Add a dismiss contract (swipe + auto-duration).
In Context

Toasts float over the app screen — not inline with content. Success toasts confirm completed actions ("Transfer sent"), pending toasts acknowledge background work ("Uploading…"), and error toasts surface failures that don't block the flow. They auto-dismiss after ~3 seconds unless swiped.

Live Preview

Type into the message to stress-test the layout. The theme dropdown gates the type options: Error only pairs with theme=default (its destructive surface), while Default / Pending pair with dark or light. That coupling is an Open Issue — see Design Recommendations.

Content
message
Properties
Type
Theme
With Icon
Large Label
DS Health
Reusable
Pass
Drops into any transient-feedback moment — transfers, uploads, validation errors. Not tied to a specific screen.
Self-contained
Warn
Owns its colors and typography, but the Pending type ships a 16/24 icon-placeholder gray circle instead of a real spinner — consumers can't drop in a live progress indicator without editing the master.
Consistent
Warn
The theme axis mixes appearance (light/dark surface) with semantic status — Error forces theme=default and loses the dark/light choice. Large Label is really a size axis phrased as a content flag.
Composable
Warn
No action slot — a sibling component (Toast - With Button) exists just to add one button. Action should be an optional slot on this component, not a separate family member.
Behavior
BehavioriOSAndroidFigma SpecNotes
Show / auto-dismissYesYesNot modeledToasts appear on a host overlay and auto-dismiss after a duration (~3s short, ~5s long). Host-screen concern — no visual state variant needed.
Swipe to dismissMissingMissingNot annotatedStandard gesture on both platforms. Not called out in the component spec.
Tap to dismissMissingMissingNot annotatedPending variants already wrap the container in a button element in the Figma code — but no interaction callback is documented.
Pending spinner animationPlaceholderPlaceholderGray circlePending icon is a static gray circle (icon-placeholder) — should be an animated spinner (ProgressView / CircularProgressIndicator).
A11y announcementImplicitImplicitNot annotatedError toasts should announce as assertive; default/pending as polite. Not spec'd.
Open Issues
  • Two components model one primitive. Toast + Toast - With Button differ only in the presence of an action button. Action belongs on this component as an optional slot, not a separate family member. Doubling the surface area of every future change. C1 · Layer Structure & Naming
  • theme axis overloaded with status. Values are default | light | dark but default is only valid when type=error (it paints the destructive red surface). Real axes are appearance=neutral | destructive | pending × theme=light | dark. Current schema blocks light/dark error variants. C2 · Variant & Property Naming
  • Large Label is a size flag, not a content flag. The two values change padding, font size, and line-height — this is a size axis. Rename to size=small | base (or compact / regular) so the schema reads correctly. C2 · Variant & Property Naming
  • Booleans use yes/no strings.With Icon / Large Label — blocks direct Swift Bool / Kotlin Boolean mapping. C2 · Variant & Property Naming
  • Pending type uses a placeholder icon. 16 × 16 (small) and 24 × 24 (base) gray icon-placeholder circles instead of an animated spinner. No instance of a ProgressView / CircularProgressIndicator — native consumers can't wire up a real loading state. C6 · Asset & Icon Quality
  • No swipe-to-dismiss or auto-duration contract. Standard toast behaviors on both iOS and Android. Neither is annotated in the Figma component spec — engineers have to guess the duration budget and gesture handling. C5 · Interaction State Coverage
  • No native primitive mapping documented. SwiftUI has no first-party toast (pre-iOS 17); Compose uses SnackbarHost + Snackbar. The component doesn't call out either mapping — dev-handoff guessing game. C4 · Native Mappability
  • Code Connect mappings not registered. Blocked on family consolidation and axis cleanup. C7 · Code Connect Linkability
Design Recommendations
  • Consolidate Toast + Toast - With Button. Target schema: EBToast(message, appearance=.neutral | .destructive | .pending, theme=.light | .dark, size=.small | .base, leadingIcon?: Icon, action?: EBToastAction). Remove the With Button component from the family. Family
  • Split theme into appearance + theme.appearance=neutral | destructive | pending controls semantic status + surface palette; theme=light | dark controls the neutral-surface contrast mode. Unlocks light/dark destructive variants and matches every other DS component's mental model. Property
  • Rename Large Label to size=small | base. Matches the actual axis (padding, font size, spacing) and mirrors Button / Alert sizing. Rename
  • Normalize booleans to true/false.With Icon and any remaining flags. Then rename With Icon to leadingIcon once promoted to a Slot. Rename
  • Replace Pending placeholder with a real spinner. Instance-swap a ProgressView / CircularProgressIndicator (or the DS Spinner component when it ships) into the leading-icon slot for appearance=pending. Same slot covers the checkmark for appearance=neutral and the X for appearance=destructive. Asset
  • Promote the leading icon to a swappable Slot. Adopt Figma Slots so consumers can drop in any Icon from the DS icon library. Default slot content per appearance: checkmark / spinner / error glyph. Slot
  • Add an optional action slot.action?: EBToastAction — takes label + callback. Covers the "Undo", "Retry", "View" use cases and replaces the need for Toast - With Button entirely. Slot
  • Document duration + dismiss behavior. Annotate default duration (3s short / 5s long), swipe-to-dismiss, tap-to-dismiss, and the onDismiss callback contract in the component spec. Not a visual variant — a usage note. Docs
  • Document the A11y live-region mapping. Error toasts announce as assertive (UIAccessibility .high / LiveRegionMode.Assertive); default + pending as polite. Spell this out so engineers wire the right roles. A11y
Default / Dark — with icon, large label node 27:53136

The canonical success toast. Dark navy surface, white text, leading checkmark icon. Used across the app to confirm completed actions (transfers, settings saved, uploads done).

Properties
Typedefault
Themedark
With Iconyes
Large Labelyes
Colors by Appearance Mode
MODEROLETOKENVALUE
Default (dark)bgdefault/bg#0A2757
labeldefault/label#FFFFFF
icondefault/icon#FFFFFF
Lightbglight/bg#FFFFFF
labellight/label#0A2757
iconlight/icon#0A2757
Destructive (error)bgdestructive/bg#D61B2C
labeldestructive/label#FFFFFF
icondestructive/icon#FFFFFF
Border (all modes){mode}/borderneutral #E5EBF4 · destructive #F4C7C9
Layout
Width312
Padding H12
Padding V (large)12
Padding V (small)0 (12 on inner offset)
Corner radius8
Border1 solid token
Shadow0 1 3 rgba(232,238,242,.79)
Gap (icon ↔ label)8
Icon size (large)24 × 24
Icon size (small)16 × 16
Icon offset top (large)7
Typography
Style (large)Primary/Label/Light/Small
Style (small)Primary/Multi-line Label/Light/Fine
FontProxima Soft Semibold
Size (large)14 / 14 · +0.25
Size (small)12 / 14 · +0.5
Error — with icon, large label node 27:53154

Destructive surface for errors. Red bg, white label, leading X glyph. Only paints when theme=default — the current schema doesn't allow a light/dark error variant.

Properties
Typeerror
Themedefault
With Iconyes
Large Labelyes
Layout overrides
Width312
Padding12 × 12
Corner radius8
Border1 · #F4C7C9
Icon glyphClose (X)
Icon size24 × 24
Pending — with icon, large label node 3424:1308

Acknowledges in-flight work. Ships a placeholder gray circle today (icon-placeholder) — should be an animated spinner. Container is wrapped in a <button> element in the Figma code, suggesting tap-to-dismiss is intended but not documented.

Properties
Typepending
Themedark
With Iconyes
Large Labelyes
Icon asset
Currenticon-placeholder circle
Fill#C2C6CF (static)
TargetProgressView / CircularProgressIndicator
Size (large)24 × 24
Size (small)16 × 16
Default — no icon, small label node 27:53196

The compact, text-only toast. No icon, smaller type (12/14, tracking +0.5). Used when the message is short and the context is unambiguous ("Copied", "Saved").

Properties
Typedefault
Themedark
With Iconno
Large Labelno
Typography
StylePrimary/Multi-line Label/Light/Fine
FontProxima Soft Semibold
Size12 / 14 · +0.5
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBToast
Property Mapping (Proposed)

The proposed schema collapses Toast + Toast - With Button into one API. Action becomes an optional slot, theme splits into appearance + theme, and Large Label becomes size.

Figma (today)Figma (proposed)SwiftUICompose
Type: default | pending | errorappearance: neutral | pending | destructiveappearance: EBToastAppearanceappearance: EBToastAppearance
Theme: default | light | dark (overloaded)theme: light | dark (neutral + pending only).ebToastTheme(.dark)theme: EBToastTheme
Large Label: yes | nosize: small | base.controlSize(.small / .regular)size: EBToastSize
With Icon: yes | noleadingIcon?: Icon (slot)leadingIcon: Image?leadingIcon: @Composable (() -> Unit)?
(implicit)message: Stringmessage: Stringmessage: String
(separate component)action?: ToastActionaction: EBToastAction?action: @Composable (() -> Unit)?
(not modeled)duration: short | longduration: EBToastDurationduration: EBToastDuration
(not modeled)onDismiss?: () -> VoidonDismiss: (() -> Void)?onDismiss: (() -> Unit)?
Suggested file paths
  • ios/Components/Toast/EBToast.swift
  • ios/Components/Toast/EBToastManager.swift — presentation overlay
  • android/components/toast/EBToast.kt — wraps Material 3 SnackbarHost
Usage
// Neutral success toast — most common form
EBToast(
    message: "Transfer completed",
    appearance: .neutral,
    leadingIcon: Image("checkmark.circle")
)
.ebToastTheme(.dark)

// Pending — spinner instead of static icon
EBToast(
    message: "Uploading…",
    appearance: .pending
)

// Error with retry action (absorbs Toast - With Button)
EBToast(
    message: "Couldn't send money",
    appearance: .destructive,
    action: EBToastAction("Retry", onTap: retry)
)

// Presentation — shown via manager overlay
EBToastManager.shared.present(toast, duration: .short)
// Neutral success toast
EBToast(
    message = "Transfer completed",
    appearance = EBToastAppearance.Neutral,
    theme = EBToastTheme.Dark,
    leadingIcon = { Icon(EBIcons.CheckCircle, contentDescription = null) }
)

// Pending — animated spinner
EBToast(
    message = "Uploading…",
    appearance = EBToastAppearance.Pending,
    theme = EBToastTheme.Dark
)

// Error with retry action
EBToast(
    message = "Couldn't send money",
    appearance = EBToastAppearance.Destructive,
    action = { EBToastAction("Retry", onClick = onRetry) }
)

// Presentation — wraps SnackbarHostState
snackbarHostState.showSnackbar("Transfer completed")
Accessibility
RequirementiOSAndroid
Live region — errorPost UIAccessibility.Notification.announcement with .high priority on present.Modifier.semantics { liveRegion=LiveRegionMode.Assertive } on the Snackbar container.
Live region — neutral / pendingPost announcement with default priority.LiveRegionMode.Polite.
Minimum durationShort ≥ 3s, long ≥ 5s; extend for longer messages per iOS HIG.SnackbarDuration.Short / Long (Material 3 defaults).
Action button labelAction slot owns its own accessibilityLabel.Action slot owns its own contentDescription.
Dismiss gestureSwipe horizontally to dismiss; respect reduce-motion for the slide-out animation.Swipe to dismiss built into Snackbar; honor TalkBackUserTouchExplorationEnabled to extend duration.
Usage Guidelines
Do
  • Use toasts for transient confirmation — success, error, and in-flight work.
  • Keep the message to one line (or two lines max for the small size).
  • Use the action slot for reversible operations (Undo, Retry, View).
  • Pair destructive toasts with assertive live-region announcements.
Don't
  • Don't use toasts for blocking errors — use a Modal or Alert instead.
  • Don't stack more than one toast at a time — queue them.
  • Don't use toasts for field-level validation — use the field's error state.
  • Don't auto-dismiss a toast that has an action — keep it visible until the action is taken or explicitly dismissed.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkTwo components for one primitive — consolidate with Toast - With Button.
C2Variant & Property NamingReworktheme overloaded with status; Large Label is a size flag; booleans on yes/no.
C3Token CoverageReadyAll colors bound to main/toast/color/{mode}/*. Spacing + typography fully tokenized.
C4Native MappabilityReworkNo SwiftUI first-party primitive; Compose has Snackbar. Needs documented mapping + ToastManager pattern.
C5Interaction State CoverageReworkNo auto-duration, swipe, or tap-to-dismiss contract; pending has no animation.
C6Asset & Icon QualityReworkPending uses icon-placeholder gray circle instead of a real spinner.
C7Code Connect LinkabilityNot MappedBlocked on family consolidation and axis cleanup.
Variants Inventory (16 total)

Effective axes today: Type (3) × Theme (3, coupled to Type) × With Icon (2) × Large Label (2). Built variants collapse the illegal combinations — Error only pairs with theme=default; Pending only pairs with theme=dark | light + with icon=yes.=16 built variants.

GroupCountAxes
Default / Dark4withIcon=yes/no × largeLabel=yes/no
Default / Light4withIcon=yes/no × largeLabel=yes/no
Error / Default4withIcon=yes/no × largeLabel=yes/no
Pending / Dark2withIcon=yes (forced) × largeLabel=yes/no
Pending / Light2withIcon=yes (forced) × largeLabel=yes/no
View full breakdown (16 rows)
#NodeTypeThemeWith IconLarge LabelDimensions
127:53136defaultdarkyesyes312 × 38
23424:1308pendingdarkyesyes312 × 38
327:53145defaultlightyesyes312 × 38
43424:1336pendinglightyesyes312 × 38
527:53154errordefaultyesyes312 × 38
627:53163defaultdarknoyes312 × 38
727:53166defaultlightnoyes312 × 38
827:53169errordefaultnoyes312 × 38
927:53172defaultdarkyesno312 × 38
103424:1386pendingdarkyesno312 × 38
1127:53180defaultlightyesno312 × 38
123424:1392pendinglightyesno312 × 38
1327:53188errordefaultyesno312 × 38
1427:53196defaultdarknono312 × 38
1527:53199defaultlightnono312 × 38
1627:53202errordefaultnono312 × 38
1.0.0 — April 2026 Initial
Initial Assessment · node 27:53135
Verdict: Restructure — Consolidate with Toast - With Button, split the overloaded theme axis, rename Large Label to size, and replace the Pending placeholder with a real spinner. Open
Schema
C1 — Family duplication — Toast + Toast - With Button model one primitive; merge via optional action slot. Open
C1
C2 — Axis overloadtheme mixes appearance + status; Large Label is a size flag; booleans on yes/no. Open
C2
C6 — Pending placeholder — 16/24 gray icon-placeholder circle; adopt a real spinner instance. Open
C6
C5 — Dismiss + duration — No auto-dismiss, swipe, or tap-to-dismiss documented. Open
C5
C4 — Native mapping — Document ToastManager overlay (iOS) + SnackbarHost wrapper (Android). Open
C4
C7 — Code Connect — Blocked on family consolidation + schema cleanup. Open
C7
Toggle - With Label RestructureReworkComponent link

A labeled toggle — the most common form of Toggle in settings and forms. Today this is a single layout frame (180 × 24) containing a Toggle instance and a "Label" text layer, with no properties, no variants, and no slots. Needs to become a proper component matching the shape already established by Radio Button With Label.

Restructure — promote from frame to real component
Add a property set: label, optional description, optional helper/error text, required marker, placement=leading | trailing, and inherit Toggle's state + size from the inner Toggle instance. Once built, drop-in for settings rows, form opt-ins, and list items.
In Context

Labeled toggle is the primary form of Toggle shown in product. Settings rows, feature opt-ins, biometric/notification preferences — nearly all consumer-facing toggles are labeled.

Live Preview (Proposed)

This preview shows the proposed component shape — the current Figma is just a layout frame, so there are no real properties to toggle.

Click the toggle to flip it — mirrors Figma's Toggle variant swap (isActive=Yes ↔ No). Type in the label or description to see how the layout handles different copy lengths. Since Toggle-With-Label is a layout frame today (not a real component), every property here is proposed.

Content
label
description
Properties (proposed)
isSelected
state
placement
required
helper
DS Health
Reusable
Fail
Not reusable in its current form — consumers can't set the label, can't add description, helper, or required marker. They must detach and rebuild.
Self-contained
Fail
No properties, no slots. Just a frame with a static Toggle + text.
Consistent
Fail
Breaks the pattern established by Radio Button With Label (real component with label + description) and Labeled Field.
Composable
Warn
Child Toggle is an instance — at least the composition is correct. But the wrapper has no slot / property surface to expose.
Behavior
StateiOSAndroidFigma SpecNotes
DefaultYesYesFrame onlyToday: one static instance. Proposed: Toggle + label rendered in row.
PressedMissingMissingNot builtTapping label should also toggle — entire row is the tap target.
DisabledMissingMissingNot builtLabel dims to secondary when toggle is disabled.
ErrorMissingMissingNot builtRequired toggle + form submit shows error text below label.
Open Issues
  • Not a real component. Layout frame with a Toggle + text, no properties or variants. Promote to a proper component. C1 · Layer Structure & Naming
  • No slots. Cannot set label, add description, mark required, or show helper/error. C2 · Variant & Property Naming
  • No placement option. iOS Form convention is trailing toggle; Material 3 allows either. Need placement=leading | trailing. C4 · Native Mappability
  • No state coverage. Pressed row, disabled label, error visual all missing. C5 · Interaction State Coverage
  • No Code Connect mapping. Blocked until component exists. C7 · Code Connect Linkability
Design Recommendations
  • Build as a real component with property set: label, description?, helper?, error?, required: boolean, placement=leading | trailing. Inherit isSelected, State, Size from the inner Toggle via instance-swap. Composition
  • Match Radio Button With Label's shape for consistency — same label/description layout, same required marker, same error styling. Selection controls should read alike. Family
  • Whole-row tap target. Tapping the label or the description should toggle the switch — the entire row is the hit region. Matches iOS Form and Material 3 list-item behavior. State
  • Consider a List Row wrapper that adds full-width chrome (dividers, padding) for use inside Settings screens. Today this shape would be built ad-hoc per screen; a dedicated variant makes Settings screens trivially composable. Family
  • See:Toggle for the base control, Radio Button With Label for the target shape. Family
Today — single frame node 18482:36538

A 180×24 layout frame: Toggle instance on the left of its auto-layout, "Label" text on the right. No property set, no variants — functionally identical to placing a Toggle + Text next to each other on a canvas.

Properties
NoneNo property set
Layout
Width180 (fixed)
Height24
Gapspace/space-8
Proposed — trailing placement proposed

Default arrangement: label stack on the left, toggle on the right. Matches iOS Form and Material 3 list-item patterns.

Proposed props
labelString
descriptionString?
placementtrailing
requiredfalse
helper / errorString?
Typography
LabelBody/M · 14/20 · text/primary
DescriptionBody/S · 12/16 · text/secondary
ErrorBody/S · 12/16 · text/danger
Proposed — leading placement proposed

Inverse arrangement: toggle on the left, label stack on the right. Useful in inline form layouts where labels are right-heavy.

Proposed API Planned API

Mirrors Radio Button With Label's shape, with a placement prop for leading/trailing toggle arrangement (iOS Form defaults to trailing; Material 3 allows either).

EBToggleLabeled(
    label: "Push notifications",
    description: "Get alerts when money moves",
    isOn: $pushEnabled,
    placement: .trailing,
    required: false,
    error: nil
)
EBToggleLabeled(
    label = "Push notifications",
    description = "Get alerts when money moves",
    checked = pushEnabled,
    onCheckedChange = { pushEnabled = it },
    placement = EBTogglePlacement.Trailing,
    required = false,
    error = null
)
Installation
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBToggleLabeled
Property Mapping (Proposed)
Figma (proposed)SwiftUICompose
label: Stringlabel: Stringlabel: String
description?: Stringdescription: String?description: String?
isSelected: true | false@Binding var isOn: Boolchecked: Boolean
placement: leading | trailingplacement: EBTogglePlacementplacement: EBTogglePlacement
required: booleanrequired: Boolrequired: Boolean
helper?: Stringhelper: String?helper: String?
error?: Stringerror: String?error: String?
State (inherited).disabled(true) / .ebState(.error)enabled / error
Suggested file paths
  • ios/Components/Toggle/EBToggleLabeled.swift
  • android/components/toggle/EBToggleLabeled.kt
Usage
// Settings row
EBToggleLabeled(
    label: "Push notifications",
    description: "Get alerts when money moves",
    isOn: $pushEnabled
)

// Form opt-in with required marker
EBToggleLabeled(
    label: "I accept the Terms of Service",
    isOn: $acceptedTerms,
    required: true,
    error: showError ? "You must accept to continue" : nil
)

// Leading placement (inline form)
EBToggleLabeled(
    label: "Remember me",
    isOn: $rememberMe,
    placement: .leading
)
// Settings row
EBToggleLabeled(
    label = "Push notifications",
    description = "Get alerts when money moves",
    checked = pushEnabled,
    onCheckedChange = { pushEnabled = it }
)

// Form opt-in with required marker
EBToggleLabeled(
    label = "I accept the Terms of Service",
    checked = acceptedTerms,
    onCheckedChange = { acceptedTerms = it },
    required = true,
    error = if (showError) "You must accept to continue" else null
)

// Leading placement
EBToggleLabeled(
    label = "Remember me",
    checked = rememberMe,
    onCheckedChange = { rememberMe = it },
    placement = EBTogglePlacement.Leading
)
Accessibility
RequirementiOSAndroid
Label ↔ Toggle linkVoiceOver announces label + state in one utterance ("Push notifications, on").TalkBack announces label + state in one utterance. Use Modifier.toggleable on the row.
Whole row tappableRow wrapped in Button or .onTapGesture that toggles.Row uses Modifier.toggleable, merging semantics.
Description announcedCombine label + description with .accessibilityElement(children: .combine).Merge descendants, description as stateDescription or second line.
Required markerAnnounce "required" after the label.Append "required" to contentDescription.
ErrorError text linked via .accessibilityHint; announce on state change.Error text in error semantics; TalkBack reads on focus.
Usage Guidelines
Do
  • Use for single-setting rows in Settings and forms.
  • Write labels as statements: "Push notifications", "Biometric login" — not questions.
  • Use description for one-line hints under the label.
  • Mark required: true for terms/consent toggles that block submit.
Don't
  • Don't build labeled toggles ad-hoc by placing Toggle + Text side by side — use this component.
  • Don't write "Turn on notifications" as the label — just state the setting.
  • Don't mix placement inside the same screen — pick leading or trailing per context and stick to it.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkNot a component — just a layout frame. Promote to real component.
C2Variant & Property NamingReworkNo properties. Add label, description, placement, required, helper, error.
C3Token CoverageReadyLabel typography and color bound via Toggle + Text styles.
C4Native MappabilityReworkCannot map a frame. Once built, maps to Toggle inside LabeledContent on iOS, Row with Modifier.toggleable on Compose.
C5Interaction State CoverageReworkNeed Default, Pressed (row), Disabled, Error.
C6Asset & Icon QualityN/ANo assets.
C7Code Connect LinkabilityNot MappedBlocked until component exists.
Variants Inventory (1 total)

Today: 1 layout frame. Proposed: placement (leading/trailing) × description (yes/no) × required (yes/no) × state (5)=40 variants. May simplify by treating required and description as runtime props rather than Figma variants.

#NodeDimensionsContents
118482:36538180 × 24Toggle instance + "Label" text · auto-layout row
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:36538
Verdict: Restructure — Not a real component. Promote to a proper component with label, description, helper/error, required marker, and leading/trailing placement. Open
Architecture
Family alignment — Match Radio Button With Label's shape. Inherit Toggle's isSelected, State, Size from the inner Toggle instance. Open
Family
C7 — Code Connect — Blocked until component exists. Open
C7
Toggle FixRefineComponent link

A binary on/off switch — the DS primitive for settings, feature flags, and opt-ins. 4 current variants from State=Default | Disabled × isActive=Yes | No. Size fixed at 48×24. Property schema is out of step with Checkbox and Radio Button — Toggle should be part of the shared "Selection Control" family.

Fix — normalize to the Selection Control schema
Rename isActiveisSelected, change Yes/No values to true/false, expand states from 2 → 5 (Default, Pressed, Focused, Disabled, Error), add Small/Medium/Large sizes. Once normalized, Toggle sits alongside Checkbox and Radio Button under one shared schema and maps cleanly to native Toggle / Switch.
In Context

Toggle appears in settings rows, form opt-ins, and any control that flips a single boolean. Usually paired with a label (see Toggle - With Label).

Live Preview

Click the toggle to flip it — it mirrors the Figma component's State=Default × isActive=Yes/No variant swap. Properties below only expose what's built in Figma today. Pressed / Focused / Error are proposals (see Open Issues) and live in Figma once the normalization ships.

Properties (today)
isActive
State
Proposed (post-normalization)
size
DS Health
Reusable
Pass
Generic on/off switch usable anywhere a boolean needs a visual control.
Self-contained
Pass
Owns its track, knob, colors, and shadow tokens.
Consistent
Warn
isActive + Yes/No breaks the DS convention set by Checkbox (isSelected + true/false). Selection controls should share one schema.
Composable
Partial
Drops into rows and forms fine, but Toggle - With Label is a frame not a component, limiting composition into list items and labeled form rows.
Behavior
StateiOSAndroidFigma SpecNotes
Default · OffYesYesState=Default, isActive=NoGray track, knob left.
Default · OnYesYesState=Default, isActive=YesBrand track, knob right.
PressedMissingMissingNot builtNeed darker track + scaled knob — critical feedback for tap.
FocusedMissingMissingNot built2px focus ring for keyboard / switch-control users.
Disabled · Off / OnYesYesState=DisabledMuted track/knob, tap blocked.
ErrorMissingMissingNot builtNeeded when required toggle (e.g. "accept terms") is unset on submit.
Open Issues
  • Property schema diverges from Checkbox. Rename isActiveisSelected, change values Yes/Notrue/false. Selection controls should share one schema. C2 · Variant & Property Naming
  • Missing interaction states. Only Default + Disabled built. Add Pressed, Focused, Error to match Checkbox's 5-state model. C5 · Interaction State Coverage
  • Missing size axis. No Small/Medium/Large. Add to match Checkbox + Radio Button. C2 · Variant & Property Naming
  • No Code Connect mapping. Blocked until schema normalizes. C7 · Code Connect Linkability
Design Recommendations
  • Normalize to the Selection Control schema. Bring Toggle in line with Checkbox and Radio Button so all three share one property language: isSelected: true | false (from isActive: Yes | No) × State: Default | Pressed | Focused | Disabled | Error (up from Default/Disabled only) × Size: Small | Medium | Large (new axis). Variant count grows from 4 → 30, all covered by a clean 2 × 5 × 3 matrix. Property
  • Promote Toggle - With Label to a proper component with label, optional description, optional helper/error text, required marker, and placement=leading | trailing. See Toggle - With Label. Composition
  • Consider a Loading state for async toggles (settings that sync to the server). Shows a spinner on the knob while the request is in flight — common in Material 3 and iOS 17. State
  • Document the ARIA role. Natively, Toggle maps to the switch role with aria-checked=true | false. Screen readers say "on/off" instead of "checked/unchecked" — the correct affordance for a settings toggle. A11y
Default · Off node 18482:36509

The "off" resting state. Gray track, white knob pinned left.

Properties
StateDefault
isActiveNo
Colors by State
ROLETOKENVALUE
Track (off)toggle/track/off#C8CDD5
Knobtoggle/knob#FFFFFF
Knob shadowshadow/elevation-10 1 2 rgba(10,23,87,0.08)
Layout
Track size48 × 24
Knob size20 × 20
Knob inset2
Corner radius12 (pill)
Typography
N/ANo text
Default · On node 18482:36512

The "on" resting state. Brand-blue track, knob pinned right.

Properties
StateDefault
isActiveYes
Colors
ROLETOKENVALUE
Track (on)toggle/track/on#005CE5
Knobtoggle/knob#FFFFFF
Disabled · Off node 18482:36515

Disabled off state. Muted gray track; interaction blocked.

Properties
StateDisabled
isActiveNo
Colors
ROLETOKENVALUE
Tracktoggle/track/disabled-off#EBEEF3
Knobtoggle/knob/disabled#F4F6FA
Disabled · On node 18482:36518

Disabled on state. Muted brand-blue track; interaction blocked.

Properties
StateDisabled
isActiveYes
Colors
ROLETOKENVALUE
Tracktoggle/track/disabled-on#9BC5FD
Knobtoggle/knob/disabled#F4F6FA
Installation Planned API
.package(url: "https://github.com/gcash/eb-design-system-ios", from: "1.0.0")

import EBDesignSystem
implementation("com.gcash.designsystem:eb-components:1.0.0")

import com.gcash.designsystem.components.EBToggle
Property Mapping (Proposed)
Figma (today)Figma (proposed)SwiftUICompose
isActive: Yes | NoisSelected: true | false@Binding var isOn: Boolchecked: Boolean
State: Default | DisabledState: Default | Pressed | Focused | Disabled | ErrorModifier: .disabled(true), .ebState(.error)enabled: Boolean, error: Boolean
(no size axis)Size: Small | Medium | Large.controlSize(.small / .regular / .large)size: EBToggleSize
Suggested file paths
  • ios/Components/Toggle/EBToggle.swift
  • android/components/toggle/EBToggle.kt
Usage
// Standalone toggle
EBToggle(isOn: $notificationsEnabled)

// Small size, in a compact row
EBToggle(isOn: $reduceMotion)
    .controlSize(.small)

// Disabled
EBToggle(isOn: .constant(true))
    .disabled(true)

// Error state (required toggle on form submit)
EBToggle(isOn: $acceptedTerms)
    .ebState(showError ? .error : .default)
// Standalone toggle
EBToggle(
    checked = notificationsEnabled,
    onCheckedChange = { notificationsEnabled = it }
)

// Small size
EBToggle(
    checked = reduceMotion,
    onCheckedChange = { reduceMotion = it },
    size = EBToggleSize.Small
)

// Disabled
EBToggle(checked = true, onCheckedChange = {}, enabled = false)

// Error state
EBToggle(
    checked = acceptedTerms,
    onCheckedChange = { acceptedTerms = it },
    error = showError
)
Accessibility
RequirementiOSAndroid
Switch roleSwiftUI Toggle automatically applies the switch accessibility trait — VoiceOver says "on/off", not "checked/unchecked".Material Switch applies Role.Switch semantics automatically.
Touch targetMinimum 44 × 44pt (pad the container — the 48×24 track alone is too small).Minimum 48 × 48dp.
State announcementVoiceOver announces "On" / "Off" as the value.TalkBack announces "On" / "Off" as the state description.
Disabled.disabled(true) blocks interaction; VoiceOver announces "dimmed".enabled=false blocks click; TalkBack announces "disabled".
Focus (external keyboard / switch control)iPad keyboards and Switch Control need a visible focus ring — must be added as part of the Focused state.D-pad / keyboard focus indicator — must be added as part of the Focused state.
Usage Guidelines
Do
  • Use for single on/off settings where the change takes effect immediately.
  • Pair with a label (Toggle - With Label) — a bare toggle needs context.
  • Reflect the toggle's current value as the primary state — don't use it for "confirm later" actions.
  • Keep the track 48×24 or larger; pad the hit target to ≥44×44pt / 48×48dp.
Don't
  • Don't use Toggle for multi-selection — use Checkbox.
  • Don't use Toggle for exclusive choice between options — use Radio Button or a Segmented Control.
  • Don't require a "Save" button after a toggle — the toggle itself should commit the change.
  • Don't use Yes/No as values in code — use true/false.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadyTrack/knob layers cleanly named.
C2Variant & Property NamingReworkRename isActiveisSelected, values Yes/Notrue/false; add Size axis.
C3Token CoverageReadyTrack + knob + shadow bound to tokens.
C4Native MappabilityReadyMaps 1:1 to SwiftUI Toggle / Material Switch.
C5Interaction State CoverageReworkMissing Pressed, Focused, Error. Need full 5-state model.
C6Asset & Icon QualityN/ANo icons.
C7Code Connect LinkabilityNot MappedBlocked until schema normalizes.
Variants Inventory (4 total)

State × isActive=4 variants today. Proposed: isSelected × State × Size=30 variants.

#NodeStateisActiveDimensions
118482:36509DefaultNo48 × 24
218482:36512DefaultYes48 × 24
318482:36515DisabledNo48 × 24
418482:36518DisabledYes48 × 24
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:36508
Verdict: Fix — Normalize to the shared Selection Control schema alongside Checkbox and Radio Button. Open
Schema
C2 — Property naming — Rename isActiveisSelected; change values Yes/Notrue/false. Open
C2
C5 — States — Add Pressed, Focused, Error states. Add Small/Medium/Large size axis. Open
C5
C7 — Code Connect — Blocked until schema normalizes. Open
C7
Tooltip Blurred and Transparent ConsolidateReworkComponent link

A dark-translucent directional tooltip intended for use over photographic or high-contrast content. Surface is #0A2757 at 80% opacity with a 2.5 px backdrop blur; contains a header and description (no icon, no close, no CTA); ships as 4 variants on a single pointer enum (top/right/bottom/left). This is the third Tooltip sibling in the DS — alongside Tooltip V2 and Onboarding - Tooltip — and is the weakest case for a distinct component: "blurred and transparent" is a visual treatment, not a role.

Fold into the canonical Tooltip as appearance: .translucent
Backdrop-blur + translucency is a material effect, not a distinct component. iOS expresses it with .background(.ultraThinMaterial) (or a tinted .regularMaterial); Compose does it with Modifier.blur() on an underlying layer (or a Haze library for true backdrop blur). Collapse this component into the unified Tooltip proposed on the V2 page as one of three appearance values (.default | .onboarding | .translucent). The translucent appearance binds to a main/nudge/color/secondary/* token set on the surface and swaps the label colors for the light-on-dark pair. No dedicated component; no "V2"-style duplicate.
Tooltip Family

Three sibling components in the DS do roughly the same job. They differ by skin, not role. The family-level target architecture lives on the Tooltip V2 page; this entry documents where this specific sibling slots in.

ComponentNodeVariantsWhat's differentProposed role
Tooltip V270:149088Opaque white surface. Header/Description/Icon/CTA presence axes.appearance: .default
Onboarding - Tooltip51:17066Walkthrough / coach-mark flavor — richer content, step indicator, prev/next CTAs.appearance: .onboarding
Tooltip Blurred and Transparent (this)49:3353494Dark translucent surface (#0A2757 @ 80%) + 2.5 px backdrop blur. Header + description only.appearance: .translucent

Of the three siblings, this one has the weakest case for standalone existence — its schema is a strict subset of V2's (header + description only, no icon/close/CTA), and its visual differentiation reduces to two tokens (secondary surface / secondary label) plus a material effect that lives on the platform, not in Figma.

In Context

Used over photographic, gradient, or high-contrast imagery where an opaque white tooltip would feel heavy. The backdrop blur keeps the background legible while the dark surface carries white type with full contrast.

Live Preview

Toggle header presence and placement. This sibling has no CTA, icon, or close control — if you need those, use Tooltip V2. Under the unified target schema, all three flavors share one component and these toggles become appearance + placement + content flags.

Content
header
description
Placement
pointer
Backdrop
scene
DS Health
Reusable
Warn
Covers a real use case (tooltips over imagery), but the component is the wrong unit of reuse — the reusable thing is a translucent appearance on the canonical Tooltip. Today a consumer must pick between 3 sibling components to choose a skin. C1
Self-contained
Partial
Surface, label, and description colors bind to main/nudge/color/secondary/* tokens; spacing to space/*; radius to radius/radius-2. Blur amount (2.5 px) is a raw literal — no effect/blur-* token. Pointer is a raster. C6
Consistent
Warn
Component name is a visual-effect description ("Blurred and Transparent") rather than a role. Pointer direction is already correctly modelled as a single pointer enum here — which makes the 4-boolean shape on Tooltip V2 even harder to defend. C2
Composable
Fail
No slots for leading icon or body content. No dismiss control. No CTA support. If a consumer needs any of those, they must switch to a different Tooltip component — a clear signal this should be an appearance, not a component. C4
Behavior
BehavioriOSAndroidFigma SpecNotes
Show / hideYesYesNot annotatedExpected: fade + slight scale-in anchored on the pointer side. Under the unified schema, shared with other appearance values.
Backdrop blurMaterialModifierbackdrop-blur 2.5 pxiOS: .background(.ultraThinMaterial) or a custom blurred UIVisualEffectView. Compose: Modifier.blur() or a Haze effect for true behind-content blur.
Tap outsideImplicitImplicitNot definedStandard tooltip contract — tap-outside dismisses. Same as other Tooltip siblings.
Pressed / FocusedMissingMissingNot builtNo interaction states modelled. No dismiss affordance at all — consumer must rely on tap-outside or a timer.
Reduce transparencyRequiredRequiredNot definediOS: respect UIAccessibility.isReduceTransparencyEnabled — fall back to an opaque #0A2757 surface. Android: same fallback when high-contrast mode is on.
Open Issues
  • Visual treatment shipped as a standalone component. "Blurred and Transparent" describes a surface material + opacity, not a component role. The reusable unit is an appearance flag on the canonical Tooltip, not a separate Figma component. iOS and Compose both model this as a modifier applied to an existing view — never as a distinct type. C1 · Layer Structure & Naming
  • Third sibling Tooltip for one primitive. Alongside Tooltip V2 (70:14908) and Onboarding - Tooltip (51:17066), this duplicates the tooltip primitive. Fold all three into one component with appearance: .default | .onboarding | .translucent. C1 · Layer Structure & Naming
  • Component name describes a visual effect, not a role. "Tooltip Blurred and Transparent" chains two adjectives instead of naming what the component does. Under the unified schema, this becomes Tooltip / appearance=.translucent — the role stays "Tooltip", and the treatment is a property value. C2 · Variant & Property Naming
  • Backdrop blur is a platform material, not a Figma shape. The 2.5 px blur is modelled as a Figma layer effect, but native platforms provide this via .ultraThinMaterial (SwiftUI), UIVisualEffectView (UIKit), and Modifier.blur() / Haze libraries (Compose). A 1:1 Code Connect mapping of today's shape would emit a drawn blur surface instead of calling the correct platform API. C4 · Native Mappability
  • No icon, close, or CTA support. Schema is a strict subset of Tooltip V2 (header + description only). Consumers needing a leading icon, dismiss X, or a CTA must abandon this component and switch to V2 — losing the translucent surface in the process. An appearance property on one unified component removes this forced trade-off. C4 · Native Mappability
  • Pointer triangle is a raster asset. 4 separate image fills (imgPointer, imgPointer1, imgPointer2, imgPointer3) — one per edge — for what should be a single vector shape rotated by the pointer enum. Same anti-pattern as Tooltip V2. C6 · Asset & Icon Quality
  • No interaction states. No Pressed / Focused / Dismissing states modelled. There is no dismiss control at all — no close X, no tap-to-dismiss contract in the component, no lifecycle annotation. C5 · Interaction State Coverage
  • Blur amount (2.5 px) is a raw literal. Not bound to an effect/blur-* token. If the DS later introduces a translucent-surface token set, this component won't pick up the change automatically. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked on the consolidation — mapping today's shape would cement a standalone "translucent tooltip" type that shouldn't exist. Map once as one of the appearance values of the unified Tooltip. C7 · Code Connect Linkability
Design Recommendations
  • Fold this component into the unified Tooltip as appearance: .translucent. Deprecate the standalone "Tooltip Blurred and Transparent" node. The translucent appearance flips surface and label tokens (main/nudge/color/secondary/bg + main/nudge/color/secondary/label) and, on native, applies .background(.ultraThinMaterial) / Modifier.blur(). No new component; no schema duplication. Family
  • Drop the visual-effect name. Rename to — or merge away as — Tooltip with appearance=.translucent. Component names should describe roles, not treatments. Rename
  • Bind the translucent surface to a named token set. Today the surface is main/nudge/color/secondary/bg (#0A2757 raw) paired with an 80% group opacity. Move to a proper main/nudge/color/translucent/{bg,label,description} token set with the alpha baked in, so the .translucent appearance picks up theme updates automatically. Token
  • Tokenize the blur radius. Replace the raw 2.5 px literal with an effect/blur-tooltip (or similar) token. Any future translucent surface (Modal, Overlay, Sheet) can then reuse the same value. Token
  • Replace the raster pointer with a vector triangle shared across appearances. One vector shape, rotated per placement, picks up the appearance's surface token automatically. Fixes the raster problem across all 3 siblings at once. Asset
  • Instance-swap (conceptually) to the canonical Tooltip. Consumers currently using this component should swap to Tooltip / appearance=.translucent during the migration; leave a short-lived alias variant if needed to avoid breaking existing instances. Composition
  • Document the reduce-transparency fallback. When iOS isReduceTransparencyEnabled or Android high-contrast is on, fall back to a solid #0A2757 surface (no blur). Write this on the unified Tooltip's accessibility section so all consumers see it. A11y
  • Document .translucent as photography-only. Annotate on the unified Tooltip: "Use .translucent only over photographic or gradient backgrounds; for flat UI backgrounds, prefer .default." Gives designers a clear usage rule. Docs
Variants

4 variants on a single pointer enum — top, right, bottom, left. Header + description are always present in today's shipped variants (the underlying code exposes a showHeader prop, but it isn't wired to a Figma variant property). Under the unified Tooltip, these 4 collapse into the shared placement enum.

pointer=top — pointer anchored above surface

Pointer above the surface — anchors a target element below. 336 × 89.

pointer=right — pointer on right edge

Pointer on the right — anchors a target element to the right of the surface. 348 × 77.

pointer=bottom — pointer below surface

Pointer below the surface — anchors a target element above. 336 × 89.

pointer=left — pointer on left edge

Pointer on the left — anchors a target element to the left of the surface. 348 × 77.

Colors by State
RoleTokenDefault
Surfacemain/nudge/color/secondary/bg#0A2757 @ 80% group opacity
Backdrop blur(untokenized literal)2.5 px
Header labelmain/nudge/color/secondary/label#FFFFFF
Descriptionmain/nudge/color/secondary/description#F6F9FD @ 80% (#F6F9FDCC)
Pointer triangle(raster, 4 images)#0A2757 (baked into raster)

No Pressed / Disabled states modelled at the tooltip level. No close / CTA — those roles are absent from this sibling. Blur amount should move to a token (effect/blur-tooltip) alongside the translucent surface tokens.

Layout
PropertyTokenValue
Surface width — top / bottom336 px
Surface width — left / right336 px (content) · 348 px (with pointer offset)
Surface corner radiusradius/radius-26 px
Surface paddingspace/space-1616 px all sides
Surface background alpha0.80 (group opacity)
Backdrop blur2.5 px (untokenized)
Header → description gapspace/space-44 px
Pointer width / height22 × 12 (top/bottom) · 20 × 12 (left/right) — raster
Pointer → surface gapspace/space-00 px (abutting)

No internal CTA / icon / close dimensions — those roles are absent from this sibling. Unified with Tooltip's full schema, the translucent appearance would inherit the same layout tokens as default/onboarding.

Typography
ElementDS text styleSpec
HeaderPrimary/Headlines/BlockProxima Soft Bold · 18 / 23 · +0.25
DescriptionSecondary/Bold/CaptionBarkAda Semibold · 12 / 18 · +0

Type styles identical to Tooltip V2 — only the color tokens change (white-on-dark vs dark-on-white). Another signal that "translucent" is an appearance, not a component. Custom-font standing action item applies (BarkAda).

Installation Planned API

This sibling shares a package with the unified Tooltip. No separate component module.

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:tooltip:1.0.0")
}
Property Mapping (proposed — after consolidation)
Figma (today)Figma (proposed)SwiftUICompose
Tooltip Blurred and TransparentTooltip (same component)EBTooltipEBTooltip
(distinct sibling)appearance: .translucent.ebAppearance(.translucent)appearance=EBTooltipAppearance.Translucent
pointer: top / right / bottom / leftplacement: .top / .right / .bottom / .leftarrowEdge: EdgeanchorPosition: EBTooltipAnchor
header (baked text)title: String?title: String?title: String?=null
description (baked text)body: String?body: String?body: String?=null
backdrop-blur 2.5 px (raw)(absorbed into .translucent appearance).background(.ultraThinMaterial)Modifier.blur(radius=2.5.dp)
surface @ 80% opacitymain/nudge/color/translucent/bgColor.nudgeTranslucentBgEBColors.nudgeTranslucentBg
(not modelled — no close)hasDismiss: Bool (inherited)dismissible: Booldismissible: Boolean=true
SwiftUI
ios/Components/Tooltip/EBTooltip.swift (shared — no separate file)
Jetpack Compose
android/components/tooltip/EBTooltip.kt (shared — no separate file)
Usage Snippets Planned API
// Translucent tooltip over imagery
EBTooltip(
    title: "Featured deal",
    body: "Limited-time discount on your next top-up.",
    placement: .top
)
.ebAppearance(.translucent)

// Under the hood, .translucent applies:
//   .background(.ultraThinMaterial)
//   .foregroundStyle(.white)
// + respects UIAccessibility.isReduceTransparencyEnabled
// Translucent tooltip over imagery
EBTooltip(
    title = "Featured deal",
    body = "Limited-time discount on your next top-up.",
    placement = EBTooltipPlacement.Top,
    appearance = EBTooltipAppearance.Translucent
)

// Under the hood, .Translucent applies:
//   Modifier.blur(radius = 2.5.dp) on the backing layer
//   background = nudgeTranslucentBg (#0A2757 @ 80%)
// + respects high-contrast settings (falls back to solid #0A2757)
Accessibility
RequirementiOSAndroid
Reduce transparencyRespect UIAccessibility.isReduceTransparencyEnabled — fall back to an opaque #0A2757 surface with no blur.Respect high-contrast text setting — fall back to an opaque #0A2757 surface with no blur.
ContrastWhite-on-#0A2757 meets WCAG AA at both states. Over photographic backdrops, the 80% alpha + blur keeps contrast above 4.5:1 in the designer's tested scenes.Same. Always test the .translucent appearance over real content; do not use it for critical error messaging.
Reduce motionRespect UIAccessibility.isReduceMotionEnabled — fade only; skip scale-in.Respect Settings.Global.TRANSITION_ANIMATION_SCALE — fade only when motion is reduced.
RoleAnnounce as tooltip; group title + body via .accessibilityElement(children: .combine).semantics { role=Role.Popup; mergeDescendants=true }.
Usage Guidelines
Do
  • Use the translucent appearance only over photographic, gradient, or richly-colored backgrounds where an opaque tooltip would feel heavy.
  • Verify contrast against the actual content behind the tooltip — a busy background can still reduce legibility even with 80% alpha + blur.
  • Fall back to the default (opaque) appearance when reduce-transparency or high-contrast is active.
  • Prefer the unified Tooltip's appearance property; don't branch on component type at the call site.
Don't
  • Don't use the translucent appearance over flat UI backgrounds — use appearance: .default.
  • Don't use for error or status messaging — use Alert.
  • Don't ship a standalone "translucent tooltip" component — this sibling should be deprecated in favor of one appearance value on the unified Tooltip.
  • Don't hardcode the blur radius in product code — bind to the effect/blur-tooltip (or equivalent) token.
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkThird sibling for one primitive. A visual treatment shipped as a discrete component. Consolidate into the canonical Tooltip.
C2Variant & Property NamingReworkComponent name is a visual-effect description ("Blurred and Transparent"). Should collapse into Tooltip / appearance=.translucent.
C3Token CoverageRefineSurface, label, description, radius, and spacing all bound to tokens. The blur amount (2.5 px) and the 80% group opacity are raw literals — move to a translucent-surface token set.
C4Native MappabilityReworkBackdrop-blur is a platform material (.ultraThinMaterial / Modifier.blur()), not a component shape. A 1:1 Figma→native mapping today would emit a drawn blur layer instead of calling the correct API. Fixes when consolidated as an appearance value.
C5Interaction State CoverageReworkNo Pressed / Focused / Dismissing states. No dismiss affordance at all (no close X).
C6Asset & Icon QualityReworkPointer triangle is 4 raster images (one per edge). Same anti-pattern as Tooltip V2.
C7Code Connect LinkabilityNot MappedBlocked on consolidation — mapping today's shape would cement a duplicate component. Map once as one of the unified Tooltip's appearance values.
Variants Inventory (4 total)

One enum axis yields 4 variantspointer: top / right / bottom / left. Header + description are present in all shipped variants. Under the unified Tooltip, these 4 collapse into the shared placement enum, and the distinguishing surface treatment moves to appearance=.translucent.

#PointerDimensionsNode
1top336 × 8949:335345
2right348 × 7749:335347
3bottom336 × 8949:335348
4left348 × 7749:335346
1.0.0 — April 2026 Initial
Initial Assessment · node 49:335349
Verdict: Consolidate — Fold into the canonical Tooltip as appearance: .translucent. Do not ship a separate "Tooltip Blurred and Transparent" component. Open
Architecture
C1 — Third sibling for one primitive — Tooltip V2, Onboarding - Tooltip, Tooltip Blurred and Transparent. Merge via appearance enum. Open
C1
C2 — Visual-effect name — "Blurred and Transparent" describes a treatment, not a role. Collapse to Tooltip / appearance=.translucent. Open
C2
C4 — Backdrop-blur is a platform material — iOS uses .ultraThinMaterial; Compose uses Modifier.blur(). A component can't model a native modifier 1:1. Open
C4
C5 — No interaction states, no dismiss — No Pressed / Focused / Dismissing; no close control at all. Open
C5
C6 — Raster pointer + untokenized blur — 4 raster pointer images; blur radius (2.5 px) is a raw literal. Open
C6
C7 — Code Connect — Blocked on consolidation. Mapping today's shape would cement a duplicate. Open
C7
Pointer enum ✓ — Unlike Tooltip V2's 4-boolean pointer shape, this sibling already uses a single pointer enum — the correct model. Noted
Praise
Tokens ✓ — Surface / label / description bound to main/nudge/color/secondary/*. Spacing via space/*. Radius via radius/radius-2. Noted
Praise
Tooltip V2 RestructureReworkComponent link

A directional floating popover — used for tips, pointers-to-action, and short in-product explanations. Contains a header, description, close X, optional leading icon, optional 1–2 CTAs, and a pointer triangle on any of four sides. Ships as 8 variants across 4 axes (CTA, Icon, Description, Header) — and is one of three Tooltip siblings in the DS (Tooltip V2, Onboarding - Tooltip, Tooltip Blurred and Transparent). The version suffix in the name is the first red flag.

Unify 3 Tooltip siblings into one Tooltip component; drop the "V2" suffix
This component, Onboarding - Tooltip, and Tooltip Blurred and Transparent model the same primitive with different skins. Merge into one Tooltip with placement: .top | .right | .bottom | .left (one enum, not 4 booleans), appearance: .default | .onboarding | .translucent, hasArrow, hasDismiss, and a content slot. Replace the raster pointer with a vector, the placeholder icon circle with a Figma Slot, and strip the V2 suffix — production component names should never carry a version. Maps cleanly to TipKit / PlainTooltip / RichTooltip on native.
Tooltip Family

Three sibling components in the DS do roughly the same job. They differ by skin, not role.

ComponentNodeVariantsWhat's differentProposed role
Tooltip V2 (this)70:149088Header/Description/Icon/CTA presence axes. Opaque white surface.appearance: .default
Onboarding - Tooltip51:17066Walkthrough / coach-mark flavor — typically richer content, step indicator, prev/next CTAs.appearance: .onboarding
Tooltip Blurred and Transparent49:335349Translucent surface with backdrop blur — used over photographic / high-contrast content.appearance: .translucent

Target architecture: one Tooltip with an appearance enum, one placement enum replacing the 4 pointer booleans, optional hasArrow / hasDismiss, and a single content slot that accepts either a simple string or a rich sub-tree.

In Context

Tooltips sit over a target element (tab, button, icon, card) with a pointer aimed at the thing they describe.

WalletNext
Live Preview

Toggle content slots, placement, and the CTA axis. Note that pointer direction in today's Figma is 4 independent booleans — the demo below uses the correct single enum shape.

Content
header
description
icon
cta
Placement
pointer
DS Health
Reusable
Partial
Covers the main in-product tooltip pattern across onboarding, tips, and nudges. But because 3 siblings exist for 3 skins, consumers have to hunt for the right one — the primitive is fragmented. C1
Self-contained
Warn
Surface, type, and spacing bind to main/nudge/* + space/* tokens. But the pointer arrow is a raster image (4 rotated copies of one shape) and the leading icon is a gray placeholder circle, not a slot. C6
Consistent
Warn
Component is named Tooltip V2 — version suffixes don't belong in production DS names. Pointer direction is 4 separate booleans rather than one enum, letting consumers set nonsensical combinations (e.g. all 4 pointers on). C2
Composable
Warn
No Figma Slot for leading icon or body content. CTAs are baked Button instances that re-implement pill padding rather than composing the Button component consistently across variants (one variant uses px-16 / py-12, another uses px-8 / py-6). C4
Behavior
BehavioriOSAndroidFigma SpecNotes
Show / hideYesYesNot annotatedExpected: fade + slight scale-in anchored on the pointer side.
Tap close (X)YesYesClose layerDismiss icon is present in markup but not wired to a property or interaction. Contract should be explicit.
Tap outsideImplicitImplicitNot definedStandard tooltip contract — tap-outside dismisses. Should be documented on the component.
Primary CTAYesYesCTA=one / twoAdvances in onboarding or performs the tip's primary action.
Secondary CTAYesYesCTA=two"Back" in onboarding; "Learn more" in tips.
Pressed / FocusedMissingMissingNot builtCTAs and close have no pressed/focused treatment on the tooltip component itself. Inner Button instances handle their own press, but close + the tooltip surface do not.
Open Issues
  • Component is named Tooltip V2. Version suffixes don't belong in production DS component names — they imply a V1 that wasn't removed, and force consumers to choose which version is "right". Rename to Tooltip and delete V1 (or, if V1 is still in use, merge/deprecate before renaming). C2 · Variant & Property Naming
  • Three sibling Tooltip components for one primitive.Tooltip V2 (70:14908), Onboarding - Tooltip (51:17066), and Tooltip Blurred and Transparent (49:335349) all model the same floating popover with different skins. Collapse into one Tooltip with an appearance enum. C1 · Layer Structure & Naming
  • Pointer direction is 4 independent booleans.pointerTop, pointerRight, pointerBottom, pointerLeft — nothing prevents a consumer from enabling two or all four. A single placement: .top | .right | .bottom | .left | .none enum is the correct shape. Maps 1:1 to TipKit.arrowEdge and Compose TooltipAnchorPosition. C2 · Variant & Property Naming
  • Pointer triangle is a raster asset. 4 separate image fills (imgPointer, imgPointer1, imgPointer2, imgPointer3) for what should be one vector shape rotated per edge. Replace with a single vector triangle component; rotation handled by the placement enum. C6 · Asset & Icon Quality
  • Leading icon is a gray placeholder circle. The Icon=yes variant renders a flat #C2C6CF 46 px circle under a "Placeholder" frame — same anti-pattern as Action List / List Item. Replace with a named leading Figma Slot so consumers can drop in an Icon, Avatar, or illustration. C6 · Asset & Icon Quality
  • Close (X) is an image asset, not a DS icon instance. The dismiss control uses imgShapeFull inside a generic "Close" frame rather than an instance of the DS's icon/close. That hides the icon from a11y / token updates and blocks Code Connect from seeing it as a real control. C1 · Layer Structure & Naming
  • CTA Button padding drifts between variants.CTA=one, Header=yes uses px-16 / py-12; CTA=one, Header=no uses px-12 / py-6; CTA=two uses yet another combo. The underlying Button - XSmall instance should be identical across all variants — same size mode, same padding. C4 · Native Mappability
  • No dismiss / show states modeled. Close button exists visually but carries no interaction property; there is no Pressed / Focused state on the dismiss control or on CTAs at the tooltip level. Tooltips also have an implicit "appearing / dismissing" lifecycle that isn't documented. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked on the consolidation + slot adoption + enum conversion. Mapping the current 3-sibling / 4-boolean shape to native would cement the wrong schema. C7 · Code Connect Linkability
Design Recommendations
  • Consolidate the 3 Tooltip siblings into one component. New schema: placement: .top | .right | .bottom | .left (replaces the 4 booleans), appearance: .default | .onboarding | .translucent (replaces the 3 sibling components), hasArrow: Bool, hasDismiss: Bool, cta: .none | .primary | .primaryAndSecondary, plus a leading slot for icon/avatar and a content slot for the body. Replaces today's 8 + ? + ? variants across 3 components with roughly 4 placement × 3 appearance × 3 cta=36 permutations of one clean schema. Family
  • Rename Tooltip V2Tooltip. Version suffixes shouldn't appear in production DS names. If V1 is still referenced anywhere, migrate its instances first, then delete V1, then drop the suffix. Rename
  • Replace 4 pointer booleans with a single placement enum. Prevents nonsensical states (all 4 pointers on), maps 1:1 to SwiftUI .arrowEdge and Compose TooltipAnchorPosition, and reduces the variant matrix dramatically. Property
  • Replace the raster pointer with a vector triangle. Today there are 4 separate rasters (top/right/bottom/left). One vector shape, rotated per placement, fills the same role with zero asset burden, scales cleanly, and picks up token color updates automatically. Asset
  • Adopt a leading Figma Slot for the icon. Drop the #C2C6CF placeholder circle. Maps to @ViewBuilder (SwiftUI) and a @Composable slot (Compose). Empty slot=no leading. Slot
  • Instance-swap the close button to icon/close. Use the canonical DS close-icon instance rather than an inline imgShapeFull. Gets you token-driven color, a11y labeling, and press-state handling for free. Composition
  • Normalize CTA Button padding. Every tooltip variant uses the same Button size mode (XSmall). Enforce one padding via the Button component itself — don't let the tooltip override. Composition
  • Document the dismiss contract + lifecycle. Add a description on the component: "Tap the close X or tap outside to dismiss, unless hasDismiss=false (force interaction with CTA). Tooltip appears with fade + slight scale from the pointer anchor; dismisses with reverse." Close the gap between designer intent and dev implementation. Docs
  • Add Pressed / Focused on the close control and CTAs. Inner Button instances handle their own states, but the close X does not — it's an image. Once it becomes an icon-button instance, states follow. State
Variants

8 variants across 4 axes — CTA (none/one/two) × Icon (yes/no) × Description (yes/no) × Header (yes/no). Only 8 of the 24 theoretical combinations ship; the rest aren't exposed.

CTA=one · Icon=yes · Description · Header — the hero variant

Full-shape onboarding variant. Leading icon placeholder + header + description + primary CTA + close. 359 × 181.

CTA=one · Description · Header — tip with action

Text-only tip with a primary CTA. No leading icon. 359 × 155. Inherits different outer padding (px-16 py-12) than the hero variant (p-16).

CTA=none · Icon=yes · Description · Header — icon + explanatory text

Dismissible explanatory tooltip with an icon. 359 × 137.

CTA=none · Description · Header — plain tip card

Plain text tip with header + description + close. 359 × 119.

CTA=none · Header only — pointer/label

Single-line title pointer — used to label or point at a UI element. 359 × 79.

CTA=none · Description only — concise explanation

Description-only tooltip — short explanatory body, no title. 359 × 92.

CTA=two · Description — back + next walkthrough

Two-CTA walkthrough step: outline "Back" + filled "Next". 359 × 136.

CTA=one · Description · no Header — tip → single CTA

Body + single CTA, no header. 359 × 136. Uses px-12 / py-6 CTA padding — different from the two variants above.

Colors by State
RoleTokenDefault
Surfacemain/nudge/color/primary/bg#FFFFFF
Bordermain/nudge/color/primary/border#E5EBF4
Header labelmain/nudge/color/primary/label#0A2757
Descriptionmain/nudge/color/primary/description#6780A9
Close iconmain/nudge/color/primary/icon-close#0A2757
Leading icon placeholder#C2C6CF (not tokenized — placeholder)
CTA primary bgmain/button/primary/brand/enabled/bg#005CE5
CTA primary labelmain/button/primary/brand/enabled/label#FFFFFF
CTA secondary border / labelmain/button/secondary/brand/enabled/border#005CE5
Pointer triangleraster (4 images)

No Pressed / Disabled states modeled at the tooltip level. Pointer triangle should inherit the surface + border tokens once it becomes a vector.

Layout
PropertyTokenValue
Surface width335 px (content) · 359 px (with offsets)
Surface corner radiusradius/radius-26 px
Surface bordermain/nudge/color/primary/border1 px solid
Surface padding — herospace/space-1616 px all sides
Surface padding — text+CTAspace/space-16 · space/space-1216 / 12 (inconsistent with hero)
Leading icon size46 × 46 (placeholder circle)
Close size18 × 18 (image asset)
Pointer width / height24 × 12 (raster image)
Icon → text gapspace/space-44 px
Text-container gapspace/space-88 px
CTA row gap (two CTAs)space/space-88 px (justify between)
CTA Button — sizeXSmall (Button component)

Padding drifts between variants — hero uses p-16, CTA=one+Header uses px-16 / py-12. Should be one rule.

Typography
ElementDS text styleSpec
HeaderPrimary/Headlines/BlockProxima Soft Bold · 18 / 23 · +0.25
DescriptionSecondary/Bold/CaptionBarkAda Semibold · 12 / 18 · +0
CTA labelPrimary/Label/BaseProxima Soft Bold · 16 / 16 · +0.25

BarkAda (secondary) is used only for the description. Custom-font standing action item applies.

Installation Planned API

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:tooltip:1.0.0")
}
Property Mapping (proposed — after consolidation)
Figma (today)Figma (proposed)SwiftUICompose
3 sibling components1 component: TooltipEBTooltipEBTooltip
(sibling=appearance)appearance: .default / .onboarding / .translucent.ebAppearance(.default / .onboarding / .translucent)appearance: EBTooltipAppearance
pointerTop/Right/Bottom/Left: Bool × 4placement: .top / .right / .bottom / .left / .nonearrowEdge: EdgeanchorPosition: EBTooltipAnchor
header: Bool + text bakedtitle: String?title: String?title: String?=null
description: Bool + text bakedbody: String? (or content slot)body: String?body: String?=null
icon: Bool (gray placeholder)leading (Slot)@ViewBuilder leadingleading: @Composable () -> Unit
Close image asset (always)hasDismiss: Booldismissible: Booldismissible: Boolean=true
cta: none / one / twocta: .none / .primary(String) / .pair(back, next)primary / secondary: TooltipAction?primaryAction / secondaryAction: TooltipAction?
outlineButton: Bool(absorbed into cta.pair)
(not modeled)hasArrow: Boolarrow: BoolshowArrow: Boolean=true
(not modeled)onDismissonDismiss: () -> VoidonDismiss: () -> Unit
SwiftUI
ios/Components/Tooltip/EBTooltip.swift
Jetpack Compose
android/components/tooltip/EBTooltip.kt
Usage Snippets Planned API
// Simple tip — title + body, pointer below
EBTooltip(
    title: "Quick transfers",
    body: "Tap here to send money to recent contacts.",
    placement: .bottom
)
.onDismiss { showTip = false }

// Walkthrough step — two CTAs
EBTooltip(
    title: "Step 2 of 4",
    body: "Review your balance before confirming.",
    placement: .top,
    cta: .pair(back: "Back", next: "Next")
)
.ebAppearance(.onboarding)

// Rich — custom leading slot
EBTooltip(title: "Welcome", body: "Tap a card to begin.") {
    Image(systemName: "sparkles")
}

// iOS 17 + Tips — recommended for onboarding flows
.popoverTip(quickTransferTip)
// Simple tip
EBTooltip(
    title = "Quick transfers",
    body = "Tap here to send money to recent contacts.",
    placement = EBTooltipPlacement.Bottom,
    onDismiss = { showTip = false }
)

// Walkthrough step — two CTAs, onboarding skin
EBTooltip(
    title = "Step 2 of 4",
    body = "Review your balance before confirming.",
    placement = EBTooltipPlacement.Top,
    appearance = EBTooltipAppearance.Onboarding,
    primaryAction = TooltipAction("Next", onClick = { advance() }),
    secondaryAction = TooltipAction("Back", onClick = { rewind() })
)

// Custom leading slot
EBTooltip(
    title = "Welcome",
    body = "Tap a card to begin.",
    leading = { Icon(Icons.Default.AutoAwesome, contentDescription = null) }
)

// Material 3 PlainTooltip equivalent
PlainTooltip { Text("Shortcut") }
Accessibility
RequirementiOSAndroid
Role + focusAnnounce tooltip as .accessibilityAddTraits(.isModal) when hasDismiss=true — otherwise role .staticText.semantics { role=Role.Popup } on the container; TalkBack focuses it on appear.
Close controlWrap close as a Button with accessibilityLabel "Dismiss tip" and 44×44 hit target.IconButton with contentDescription="Dismiss tip"; 48×48dp minimum.
Dismiss-outsideRespect UIAccessibility.isVoiceOverRunning — do not auto-dismiss tooltips while VO is active.Do not auto-dismiss while TalkBack is active; hold the tooltip until the user explicitly moves on.
Reduce motionRespect UIAccessibility.isReduceMotionEnabled — skip the scale-in animation; fade only.Respect Settings.Global.TRANSITION_ANIMATION_SCALE — fade only when user has motion reduced.
Combined labelRead title + body + "Dismiss" as one phrase; avoid reading pointer.Same; set mergeDescendants=true on the container.
Usage Guidelines
Do
  • Use for short, transient explanations or pointer-to-action nudges tied to a specific element.
  • Anchor the pointer at the target — consistency helps users associate the tip with its referent.
  • Prefer one primary CTA; use two only for onboarding walkthroughs (Back + Next).
  • Provide a dismiss control unless the tooltip is a required onboarding gate.
Don't
  • Don't use Tooltip for error / status messaging — use Alert.
  • Don't use Tooltip for large or scrollable content — promote to Modal.
  • Don't stack multiple tooltips simultaneously — one focal point per screen.
  • Don't enable more than one pointer direction — pick one with placement (when the schema lands).
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRework3 sibling components for one primitive. Close uses a raw image asset inside a generic frame rather than an icon/close instance.
C2Variant & Property NamingReworkVersion suffix in the component name (Tooltip V2). Pointer direction is 4 booleans instead of one enum.
C3Token CoverageReadySurface, border, label, description, and CTA colors bound to main/nudge/* and main/button/* tokens. Spacing via space/*.
C4Native MappabilityReworkMaps cleanly once pointer booleans → placement enum and sibling skins → appearance enum. CTA padding inconsistencies block a 1:1 Button reuse.
C5Interaction State CoverageReworkNo Pressed / Focused on close. No lifecycle (appearing / dismissing) annotated. Close isn't wired to a dismiss property.
C6Asset & Icon QualityReworkPointer is 4 raster images (one per edge). Leading icon is a gray placeholder circle. Close is an image asset.
C7Code Connect LinkabilityNot MappedBlocked on consolidation + enum conversion + slot adoption. Mapping today's shape would cement the wrong schema.
Variants Inventory (8 total)

4 booleans and one 3-value enum yield 8 shipped variants out of a 24-cell theoretical matrix (CTA (3) × Icon (2) × Description (2) × Header (2)=24). Only the 8 meaningful combinations are exposed. Pointer direction is 4 additional booleans outside the variant matrix.

#CTAIconDescriptionHeaderDimensionsNode
1oneyesyesyes359 × 18170:14907
2onenoyesyes359 × 1557977:12260
3noneyesyesyes359 × 13770:14903
4nonenoyesyes359 × 11970:14902
5nonenonoyes359 × 7970:14900
6nonenoyesno359 × 9270:14901
7twonoyesno359 × 13670:14905
8onenoyesno359 × 13670:14906
1.0.0 — April 2026 Initial
Initial Assessment · node 70:14908
Verdict: Restructure — Consolidate 3 sibling Tooltip components; drop the V2 suffix; replace 4 pointer booleans with one placement enum; replace raster pointer with vector; replace placeholder icon with a Figma Slot. Open
Architecture
C1 — 3 siblings for 1 primitive — Tooltip V2, Onboarding - Tooltip, Tooltip Blurred and Transparent. Merge via appearance enum. Open
C1
C2 — Version suffix in nameTooltip V2 shouldn't exist in production; no V1 surfaces in the file. Open
C2
C2 — Pointer is 4 booleans — Replace pointerTop/Right/Bottom/Left with a single placement enum. Open
C2
C4 — CTA padding drift — Same XSmall Button, three different paddings across variants. Normalize. Open
C4
C5 — No dismiss/focus states — Close is decorative; Pressed / Focused / Lifecycle not modeled. Open
C5
C6 — Raster pointer + placeholder icon — 4 raster images for the pointer; gray circle for the leading icon. Vector + slot. Open
C6
C7 — Code Connect — Blocked on the architectural changes above. Open
C7
Tokens ✓ — Surface / border / label / description / CTA colors all bound to main/nudge/* and main/button/*. Spacing via space/*. Noted
Praise
Upload File FixNeeds RefinementComponent link

File upload input with five states (Default, Uploading, Uploaded, Upload error, Uploaded with thumbnail) and an optional label above. 304px fixed width. Uses a Lottie animation for the progress bar during upload. 10 variants total across the state × hasLabel matrix.

Property + token cleanup needed
Fix the boderborder token typo (library-wide). Normalize hasLabel to true/false. Split state="Uploaded with thumbnail" into state=uploaded + hasThumbnail: Bool. Rename "Upload error"error (remove space). Adopt a Figma Slot for the thumbnail image. Add disabled state.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. Upload File appears in forms requiring document proof (KYC, insurance claims, verification).

Upload DocsAttach file / photoID_proof.jpgSubmit
Live Preview

Toggle state and hasLabel to see each variant render.

Properties
state
hasLabel
DS Health
Reusable
Pass
Used in KYC flows, insurance claims, profile setup — anywhere a user uploads a document or photo. Five states cover the full upload lifecycle.
Self-contained
Warn
Carries its own bg, border, padding, radius. Progress bar relies on an external Lottie animation — must be bundled with the native package as an asset dependency. Thumbnail placeholder uses a hardcoded hex with baked opacity instead of a token. C3
Consistent
Warn
Three property-naming issues: hasLabel uses yes/no, state="Upload error" contains a space, and "Uploaded with thumbnail" is really an orthogonal boolean, not a state. Also the boder typo in every border token. C2C3
Composable
Warn
Thumbnail is a 52×52 hardcoded placeholder block (same icon-placeholder pattern we've flagged in Chip, Tab Item, List Item Asset) — should be a Figma Slot so product teams can drop in any preview image. C6
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesstate=DefaultEmpty input with paperclip + "Attach file / photo" label
UploadingYesYesstate=UploadingShows file name + Lottie progress bar + percentage
UploadedYesYesstate=UploadedFile name + trailing trash icon to remove
Uploaded with thumbnailYesYesstate=Uploaded with thumbnail52×52 image preview + truncated file name + trash. Should be orthogonal hasThumbnail prop.
Upload errorYesYesstate=Upload errorRed 2px border + red subtext ("Maximum file size: 20MB")
Disabled / Pressed / FocusedN/AN/ANot defined. Engineers must improvise. C5
Open Issues
  • Property naming issues.hasLabel uses yes/no instead of true/false; state="Upload error" contains a space; state="Uploaded with thumbnail" mixes two orthogonal dimensions (state + thumbnail presence). C2 · Variant & Property Naming
  • Token typo — boder. Every border token is misspelled: main/upload-file/color/default/boder, main/upload-file/color/error/boder. Rename across the whole collection. C3 · Token Coverage
  • Thumbnail color hardcoded. Uses #0057E4 with opacity: 5% baked in instead of a token. C3 · Token Coverage
  • No disabled / pressed / focused states. Forms need a disabled variant for read-only views; pressed and focused are expected interaction affordances. C5 · Interaction State Coverage
  • Thumbnail is a placeholder. 52×52 gray block instead of a swappable image slot. Should be a Figma Slot that accepts a real image instance. C6 · Asset & Icon Quality
  • Lottie dependency. Progress bar requires the Lottie animation (0a1cb540-b53a-4e28-afa5-8aa5ca7ebaa1) to be bundled with the native package. Document as a required asset. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until state/property restructure and token rename land. C7 · Code Connect Linkability
Design Recommendations
  • Restructure the state property:
    state: default / uploading / uploaded / error (4 values, clean enums)
    hasThumbnail: Bool (orthogonal — can combine with uploaded)
    hasLabel: Bool (true/false)
    disabled: Bool (new)
    Collapses 10 variants into 4 states × 2 hasLabel × 2 hasThumbnail × 2 disabled=32 prop combinations with no invalid states. Property
  • Fix the boder typo across the token collection. Rename both default/boder and error/boder. Library-wide change — affects every variant. Token
  • Tokenize the thumbnail placeholder — replace hardcoded #0057E4 @ 5% with main/upload-file/color/default/thumbnail-bg. Token
  • Adopt a Figma Slot for the thumbnail — swappable preview image. Maps to @ViewBuilder / @Composable slot for Code Connect. Slot
  • Reuse Labeled Field for the label + subtext scaffolding. Today Upload File reimplements the label-above + subtext-below pattern that Labeled Field already ships. Making Upload File an input slot inside Labeled Field reduces duplication. Composition
  • Document the Lottie dependency — Progress bar is a Lottie animation. Native packages must bundle the animation JSON. Consider replacing with a native progress bar for a lighter dependency. Docs
Variants

Five states × hasLabel yes/no=10 variants. Showing each state at hasLabel=no; the label variant simply prepends a 14 / 14 label + 8px bottom spacing.

Default — empty state

Empty state with paperclip + "Attach file / photo" placeholder text. 2px border, white bg. Subtext below lists accepted formats.

Uploading — Lottie progress

Shows file name + 5px-tall Lottie progress bar + percentage. Height grows to 91px to accommodate the progress row.

Uploaded — file name + trash

File name (GCash_File.png) + trailing trash icon for removal.

Uploaded with thumbnail — preview + name

52×52 thumbnail preview + truncated file name (New_GCash_Fi….jpeg) + trash. Recommended to split into state=uploaded + hasThumbnail: true.

Upload error — red border + error subtext

Red 2px border + red error subtext ("Maximum file size: 20MB").

Colors by State
StateRoleTokenValue
Defaultbgmain/upload-file/color/default/bg#FFFFFF
bordermain/upload-file/color/default/bodertypo#E5EBF4
leading iconmain/upload-file/color/default/icon-leading#6780A9
trailing iconmain/upload-file/color/default/icon-trailing#005CE5
labelmain/upload-file/color/default/label#0A2757
file namemain/upload-file/color/default/label-name#005CE5
progress labelmain/upload-file/color/default/progress-label#0A2757
thumbnail bg— (hardcoded #0057E4 @ 5%) not tokenized
Errorbgmain/upload-file/color/error/bg#FFFFFF
bordermain/upload-file/color/error/bodertypo#D61B2C
leading iconmain/upload-file/color/error/icon-leading#6780A9
labelmain/upload-file/color/error/label#0A2757
file namemain/upload-file/color/error/label-name#005CE5
error subtextmain/subtext-message/error/label#D61B2C
Subtextdefault labelmain/subtext-message/primary/label#6780A9
Layout
PropertyTokenValue
Container width304px
Input height (default/uploaded/error)72px
Input height (uploading)91px (adds progress row)
Border width2px
Corner radiusradius/radius-26px
Horizontal padding16px (12L / 16R for thumbnail)
Vertical padding24px
Icon → name gapspace/space-44px
Thumbnail size52 × 52
Thumbnail → name gapspace/space-88px
Label → input gapspace/space-88px
Input → subtext gapspace/space-88px
Progress bar height5px
Progress bar width250px
Leading / trailing icon size24 × 24
Typography
ElementDS text styleSpec
LabelPrimary/Label/Light/SmallHeyMeow Rnd Semibold · 14 / 14 · +0.25
File name / placeholderPrimary/Label/Light/LargeHeyMeow Rnd Semibold · 18 / 18 · +0.25
SubtextSecondary/Bold/CaptionBarkAda Semibold · 12 / 18
Progress percentageSecondary/Bold/Small CaptionBarkAda Semibold · 10 / 15
Installation Planned API

iOS — Swift Package Manager

// In Xcode: File → Add Package Dependencies
"https://github.com/AY-Org/eb-ds-ios"

// Requires: lottie-ios for progress animation
"https://github.com/airbnb/lottie-ios"

Android — Gradle (Kotlin DSL)

dependencies {
    implementation("com.eastblue.ds:upload-file:1.0.0")
    // Requires: lottie-compose for progress animation
    implementation("com.airbnb.android:lottie-compose:6.4.0")
}
Property Mapping (proposed — after restructure)
Current FigmaProposedSwiftUICompose
state=Default/Uploading/Uploaded/Upload errorstate: EBUploadStatestate: .default / .uploading / .uploaded / .errorstate=EBUploadState.*
state=Uploaded with thumbnailstate=uploaded + hasThumbnail.hasThumbnail(true)hasThumbnail=true
hasLabel=yes/nolabel: String?label: String?label: String?
fileName: String?fileName: String?fileName: String?
progress: Doubleprogress: Double (0.0–1.0)progress: Float (0f–1f)
thumbnail placeholderFigma Slot → ViewBuilder@ViewBuilder thumbnailthumbnail: @Composable () -> Unit
disabled: Bool.disabled(true)enabled=false
onSelect / onRemoveonSelect / onRemoveonSelect / onRemove
SwiftUI
ios/Components/UploadFile/EBUploadFile.swift
Jetpack Compose
android/components/uploadfile/EBUploadFile.kt
Usage Snippets Planned API
// Default — empty state
EBUploadFile(label: "Proof of ID", onSelect: { url in
    // handle picked file
})

// Uploading
EBUploadFile(fileName: "GCash_File.png", progress: 0.2)
    .ebState(.uploading)

// Uploaded with thumbnail (Figma Slot)
EBUploadFile(fileName: "ID_proof.jpg", onRemove: { ... }) {
    AsyncImage(url: imageURL)
        .aspectRatio(contentMode: .fill)
        .clipShape(RoundedRectangle(cornerRadius: 4))
}
.ebState(.uploaded)

// Error
EBUploadFile(label: "Upload receipt",
    errorMessage: "Maximum file size: 20MB")
    .ebState(.error)
// Default — empty state
EBUploadFile(
    label = "Proof of ID",
    onSelect = { uri -> /* handle picked file */ }
)

// Uploading
EBUploadFile(
    state = EBUploadState.Uploading,
    fileName = "GCash_File.png",
    progress = 0.2f
)

// Uploaded with thumbnail (Figma Slot)
EBUploadFile(
    state = EBUploadState.Uploaded,
    fileName = "ID_proof.jpg",
    onRemove = { /* ... */ }
) {
    AsyncImage(
        model = imageUrl,
        contentDescription = null,
        modifier = Modifier.clip(RoundedCornerShape(4.dp))
    )
}

// Error
EBUploadFile(
    state = EBUploadState.Error,
    label = "Upload receipt",
    errorMessage = "Maximum file size: 20MB"
)
Accessibility
RequirementiOSAndroid
Role.accessibilityAddTraits(.isButton) when empty; announce as "Upload" when actionableRole.Button in semantics
File picked announcementAnnounce file name after selection via .accessibilityAnnouncementAccessibilityManager.announce()
Progress announcement.accessibilityValue("\(Int(progress * 100)) percent")stateDescription="$percent percent"
Error announcementInclude error message in accessibility label; use .isRejected traitsemantics { error(...) }
Remove buttonSeparate accessibility element: .accessibilityLabel("Remove \(fileName)")contentDescription="Remove $fileName"
Tap target72px height > 44pt minimum> 48dp minimum
Usage Guidelines

Do

Use the thumbnail slot for image uploads (ID photos, receipts) so users can verify the correct file was picked.

Don't

Show a generic thumbnail placeholder as the final state — either show the real thumbnail or use the plain uploaded state with just the filename.

Do

Always pair the default state with subtext listing accepted formats and size limits so users don't discover constraints only via error state.

Don't

Let users attempt uploads silently only to show an error — preempt format / size violations on the client side.

Do

Use the error state for client-side validation failures (size, format). Show a specific error message indicating what needs to change.

Don't

Use the error state for network failures during upload — those are transient. Show a toast or retry affordance instead.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic: input-field, Attach, Trash, Icon Placeholder, upload-file-progress, Subtext Message.
C2Variant & Property NamingNeeds FixhasLabel yes/no, state has "Upload error" with space, "Uploaded with thumbnail" is orthogonal.
C3Token CoverageNeeds FixLibrary-wide boder token typo. Thumbnail bg hardcoded.
C4Native MappabilityReadyMaps to PhotosPicker / DocumentPicker (iOS), GetContent / PickVisualMedia (Android).
C5Interaction State CoverageNeeds FixNo disabled, pressed, or focused states.
C6Asset & Icon QualityPartialThumbnail is a placeholder; Lottie dependency needs documentation.
C7Code Connect LinkabilityPendingBlocked by C2 cleanup.
Variants Inventory (10 total)

5 state × 2 hasLabel=10 variants. Clean matrix — every combination exists.

StatehasLabelCount
Defaultyes + no2
Uploadingyes + no2
Uploadedyes + no2
Upload erroryes + no2
Uploaded with thumbnailyes + no2
View full State × hasLabel breakdown (10 rows)
statehasLabelHeightNode ID
Defaultno98px18482:35065
Defaultyes120px18482:35071
Uploadingno117px18482:35084
Uploadingyes139px18482:35097
Uploadedno98px18482:35119
Uploadedyes120px18482:35126
Upload errorno98px18482:35142
Upload erroryes120px18482:35148
Uploaded with thumbnailno98px18482:35163
Uploaded with thumbnailyes120px18482:35173
1.0.0 — April 2026 Initial
Initial Assessment · node 18482:35064
Component assessed — 10 variants (5 state × 2 hasLabel). Lottie progress bar, thumbnail placeholder. Documented
Initial
Property naming issueshasLabel=yes/no, state="Upload error" has a space, "Uploaded with thumbnail" is orthogonal to the state axis. Open
C2 Open
Token typo — All border tokens spelled boder. Library-level rename needed. Open
C3 Open
Thumbnail bg hardcoded#0057E4 @ 5% not tokenized. Open
C3 Open
Missing states — No disabled, pressed, or focused. Open
C5 Open
Thumbnail placeholder + Lottie dependency — Thumbnail is not a slot; Lottie requires asset bundling. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open
Vertical Voucher ConsolidateReworkComponent link

A 162-wide vertical voucher tile composed of a Voucher Asset image (stacked at two fixed sizes — large 153h and small 100h — both nested in the same symbol and toggled via booleans) plus a content block with hardcoded title, description, price (PHP 100.00 with PHP 150.00 strikethrough), validity period, and two rows of hardcoded status badges ("Limited" / "Expiring" in row 1, "Hot" / "Discounted" in row 2). 1 symbol, no variants, 8 boolean toggles. All text and badge labels are frozen placeholder strings.

Consolidate into one Voucher Card with orientation + state axes
Vertical Voucher, Horizontal Voucher (5121:4533), and Voucher Card Horizontal (5119:1786) are three parallel records of the same component. Merge into a single Voucher Card with orientation: vertical | horizontal, state: default | limited | expiring | used | expired (borrowed from Voucher Card Horizontal), an image Slot that accepts Voucher Asset instances, and text slots for title / description / price / original price / validity. Drop the 8 booleans in favor of a single assetSize: large | small enum and a proper badges: [Badge] array.
Current Figma

Single symbol, 162 × 465. Both asset frames (large 153h, small 100h) ship stacked in the default state. Content block below holds 2 fixed rows of hardcoded badges, a hardcoded title, description, price, and validity period. Every text string is placeholder; every badge label is frozen.

DS Health
Reusable
Fail
All text content is hardcoded placeholder. Consumers cannot set a title, price, validity, or badge label without detaching. A "reusable" voucher component that can only render the sample "Buy Load Pre-seeded SKU Voucher Sample" string is not reusable.
Self-contained
Warn
The symbol does carry its own layout, spacing, and token-bound colors via main/vouchers/color/default/*. But it ships two asset frames bundled together, assuming consumers will turn one off — nothing enforces the mutual exclusion.
Consistent
Fail
Parallel to Horizontal Voucher and Voucher Card Horizontal — three components for one concept. Voucher Card Horizontal ships a proper state axis (Default/Limited/Expiring/Used/Expired); Vertical Voucher ships none. Property shape diverges across the family.
Composable
Warn
Nests a Voucher Asset instance (composition works) and Badge instances (composition works). But since the voucher content is locked placeholder, a parent screen cannot actually compose real voucher data into this component.
Behavior
AspectiOSAndroidFigmaNotes
Asset sizeassetSize: .large | .smallassetSize=Large | SmalllargeAsset + smallAsset booleansTwo booleans for a mutually exclusive choice. Should be a single enum.
Titletitle: Stringtitle: Stringheader boolean (string hardcoded)String is frozen in the symbol. Boolean only toggles visibility.
Price / originalprice: String, originalPrice: String?Sameamount boolean (strings hardcoded)"PHP 100.00" and "PHP 150.00" frozen; one boolean toggles both.
Validityvalidity: String?SamevalidityPeriod boolean (string hardcoded)"Validity: Dec 25 2022 - Jan 5 2023" frozen.
Status badgesbadges: [EBBadge]badges: List<EBBadge>prop1stRowBadges + prop2ndRowBadgesTwo fixed rows of 2 fixed badge labels each. Row-level visibility only — consumers can't pick which badges to render.
Statestate: .default | .limited | .expiring | .used | .expiredSame enumNot modelledAbsent entirely. Voucher Card Horizontal has it; Vertical Voucher does not.
Tap targetEntire card as Button with PlainButtonStyleCard with onClick + rippleNot modelledVouchers are always tappable; current symbol has no pressed/disabled states.
Resolved Issues
  • Voucher content tokens exist. Background (main/vouchers/color/default/bg), title (label-title), description (label-description), amount (label-amount), strikethrough amount (label-amount-original), and metadata (label-metadata) are all bound to the voucher component's variable collection. C3 · Token Coverage
Open Issues
  • Three parallel components for one concept. Vertical Voucher, Horizontal Voucher (5121:4533), and Voucher Card Horizontal (5119:1786) share the same anatomy — voucher asset image + title + description + price + validity + status badges — but ship as three separate components with divergent property shapes. This is a family-level consolidation, not a single-component fix. C4 · Native Mappability
  • No state axis. Voucher Card Horizontal ships Default / Limited / Expiring / Used / Expired as a proper state variant that drives background, label colors, and badge treatment. Vertical Voucher has no state concept — a used or expired vertical voucher cannot be rendered in greyed-out treatment. C5 · Interaction State Coverage
  • All text content is hardcoded placeholder. Title "Buy Load Pre-seeded SKU Voucher Sample", description "This is the description of the voucher.", price "PHP 100.00", original price "PHP 150.00", and validity "Validity: Dec 25 2022 - Jan 5 2023" are all frozen strings inside the symbol. Booleans toggle visibility but not content. Consumers cannot render a real voucher without detaching. C2 · Variant & Property Naming
  • Two asset sizes bundled in one symbol. The symbol contains both the large (153h) and the small (100h) Voucher Asset instances stacked; largeAsset and smallAsset booleans turn them on/off. The default 465h render shows both. This is a single enum (assetSize: large | small) masquerading as two booleans with implied mutual exclusion. C2 · Variant & Property Naming
  • Badges are row-level, not array-level.prop1stRowBadges and prop2ndRowBadges toggle rows of two hardcoded badges each ("Limited" + "Expiring" / "Hot" + "Discounted"). A real voucher with one "Limited" badge and nothing else cannot be rendered. Badges should be a composable array, not fixed rows. C2 · Variant & Property Naming
  • Layer structure is flat and unlabeled.large asset, small asset, content, badges, badges (duplicate), price. Two layers named badges, no semantic names for the title/description/validity text nodes. Consumers inspecting the Dev Mode output see anonymous text layers. C1 · Layer Structure & Naming
  • Voucher Asset nested image is raster with hardcoded "35% off" Badge. Inherits all the issues of Voucher Asset (5119:1664) — the discount amount is baked into the image-frame variant, not a property on the parent. A voucher offering "50% off" or "BUY1 TAKE1" cannot be rendered. C6 · Asset & Icon Quality
  • No native component maps to this shape. 8 booleans with hardcoded content do not map to any reasonable native API. A proper EBVoucherCard takes title, price, validity, badges array, and image as parameters — not eight visibility toggles. Code Connect has no 1:1 target. C4 · Native Mappability
  • Code Connect cannot link an 8-boolean symbol with frozen strings. Even if a mapping existed, swapping the "title" string or the badge labels would require detaching the component. Linkability requires real string/array properties first. C7 · Code Connect Linkability
Design Recommendations
  • Merge the three voucher cards into a single Voucher Card component. Vertical Voucher + Horizontal Voucher + Voucher Card Horizontal become one component with orientation: vertical | horizontal (swaps the layout axis) and state: default | limited | expiring | used | expired (borrowed from Voucher Card Horizontal). Target shape: 2 orientations × 5 states=10 variants instead of three separate components with divergent schemas. Family
  • Promote every text string to a property. Add title: String, description: String, price: String, originalPrice: String?, validity: String?. Retire the header / amount / description / validityPeriod booleans — visibility falls out of whether the string is empty. Property
  • Replace the two asset-size booleans with one assetSize enum.largeAsset + smallAsset become assetSize: large | small | none. Nothing currently prevents both being on simultaneously (which is the default render). A single enum makes the choice explicit and mutually exclusive. Property
  • Adopt a Figma Slot for the voucher image. Replace the hardcoded Voucher Asset nested instance with a Slot that accepts any Voucher Asset variant (or a partner brand illustration). The discount badge should be composed on top of the slot, not baked into the asset. Slot
  • Replace fixed badge rows with a composable badges array. Drop prop1stRowBadges / prop2ndRowBadges. Expose a badges Slot that accepts 0..n Badge instances, wraps when it runs out of width. Consumers choose which badges apply ("Limited" alone, "Hot" + "Discounted" + "New", etc.). Slot
  • Add the state axis missing from Vertical Voucher. Used and Expired vouchers render in greyed-out treatment with muted labels and a dim asset overlay — a pattern Voucher Card Horizontal already ships. Port the same 5-state treatment to the unified Voucher Card. State
  • Rename duplicated badges layers. The two badge-row frames are both named badges. After the consolidation above they should collapse into a single badges-slot layer; until then, name them badges-row-1 / badges-row-2 to disambiguate. Rename
  • Document that Voucher Card is the tap target. Vouchers are always tappable entry points to the voucher detail screen. The unified component should ship a pressed/focused state on the card frame; the handoff is an onTap closure, not an internal CTA button. Docs
Native equivalent Planned API

Native models the voucher family as a single EBVoucherCard with orientation, state, text properties, and a Voucher Asset image parameter. The current Vertical Voucher's 8 booleans do not have a 1:1 native analog; consumers write real strings and state enums instead.

// Proposed API — unified Voucher Card (vertical orientation)
EBVoucherCard(
    orientation: .vertical,
    state: .limited,
    title: "Buy Load Voucher",
    description: "Get a discount on any prepaid load.",
    price: "PHP 100.00",
    originalPrice: "PHP 150.00",
    validity: "Dec 25 2022 - Jan 5 2023",
    badges: [
        EBBadge("Limited", style: .informationHeavy),
        EBBadge("Expiring", style: .negativeHeavy)
    ]
) {
    EBVoucherImageFrame(size: .large, discount: "35% off") {
        Image("voucher-food").resizable().scaledToFill()
    }
}
.onTapGesture { // navigate to voucher details }

// Used/expired treatment
EBVoucherCard(orientation: .vertical, state: .used, title: "Redeemed", price: "PHP 100.00") {
    EBVoucherImageFrame(size: .small) {
        Image("voucher-food").resizable().scaledToFill()
    }
}
// Proposed API — unified Voucher Card (vertical orientation)
EBVoucherCard(
    orientation = EBVoucherOrientation.Vertical,
    state = EBVoucherState.Limited,
    title = "Buy Load Voucher",
    description = "Get a discount on any prepaid load.",
    price = "PHP 100.00",
    originalPrice = "PHP 150.00",
    validity = "Dec 25 2022 - Jan 5 2023",
    badges = listOf(
        EBBadge("Limited", style = EBBadgeStyle.InformationHeavy),
        EBBadge("Expiring", style = EBBadgeStyle.NegativeHeavy)
    ),
    onClick = { /* navigate to voucher details */ },
    image = {
        EBVoucherImageFrame(size = EBVoucherImageFrameSize.Large, discount = "35% off") {
            Image(
                painter = painterResource(R.drawable.voucher_food),
                contentDescription = null,
                contentScale = ContentScale.Crop
            )
        }
    }
)
Property Mapping (Proposed)

The current 8 booleans do not map cleanly to native. The table below shows the target shape after the family consolidation — each row captures what the proposed EBVoucherCard replaces from the current Vertical Voucher.

Current FigmaProposed FigmaSwiftUIComposeNotes
orientationorientation: EBVoucherOrientationorientation=EBVoucherOrientationvertical | horizontal — collapses 3 components into 1
statestate: EBVoucherStatestate=EBVoucherStatedefault | limited | expiring | used | expired (port from Voucher Card Horizontal)
largeAsset + smallAssetassetSizeassetSize: .large | .small | .noneSameTwo booleans → one enum
header (boolean, string frozen)title (string)title: Stringtitle: StringVisibility=whether string is empty
description (boolean, string frozen)description (string)description: String?SameSame
amount (boolean, PHP 100 / PHP 150 frozen)price + originalPriceprice: String, originalPrice: String?SameTwo strings, strikethrough applied to originalPrice
validityPeriod (boolean, string frozen)validity (string)validity: String?SameSame
prop1stRowBadges + prop2ndRowBadgesbadges Slotbadges: [EBBadge]badges: List<EBBadge>Composable array, wraps on overflow
nested Voucher AssetImage Slottrailing closureimage: @Composable () -> UnitAccepts any EBVoucherImageFrame instance
onTap: () -> VoidonClick: () -> UnitCard is the tap target
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkTwo frames both named badges. Text layers are unnamed. Asset frames large asset / small asset should be one frame with an assetSize property.
C2Variant & Property NamingRework8 booleans where most should be strings (title, price, validity) or a Slot (badges). largeAsset + smallAsset should be one enum. All content is frozen placeholder.
C3Token CoverageReadyBackground, title, description, amount, strikethrough amount, and metadata are all bound to main/vouchers/color/default/*. Typography uses named text styles (Primary/Multi-line Label/Base, Primary/Label/Small, Secondary/Default/Caption, Secondary/Default/Fine).
C4Native MappabilityReworkParallel to 2 other voucher components with divergent schemas. Native has one EBVoucherCard, not three. 8 booleans with frozen strings have no native analog.
C5Interaction State CoverageReworkNo state axis at all. Voucher Card Horizontal ships Default/Limited/Expiring/Used/Expired; Vertical Voucher has none. No pressed/focused/disabled on the card frame either.
C6Asset & Icon QualityReworkInherits Voucher Asset's raster + hardcoded "35% off" badge. The discount amount is not a property on the parent Voucher.
C7Code Connect LinkabilityReworkCannot map. Frozen strings, row-level badge toggles, and two-boolean asset size do not have 1:1 native parameters. Linkability requires the family consolidation first.
Variants Inventory (1 total)

Single symbol, no variant axes declared. All configurability is through 8 boolean property toggles on the lone instance.

Node IDNameDimensionsProperty toggles
5119:1635Vertical Voucher162 × 465 (with both assets + all content on)amount, description, header, largeAsset, prop1stRowBadges, prop2ndRowBadges, smallAsset, validityPeriod — all boolean, all default true

Nested instances (both carry their own hardcoded "35% off" Badge):

Node IDLayerKindDimensions
5119:1637large asset > Voucher AssetVoucher Asset instance162 × 153
5119:1639small asset > Voucher AssetVoucher Asset instance162 × 100
5119:1642badges > Badge "Limited"Badge instance (information/heavy)auto
5119:1643badges > Badge "Expiring"Badge instance (negative/heavy)auto
5119:1645badges > Badge "Hot"Badge instance (destructive)auto
5119:1646badges > Badge "Discounted"Badge instance (brand/heavy)auto
1.0.0 — April 2026 Initial
Initial Assessment · node 5119:1635
Assessed with Consolidate verdict. Single symbol with no variants, 8 boolean toggles, and entirely hardcoded content. Parallel to Horizontal Voucher (5121:4533) and Voucher Card Horizontal (5119:1786). Open
Design Decision
Proposed family-level merge. Collapse the 3 voucher components into one Voucher Card with orientation + state axes, text slots (title, description, price, originalPrice, validity), a badges array, and a voucher image Slot. Target: 2 × 5=10 variants instead of 3 divergent components. Open
Design Decision
View Only Field KeepNeeds RefinementComponent link

A read-only data display field with a muted label, prominent value, optional subtext description, and an optional trailing slot (badge, text link, or icon). 8 variants across variant (Default / with Badge / with Text Link / with Icon) × Size (Default / Large). Part of the Form Elements group — used in profile screens, transaction details, account information displays, and read-only settings.

Minor refinements before handoff
Property variant is overloaded with 4 trailing content types — consider renaming to trailingContent (C2). Checkmark uses raster IMG instead of a vector icon (C6).
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns.

Account Details
Live Preview

Toggle variant and size to see the view-only field update in real time.

Properties
variant
Size
hasCheckmark
hasDescription
DS Health
Reusable
Pass
Read-only data display used across profile screens, transaction details, account information, settings displays, and confirmation flows. Highly reusable across contexts.
Self-contained
Pass
Bundles label, value, optional subtext, and optional trailing slot (badge/link/icon). All colors token-bound. Typography scales per size (Default / Large).
Consistent
Partial
Property variant is overloaded — conflates 4 different trailing content types (none/badge/textLink/icon) into one enum. Should be renamed to trailingContent. Size=Default isn't a size name — should be Regular.
Composable
Pass
Composes cleanly in list rows, profile pages, transaction detail sheets, and settings screens. Uses Badge component instance for the "with Badge" variant — good compositional inheritance.
Behavior
StateiOSAndroidFigma PropertyNotes
DefaultYesYesvariant=DefaultLabel + value only, no trailing slot.
with BadgeYesYesvariant=with BadgeBadge instance in the trailing slot (e.g. "Change" status chip).
with Text LinkYesYesvariant=with Text LinkText link in the trailing slot (e.g. "What is this?" contextual help).
with IconYesYesvariant=with IconEdit icon (pencil) in the trailing slot — typically navigates to an editable state.
CheckmarkYesYeshasCheckmark=trueDisplays a 13×13 checkmark next to the value (e.g. verified status).
DescriptionYesYeshasDescription=trueShows a subtext message below the value (e.g. "Message content" helper text).
Open Issues
  • Property variant is overloaded. Conflates 4 different trailing content types (none, badge, text link, icon) into a single enum. Better expressed as a trailingContent property with semantic values, or split into separate boolean properties per slot. C2 · Variant & Property Naming
  • Property value Size=Default isn't a size name. "Default" describes the starting state, not a size. Rename to Regular (or Small) for consistency with standard size naming across the DS. C2 · Variant & Property Naming
  • Checkmark is a raster image. Uses imgCheck from Figma CDN instead of a vector icon instance. Replace with a vector from the DS icon library for clean cross-platform rendering at any DPI. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until the variant rename and asset fix land. C7 · Code Connect Linkability
Design Recommendations
  • Rename variant to trailingContent. Values: none / badge / textLink / icon. Clearer intent, cleaner native enum mapping, no invalid combinations. Rename
  • Rename Size=Default to Size=Regular. (Or Size=Small, depending on where it sits in the size scale.) Aligns with the Small/Medium/Large convention other components use. Rename
  • Replace the raster checkmark with a vector icon instance. Drop in a real Icon from the DS icon library — ensures crisp rendering at any DPI on iOS and Android. Asset
  • Expose label, value, and description as text overrides. Designers can then customize copy without editing the master component. Property
  • Add an error/warning state. Read-only fields sometimes need to convey validation status (e.g. "verification pending", "expired"). A status prop covering none / warning / error makes this explicit. State
Variants

4 variants across 2 sizes=8 total. All share the same base layout (label + value + optional subtext) with different trailing slot content. Large size uses bigger, bolder value typography.

Default

Label + value only. Used for plain read-only information display.

with Badge

Label + value + Badge instance in the trailing slot. Uses Badge component (layout=overflow or similar) for status indicators.

with Icon

Label + value + 24×24 icon (typically Edit pencil) in the trailing slot. Icon typically navigates to an editable state.

Colors by State

Display-only component — no interaction states. All colors token-bound.

RoleTokenValue
Labelmain/view-only-field/color/label#6780A9
Value textmain/view-only-field/color/text#0A2757
Text linkmain/view-only-field/color/label-link#005CE5
Edit iconmain/view-only-field/color/icon#005CE5
Subtext descriptionmain/subtext-message/primary/label#6780A9
Badge bg (default)main/badge/information/light/background#E5F1FF
Badge labelmain/badge/information/light/label#005CE5
Layout
PropertyDefaultLarge
Height57px71px
Width360px360px
Label-value gap8px8px
Subtext top padding4px4px
Trailing icon size24 × 2424 × 24
Checkmark size13 × 1313 × 13
Typography
LayerDefault SizeLarge Size
LabelPrimary/Label/Light/Small — 14px SemiboldPrimary/Label/Light/Base — 16px Semibold
Value textPrimary/Label/Light/Base — 16px SemiboldPrimary/Headlines/Section — 22px Bold
Subtext descriptionSecondary/Bold/Small Caption — 10px Semibold (BarkAda)Secondary/Bold/Caption — 12px Semibold (BarkAda)
Text link12px Semibold (BarkAda)12px Semibold (BarkAda)
Badge labelPrimary/Label/Fine — 12px BoldPrimary/Label/Fine — 12px Bold
Installation Planned API

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")
}

Package not yet published. These are the planned distribution paths.

Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
variant=Default(default — no trailing)trailing=nullNo trailing slot content
variant=with Badge renametrailing: { EBBadge(...) }trailing={ EBBadge(...) }Pass a Badge composable to the trailing slot
variant=with Text Link renametrailing: { Button("...") {} }trailing={ TextButton(...) }Pass a text button
variant=with Icon renametrailing: { Image(...) }trailing={ Icon(...) }Pass an icon (tap handler optional)
Size=Default rename.controlSize(.regular)size=EBFieldSize.RegularRename Size=Default to Size=Regular
Size=Large.controlSize(.large)size=EBFieldSize.LargeBigger, bolder value typography
hasCheckmark (boolean)isVerified: BoolisVerified: BooleanShows checkmark next to value
hasDescription (boolean)description: String?description: String?Optional subtext below value
SwiftUI
ios/Components/FormElements/EBViewOnlyField.swift
Jetpack Compose
android/components/form/EBViewOnlyField.kt
Usage Snippets Planned API
Default
EBViewOnlyField(
    label: "Mobile Number",
    value: "+63 917 123 4567",
    description: "This is your verified number"
)
EBViewOnlyField(
    label = "Mobile Number",
    value = "+63 917 123 4567",
    description = "This is your verified number"
)
with Badge
EBViewOnlyField(
    label: "Account Status",
    value: "Active",
    trailing: {
        EBBadge("Change", state: .information, level: .light)
    }
)
EBViewOnlyField(
    label = "Account Status",
    value = "Active",
    trailing = {
        EBBadge(
            text = "Change",
            state = BadgeState.Information,
            level = BadgeLevel.Light
        )
    }
)
with Icon (Edit)
EBViewOnlyField(
    label: "Email Address",
    value: "dhar@frostdesigngroup.com",
    trailing: {
        Button(action: { /* navigate to edit */ }) {
            Image(systemName: "pencil")
        }
    }
)
EBViewOnlyField(
    label = "Email Address",
    value = "dhar@frostdesigngroup.com",
    trailing = {
        IconButton(onClick = { /* navigate to edit */ }) {
            Icon(Icons.Default.Edit, contentDescription = "Edit")
        }
    }
)
Accessibility
RequirementiOSAndroid
Accessibility label.accessibilityLabel("\(label): \(value)")contentDescription="$label: $value"
Trailing action (with Icon)Button with .accessibilityLabel("Edit \(label)")IconButton with contentDescription="Edit $label"
Text linkButton with .accessibilityHint("Opens help")TextButton with semantics { role=Role.Button }
Min touch target (trailing action)44 × 44 pt48 × 48 dp
Usage Guidelines

Do

Use View Only Field to display verified or system-set data (phone number, email, account status) where the user shouldn't edit directly.

Don't

Use for editable input — use Input Field, Labeled Field, or Select Field instead. Read-only fields imply the value is final or managed elsewhere.

Do

Use the with Icon variant with a pencil to indicate the field can be edited in a separate screen — provides a clear tap target.

Don't

Rely on the icon alone without an accessibility label — screen readers need to announce the trailing action's purpose.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: container, content-container, text-container, badge-container, text-link-container, icon-container. Clean hierarchy.
C2Variant & Property NamingPartialProperty variant is overloaded (4 trailing content types as one enum). Size=Default isn't a size name — should be Regular.
C3Token CoverageReadyAll colors bound to design tokens. Space, typography, and badge tokens all present.
C4Native MappabilityReadyMaps cleanly to SwiftUI VStack / Compose Column with label + value + optional trailing closure.
C5Interaction State CoverageReadyDisplay-only component — no interaction states expected. Trailing actions (icon/text link) handle their own tap states.
C6Asset & Icon QualityPartialCheckmark uses a raster IMG from Figma CDN. Edit icon is a clean vector. Replace checkmark with an icon component instance.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Variants Inventory (8 total)

4 variant values × 2 Size values. Two booleans (hasCheckmark, hasDescription) apply to all variants.

variantSizeHeightNode ID
DefaultDefault5718403:4521
with BadgeDefault5718403:4533
with Text LinkDefault5718403:4547
with IconDefault5718403:4561
DefaultLarge7118403:4575
with BadgeLarge7118403:4587
with Text LinkLarge7118403:4601
with IconLarge7118403:4615
1.0.0 — April 2026 Initial
Initial Assessment · node 18403:4520
Component assessed — 8 variants documented across variant (Default/with Badge/with Text Link/with Icon) × Size (Default/Large). Read-only data display field with label, value, optional subtext, and optional trailing slot. Grouped under Form Elements. Documented
Initial
Overloaded variant propertyvariant conflates 4 different trailing content types into one enum. Should be renamed to trailingContent or split into semantic properties. Open
C2 Open
Size=Default should be Size=Regular — "Default" isn't a size name; describes starting state, not dimension. Open
C2 Open
Raster checkmark — Checkmark uses a raster IMG from Figma CDN instead of a vector icon instance. Replace with icon library component. Open
C6 Open
Code Connect mappings — No CLI mappings registered yet. Open
C7 Open
Visual Popup FixNeeds RefinementComponent link

A 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).

Open issues remain
Variant naming mixes paradigms — 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).
In Context

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.

Hero imageOkay
Live Preview

Toggle Type to see each variant. Hero image, title, description, and CTA(s) update accordingly.

Properties
Type
DS Health
Reusable
Pass
Three layouts cover info modals, confirmation prompts (single + dual CTA), and onboarding popups (Version 2). Fixed 320 / 312 px width fits standard mobile dialog patterns.
Self-contained
Pass
Carries its own bg (main/modal-popup/color/bg), Shadow/Depth 0, radius/radius-2, and 24px padding. Composes Button instances rather than redefining button styles.
Consistent
Warn
Variant naming mixes three paradigms: Default (generic), 2 CTA (count), Version 2 (version). Native enums need a single semantic axis — e.g. single-cta / dual-cta / dismissible. C2
Composable
Pass
All CTAs are real Button instances. Close icon (V2) is a vector instance. Hero image is the only non-component child — see C6.
Behavior
StateiOSAndroidFigma PropertyNotes
Single CTAYesYesType=DefaultHero (180px) + title + description + primary CTA. Use for info or single-action confirm.
Dual CTAYesYesType=2 CTAAdds a secondary outline + tertiary text button below primary. Use for cancel/confirm pairs.
Dismissible (V2)YesYesType=Version 2Preamble label + title with close icon + content-first layout. Use for onboarding/tutorial popups.
Destructive / Error / LoadingN/AN/ANo variants for destructive confirms or async/loading states. C5
Open Issues
  • Variant naming mixes paradigms.Default (generic), 2 CTA (count), and Version 2 (version) can't coexist as one enum. Collapse to a single semantic axis: single-cta / dual-cta / dismissible. C2 · Variant & Property Naming
  • No destructive / error / loading state coverage. Engineers must improvise these for "Cancel transaction?", error confirms, and async submit flows. C5 · Interaction State Coverage
  • Hero image is a raster placeholder with "Replace me" overlay. Should be a swappable Image slot (component instance) so product teams override per-popup without editing the master. C6 · Asset & Icon Quality
  • Code Connect mappings not registered. Blocked until the variant naming and asset-slot issues above are resolved. C7 · Code Connect Linkability
Design Recommendations
  • Collapse Type to one semantic enum. Values: single-cta, dual-cta, dismissible. Eliminates the version/count/default mix and maps cleanly to an EBVisualPopupKind enum. Property
  • Replace the raster Modals Asset with a swappable Image slot. A component placeholder that product teams can instance-swap with their illustration — matches the pattern Avatar uses for its image type. Slot
  • Add a destructive mode. Whether as a boolean or a kind=destructive variant — lets destructive confirms (Cancel / Logout / Delete) use red CTAs without bespoke overrides. State
  • Add a loading state for the primary CTA. Async submits in popups currently have no documented affordance — reuse the planned Button loading state rather than invent a new pattern. State
Variants

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.

Default — single primary CTA

Hero image (320 × 180, 16:9) + title + 2-line description + single primary CTA. Use for informational modals or single-action confirms ("Okay").

2 CTA — primary outline + tertiary text

Same hero + title + description as Default, then a secondary outline button on top of a tertiary text button. Use for confirm/cancel pairs.

Version 2 — preamble + close icon, content-first

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.)

Colors by Variant

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).

RoleTokenValue
Modal backgroundmain/modal-popup/color/bg#FFFFFF
Title labelmain/modal-popup/color/label#0A2757
Description labelmain/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 containerbg/color-bg#F6F9FD
Layout
PropertyTokenValue
Default / 2 CTA width320px
Version 2 width312px
Hero image (Default / 2 CTA)320 × 180 (16:9)
Hero image (V2)280 × 180, 10px radius
Body paddingspace/space-2424px
CTA group padding (vertical)space/space-2424px
2 CTA gap between buttonsspace/space-88px
V2 inner container paddingspace/space-1616px h, 16t / 24b
Corner radiusradius/radius-26px
ShadowShadow/Depth 00 0 4px #E8EEF2C9
Close icon (V2)24 × 24
Typography
ElementDS text styleSpec
TitlePrimary/Headlines/SectionHeyMeow Rnd Bold · 22 / 26
DescriptionSecondary/Default/BaseBarkAda Medium · 14 / 20
Preamble (V2)Primary/Label/TinyHeyMeow Rnd Bold · 10 / 10 · +0.25
CTA labelPrimary/Label/LargeHeyMeow Rnd Bold · 18 / 18 · +0.25
Installation Planned API

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")
}
Property Mapping
Figma PropertySwiftUI ParamCompose ParamNotes
Type=Default.ebKind(.singleCTA)kind=EBVisualPopupKind.SingleCTAHero + title + description + primary CTA
Type=2 CTA.ebKind(.dualCTA)kind=EBVisualPopupKind.DualCTAAdds secondary outline + tertiary text button
Type=Version 2.ebKind(.dismissible)kind=EBVisualPopupKind.DismissiblePreamble + close icon + content-first layout
Hero image (raster)heroImage: ImageheroImage: PainterCurrently a placeholder; should become a swappable slot
CTA buttonsprimary / secondary / tertiary: EBButtonprimary / secondary / tertiary: @ComposableCompose Button instances directly
SwiftUI
ios/Components/VisualPopup/EBVisualPopup.swift
Jetpack Compose
android/components/visualpopup/EBVisualPopup.kt
Usage Snippets Planned API
// 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 */ }
)
Accessibility
RequirementiOSAndroid
Modal trait / rolePresent via .sheet or .alert — VoiceOver announces as modalDialog announces as modal; TalkBack focus trapped inside
Focus trapAutomatic with .sheetAutomatic with Dialog — set dismissOnClickOutside=false for confirm popups
Close button label (V2).accessibilityLabel("Close")contentDescription="Close"
Hero imageIf decorative: .accessibilityHidden(true). If informative: provide a label.Same — contentDescription=null for decorative, otherwise describe
Tap targetsCTAs use Button which meets HIG 44ptCTAs meet Material 48dp
Destructive roleCurrently undefined — needs role: .destructive when state landsCurrently undefined — needs Button destructive colors when state lands
Usage Guidelines

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.

Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReadySemantic names: Modals Asset, body, header, CTA - Base Button Group, Close.
C2Variant & Property NamingNeeds FixProperty values mix paradigms: Default / 2 CTA / Version 2. Should be one semantic axis.
C3Token CoverageReadyAll colors, spacing, radii, shadow, and typography bound to tokens.
C4Native MappabilityReadyMaps to .sheet / .alert on iOS and Dialog / AlertDialog on Android.
C5Interaction State CoverageNeeds FixNo destructive, error, or loading variants. Close affordance only on Version 2.
C6Asset & Icon QualityNeeds FixHero is a flat raster placeholder with "Replace me" overlay. Should be a swappable Image slot.
C7Code Connect LinkabilityPendingNo CLI mappings registered yet.
Variants Inventory (3 total)
TypeWidthHeroCTAsNode ID
Default320px320 × 180 (raster)1 primary18477:23789
2 CTA320px320 × 180 (raster)1 outline + 1 text18477:23797
Version 2312px280 × 180 (raster, in container)1 primary + close icon18477:23806
1.0.0 — April 2026 Initial
Initial Assessment · node 18477:23788
Component assessed — 3 variants (Default / 2 CTA / Version 2). Hero image, title, description, CTA(s). Used for confirms, success states, and onboarding popups. Documented
Initial
Variant naming mixes paradigmsDefault (generic), 2 CTA (count), Version 2 (version). Should be a single semantic axis (single-cta / dual-cta / dismissible). Open
C2 Open
No destructive/error/loading state — Engineers must improvise these for cancel/delete confirms and async submits. Open
C5 Open
Hero image is a raster placeholder — "Replace me" overlay on a flat Modals Asset image. Should be a swappable Image slot via instance swap. Open
C6 Open
Code Connect mappings — No CLI mappings registered yet. Open
C7 Open
Voucher Asset RestructureReworkComponent link

A ticket-shaped raster image used as the visual for voucher placements — a full-bleed photo clipped into a notched-ticket frame, with a Badge instance anchored to the right edge displaying "35% off". The current component has four property axes: type (midfi | hifi), size (small | large), use case (10 category values: default, restaurant, vacation, beverage, snack, fashion, party, meal, games, food), and orientation (default | horizontal). 20 variants ship out of a possible 40 in the cartesian space. Every variant is a hardcoded image fill; the "35% off" string is baked in.

Collapse use-case variants into an image Slot
Use case is illustration content — it does not belong as a Figma variant axis. A new category launch should not require a new DS variant. Retire type=midfi|hifi (authoring fidelity, not product concern) and use case (content, not variant). Ship a single Voucher Image Frame with size: small | large + orientation: vertical | horizontal, plus an image Slot and a discount string. Category artwork lives in a separate asset library, instance-swapped into the Slot.
Current Figma

20 variants across type × size × use case × orientation. The top row is the midfi "Placeholder image" wireframes; the rest are hifi raster photos per category. Every tile has the same hardcoded "35% off" Badge pinned to the right edge.

DS Health
Reusable
Fail
Tied to specific illustration categories. Consumers cannot supply their own voucher image — they must pick from the 10 baked-in use cases or detach the component.
Self-contained
Warn
Ships the ticket frame and Badge instance, but the voucher amount ("35% off") is hardcoded into the image, not a property. Consumers can't change the discount without swapping the whole variant.
Consistent
Fail
Property schema is contradictory. type=midfi is an authoring state, use case is content, size and orientation are geometry. Cartesian space is sparse — only default and food exist in horizontal; most use cases don't. New categories require new variants.
Composable
Warn
Used inside Vertical Voucher, Horizontal Voucher, and Voucher Card Horizontal — composition works. But since content is locked to 10 categories, the parent voucher components inherit the same constraint.
Behavior
AspectiOSAndroidFigmaNotes
Image sourceImage from asset catalogImage from drawable resourcesuse case enumNative doesn't switch on an enum — it reads a named asset. The Figma enum is a Figma-only crutch.
SizeFixed frames: 162×100 (small), 162×153 (large)Samesize enumTwo fixed sizes; horizontal variants override to 336×144.
Discount labelEBBadge("35% off", style: .brandHeavy)EBBadge("35% off", style=BrandHeavy)Badge instanceCurrently hardcoded string. Should be a discount property on the parent component.
Clipping shapeCustom Path with notch cutoutsCustom Shape with notchesMaskTicket notch + dashed center line is the only DS-specific visual primitive here.
Open Issues
  • Use-case axis promotes illustration content into variants. 10 use-case values (restaurant, vacation, beverage, snack, fashion, party, meal, games, food, default) mean every new voucher category ships a new DS variant. This is the same anti-pattern Ad Space retired. Consumers should instance-swap an illustration into a Slot, not pick from a DS-owned enum. C2 · Variant & Property Naming
  • Fidelity axis type=midfi|hifi is not a product concern. Mid-fidelity "Placeholder image" wireframes vs hi-fidelity final photos is an authoring-workflow state, not a variant the product should expose. Native has no concept of "mid-fidelity"; this axis will not map. Same pattern retired on Ad Space. C2 · Variant & Property Naming
  • Cartesian space is sparse (20/40 shipped). 10 use cases × 2 sizes × 2 orientations=40 possible variants. Only 20 actually exist. Horizontal orientation only exists for default (midfi wireframe) and food (hifi GrabFood). Most use cases have no horizontal artwork. The matrix is authored ad-hoc, not systematically. C1 · Layer Structure & Naming
  • Discount amount "35% off" is hardcoded. The Badge instance inside every variant renders a fixed "35% off" string. There is no property to change the voucher discount — consumers with a 50% off or BUY1TAKE1 promo must detach. C2 · Variant & Property Naming
  • All artwork is raster image fill. 19 of 20 variants are photographic images; 1 is a grey placeholder. No vector illustrations, no token-driven coloring. Images live inside the component, not in a separate asset library, which means they ship with every DS publish and bloat the library. C6 · Asset & Icon Quality
  • No native component to map to. Native handoff for category illustration is an asset catalog entry (Image("voucher-restaurant-large")) — not a Code-Connected DS component. Only the ticket frame + badge overlay warrants a component on native. The use-case enum has no iOS/Android correlate. C4 · Native Mappability
  • Code Connect cannot map a 10-value content enum. Even if a mapping existed, it would force 10 named image assets per size × orientation combination into the codebase, locked to the Figma enum. Any new category requires a Figma variant ship + a native code ship in lockstep. C7 · Code Connect Linkability
Design Recommendations
  • Collapse into one Voucher Image Frame component. Retire use case and type. Keep size: small | large and orientation: vertical | horizontal. Add an image Slot that accepts any illustration instance, plus a discount string (default "35% off") that drives the Badge. Target schema: 2 × 2=4 variants instead of 20. Property
  • Move category artwork to a separate Voucher Illustrations library. Ship a sibling library with named illustration instances organized by category (Food, Travel, Entertainment, etc.). Consumers instance-swap the illustration into the Voucher Image Frame's Slot. New categories are added to the library, not to the DS component. Family
  • Adopt a Figma Slot for the image fill. The current pattern (hardcoded image fill per variant) prevents consumers from using branded partner artwork (GrabFood, Tim Hortons, etc. — already visible in current variants) without detaching. A Slot lets product teams bring their own asset. Slot
  • Promote discount to a string property. Extract the "35% off" text out of the variants so vouchers can show any discount format ("50% off", "BUY1 TAKE1", "₱100 OFF"). The Badge instance is already composable; only the string needs exposing. Property
  • Close the orientation matrix. Either commit to supporting horizontal for every size (ship the missing artwork through the Slot), or drop orientation as an axis and let parents set aspect ratio. The current sparse matrix (2 horizontal variants out of 20) signals the axis is accidental. Property
  • Native handoff is asset catalog, not Code Connect. Document that voucher illustrations ship via Assets.xcassets (iOS) and res/drawable/ (Android). The DS component's Code Connect mapping should cover only the ticket frame + Badge + discount string; category art is a product-team responsibility. Docs
Native equivalent Planned API

Native splits this into two layers: (1) EBVoucherImageFrame, the ticket-shape container with notch + dashed separator + Badge overlay, and (2) category artwork, which lives in the platform asset catalog and is passed in as an Image. The current Figma use case enum is not modelled natively — consumers pick the asset name directly.

// Proposed API — restructured component
EBVoucherImageFrame(
    size: .large,
    orientation: .vertical,
    discount: "35% off"
) {
    Image("voucher-restaurant")   // from asset catalog
        .resizable()
        .scaledToFill()
}

// Horizontal hero voucher
EBVoucherImageFrame(size: .large, orientation: .horizontal, discount: "50% off") {
    Image("voucher-food-grabfood").resizable().scaledToFill()
}
// Proposed API — restructured component
EBVoucherImageFrame(
    size = EBVoucherImageFrameSize.Large,
    orientation = EBVoucherImageFrameOrientation.Vertical,
    discount = "35% off"
) {
    Image(
        painter = painterResource(R.drawable.voucher_restaurant),
        contentDescription = null,
        contentScale = ContentScale.Crop
    )
}

// Horizontal hero voucher
EBVoucherImageFrame(
    size = EBVoucherImageFrameSize.Large,
    orientation = EBVoucherImageFrameOrientation.Horizontal,
    discount = "50% off"
) {
    Image(painterResource(R.drawable.voucher_food_grabfood), null, contentScale = ContentScale.Crop)
}
Property Mapping (Proposed)
Figma (proposed)SwiftUIComposeNotes
sizesize: EBVoucherImageFrameSizesize=EBVoucherImageFrameSizesmall (162×100) | large (162×153)
orientationorientation: .vertical | .horizontalorientation=Vertical | HorizontalHorizontal large is 336×144
discount (string)discount: Stringdiscount: StringRenders via EBBadge overlay
Image Slotcontent: () -> Imagecontent: @Composable () -> UnitConsumer-supplied illustration
use caseRetired — asset name is consumer's choice
type (midfi/hifi)Retired — authoring fidelity, not a product axis
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingRework20 variants with sparse orientation coverage — only default and food have horizontal artwork. Matrix is not closed.
C2Variant & Property NamingReworkuse case is content, not a variant. type=midfi|hifi is authoring fidelity, not a product axis. "35% off" is hardcoded.
C3Token CoverageRefineBadge uses token-bound colors and typography. Image fills bypass tokens (raster).
C4Native MappabilityReworkNo native correlate for a 10-value illustration enum. Correct handoff is asset catalog + lean frame component.
C5Interaction State CoverageN/ADisplay-only artwork; no interactive states.
C6Asset & Icon QualityReworkAll raster. 19/20 variants are photos; 1 placeholder. Assets should live in a sibling library, not the component.
C7Code Connect LinkabilityReworkCannot map a 10-value content enum 1:1 to a native parameter. Linkability requires collapsing use case first.
Variants Inventory (20 total)

Current shape: type × size × use case × orientation. Cartesian space is 2 × 2 × 10 × 2=80, only 20 shipped. Heavily skewed toward type=hifi / orientation=Default (16 of 20). midfi exists only for use case=Default; every other use case has only hifi artwork.

Use caseSizes shippedOrientations shippedCount
Default (midfi)small, largeDefault, horizontal3
Restaurantsmall, largeDefault2
Vacationsmall, largeDefault2
Beveragesmall, largeDefault2
Snacksmall, largeDefault2
Fashionsmall, largeDefault2
Partysmall, largeDefault2
Mealsmall, largeDefault2
Gamessmall, largeDefault2
Foodlargehorizontal1
Total20
View full type × size × use case × orientation breakdown (20 rows)
Node IDtypesizeuse caseorientationDimensions
5119:1665midfismallDefaultDefault162 × 100
5119:1669midfilargeDefaultDefault162 × 153
5119:1673midfilargedefaulthorizontal336 × 144
5119:1682hifismallrestaurantDefault162 × 100
5119:1687hifilargerestaurantDefault162 × 153
5119:1692hifismallvacationDefault162 × 100
5119:1697hifilargevacationDefault162 × 153
5119:1702hifismallbeverageDefault162 × 100
5119:1707hifilargebeverageDefault162 × 153
5119:1712hifismallsnackDefault162 × 100
5119:1717hifilargesnackDefault162 × 153
5119:1722hifismallfashionDefault162 × 100
5119:1727hifilargefashionDefault162 × 153
5119:1732hifismallpartyDefault162 × 100
5119:1737hifilargepartyDefault162 × 153
5119:1742hifismallmealDefault162 × 100
5119:1747hifilargemealDefault162 × 153
5119:1752hifismallgamesDefault162 × 100
5119:1757hifilargegamesDefault162 × 153
5119:1762hifilargefoodhorizontal336 × 144
1.0.0 — April 2026 Initial
Initial Assessment · node 5119:1664
Assessed with Restructure verdict. 20-variant sprawl driven by a 10-value use case enum and a type=midfi|hifi fidelity axis — both are anti-patterns. Open
Design Decision
Proposed restructure. Collapse to a single Voucher Image Frame with size + orientation + image Slot + discount string. Move category artwork to a sibling asset library. Target: 4 variants instead of 20. Open
Design Decision
Voucher Card Horizontal RestructureReworkComponent link

A 336 × 111 horizontal voucher card with a text content block on the left (title, price, strikethrough original price, validity) and a 96-wide perforated partner-image frame on the right containing the GCash logo and a rotated "GET VOUCHER" CTA label. Ships 4 state variants — limited, expiring, used, expired — plus two boolean toggles (badge, crossedValue). State drives background (active vs greyed), label colors, and the corner badge (information/heavy "Limited" / negative/heavy "Expiring" / muted/light "Used" / muted/light "Expired"). This is the canonical sibling in the voucher-card family — the only one of three parallel components that models state.

Merge the voucher-card family, port the state axis to the unified component
Voucher Card Horizontal, Vertical Voucher (5119:1635), and Horizontal Voucher (5121:4533) are three parallel records of the same component. This one carries the canonical state coverage the others lack — state: limited | expiring | used | expired. The consolidation target is a single Voucher Card with orientation: vertical | horizontal × state (5 values if a default is added), text Slots for title / price / originalPrice / validity, a logo Slot for partner branding, and a composable badges array replacing the hardcoded state-to-badge mapping. Push the merge from here, not from the other siblings.
Current Figma

4 state variants at 336 × 111. Left content block uses voucher-color tokens for title, amount, and metadata. Right partner-image frame is a raster GCash asset with a perforated edge and a rotated "GET VOUCHER" text label. Badge corner-ribbon color changes per state: information/heavy #2340A9 (Limited), negative/heavy #D61B2C (Expiring), muted/light #C2C5CA (Used / Expired).

DS Health
Reusable
Fail
Title "Buy Load Globe Go90", price "PHP 50.00", original price "PHP 90.00", and validity string are all hardcoded inside the symbol. The partner logo is a raster GCash asset baked into the right frame. Any real voucher (a Globe load voucher, a GrabFood 50% off, a Shopee ₱100 off) cannot render without detaching.
Self-contained
Partial
Colors are token-bound (main/vouchers/color/default/* and main/vouchers/color/expired/*). Typography uses named text styles (Primary/Multi-line Label/Base, Primary/Label/Small, Secondary/Bold/Small Caption). The card does carry its own layout and state treatment — but the logo raster, the "GET VOUCHER" CTA text, and the validity date are all frozen.
Consistent
Warn
Correctly models state as a variant axis — the other two voucher-card siblings (Vertical Voucher, Horizontal Voucher) do not. But the three components ship as three separate records with divergent property shapes. Family-level inconsistency.
Composable
Warn
Nests a Badge instance (composition works). But the partner-image frame is a bespoke raster tree, not an instance-swap slot; and the content block has no text slots. A parent screen cannot compose real voucher data into this card.
Behavior
AspectiOSAndroidFigmaNotes
Statestate: .limited | .expiring | .used | .expiredstate=VoucherState.Limited | Expiring | Used | Expiredstate enum (4)Drives bg, label colors, partner-image treatment, and corner badge. Add a default fifth state for vouchers without a status callout.
Titletitle: Stringtitle: StringHardcoded "Buy Load Globe Go90"No property; must be set via detach.
Price / originalprice: String, originalPrice: String?SamecrossedValue boolean (strings hardcoded)"PHP 50.00" and "PHP 90.00" frozen; boolean only toggles visibility of the strikethrough.
Validityvalidity: String?SameHardcoded "Validity: Dec 25 2022 - Jan 5 2023"No property; must be set via detach.
Status badgebadge: EBBadge?badge: EBBadge?badge boolean + state-derived text"Limited" / "Expiring" / "Used" / "Expired" are derived from state. Consumers cannot set their own badge text.
Partner logologo: Image slotlogo: @Composable () -> UnitRaster GCash assetNo slot — logo is baked into the partner-image frame.
CTAEntire card as Button with PlainButtonStyleCard with onClick + ripple"GET VOUCHER" rotated text (non-interactive)Card is the tap target; the rotated text is decorative, not a Button instance.
Tap statePressed: opacity 0.7 or scale 0.98Ripple via Modifier.clickableNot modelledNo pressed/focused/disabled on the card frame.
Resolved Issues
  • State axis is modelled correctly. Unlike Vertical Voucher and Horizontal Voucher, this component ships a proper state variant driving background, label colors, partner-image tint, and badge style. The 4-value enum (limited / expiring / used / expired) is the canonical shape to port to the unified Voucher Card. C5 · Interaction State Coverage
  • Voucher color tokens are bound. Background (main/vouchers/color/default/bg, main/vouchers/color/expired/bg), title (default/label-title #0A2757, expired/label-title #445C85), amount (label-amount-horizontal #2340A9, expired/label-amount #6780A9), original amount (label-amount-original #90A8D0), and metadata (expired/label-metadata #6780A9) all resolve through the voucher component variable collection. C3 · Token Coverage
  • Badge instance is a DS component. The corner ribbon is a real Badge instance — styled via main/badge/information/heavy/background, main/badge/negative/heavy/background, or main/badge/muted/light/background depending on state. Composition works; only the label string is state-derived rather than user-settable. C7 · Code Connect Linkability
  • Shadow uses the app elevation token.app/shadow/shadow-low (0 0 4 0 #020e220f) is applied to the card frame. C3 · Token Coverage
Open Issues
  • Three parallel components for one concept. Voucher Card Horizontal, Vertical Voucher (5119:1635), and Horizontal Voucher (5121:4533) are three separate records of the same component. This is a family-level consolidation — the unified Voucher Card needs orientation: vertical | horizontal × state: default | limited | expiring | used | expired=10 variants, not three divergent symbols. C4 · Native Mappability
  • All text content is hardcoded. Title "Buy Load Globe Go90", price "PHP 50.00", original price "PHP 90.00", and validity "Validity: Dec 25 2022 - Jan 5 2023" are frozen strings inside every variant. Booleans badge and crossedValue only toggle visibility — they do not accept content. Consumers cannot render a real voucher without detaching. C2 · Variant & Property Naming
  • Badge text is state-derived, not independently settable. The corner ribbon label flips between "Limited" / "Expiring" / "Used" / "Expired" based on the state enum. A consumer who wants to show "New" or "Featured" on a limited voucher cannot — state and badge label are conflated. Split into state (drives visual treatment) + badge (independent Slot/string). C2 · Variant & Property Naming
  • Partner logo is a raster GCash asset with no slot.imgLogoNoText, imgGCashLogosV2RgbIconBwWhiteTransparent, and imgVoucherImageV1 are raster image fills inside the 96×111 partner-image frame. Vouchers for Globe, Smart, GrabFood, or Shopee all render with the GCash logo. No logo Slot exists. C6 · Asset & Icon Quality
  • Duplicated partner-image subtrees per state.voucher (used by limited/expiring) and Voucher Image V1 (used by used/expired) are two complete parallel subtrees inside the same frame — differing only by background fill (bg/color-bg-primary vs bg/color-bg-overlay-weak). Should be a single subtree with state-driven tokens. C1 · Layer Structure & Naming
  • "GET VOUCHER" rotated text is not a Button. The CTA label is a rotated <p> inside the partner-image frame — not a Button instance, no pressed/focused/disabled state, no onTap handler semantics. If tapping the partner half is supposed to redeem the voucher, that needs to be an actual Button or the whole card needs to be the tap target. C5 · Interaction State Coverage
  • Perforated ticket edge is a raster mask.imgPerforate is a raster image used as a mask to produce the perforated dashed edge between the content block and partner frame. Should be a vector path or an SVG mask; at 1× / 2× / 3× the raster will alias. C6 · Asset & Icon Quality
  • No default (neutral) state. Every variant renders a corner badge. A voucher that is simply available (neither limited nor expiring) has no option to render without a badge beyond setting badge=false, which drops the callout but keeps the limited-state visual treatment. Add a default state for active-but-unflagged vouchers. C5 · Interaction State Coverage
  • Two-boolean + 4-enum surface cannot map 1:1 to native. A proper EBVoucherCard(orientation:, state:, title:, price:, originalPrice:, validity:, logo:, badge:, onTap:) shape has no 1:1 correspondence in the current component — title/price/originalPrice/validity/logo are all hardcoded. Code Connect linkability requires the family consolidation and property-ification first. C7 · Code Connect Linkability
Design Recommendations
  • Merge the three voucher cards into a single Voucher Card component. Vertical Voucher + Horizontal Voucher + Voucher Card Horizontal collapse into one component with orientation: vertical | horizontal × state: default | limited | expiring | used | expired=10 variants. Port this component's state axis to the unified schema; port Vertical Voucher's content-block structure; drop Horizontal Voucher entirely. Family
  • Promote every text string to a property. Add title: String, price: String, originalPrice: String?, validity: String?. Retire the crossedValue boolean — visibility falls out of whether originalPrice is set. Keep the text-style bindings intact. Property
  • Split state (visual treatment) from badge (label). Keep state as the 5-value enum that drives bg / label colors / partner-image treatment. Expose badge: EBBadge? as an independent Slot so consumers can pick any badge style and text ("New", "Featured", "Limited", custom). The current state-to-badge-text mapping becomes the default badge when none is supplied. Property
  • Adopt a Figma Slot for the partner logo. Replace the raster GCash asset with a 64×64 logo Slot inside the partner-image frame. Consumers instance-swap partner brand marks (Globe, Smart, GrabFood, Shopee, etc.) without detaching. Keep the perforated ticket shape and overlay treatment in the frame itself. Slot
  • Collapse the two partner-image subtrees into one.voucher (limited/expiring) and Voucher Image V1 (used/expired) are duplicate layer trees differing only by background token. A single subtree gated by state-driven fills (main/vouchers/color/{state}/partner-bg) removes the duplication. Composition
  • Add a default state. Current states are all "flagged" — active-but-unflagged vouchers have no clean render. Add state: default with the active (non-greyed) treatment and no corner badge by default. State
  • Replace the perforated raster mask with a vector path. The perforated ticket edge should be an SVG path or a vector mask — not a raster image. Same treatment as recommended for Voucher Asset. Asset
  • Make the card the tap target; remove the rotated "GET VOUCHER" text as a faux-button. The entire card is the semantic action ("redeem / open voucher details"). Document the handoff as onTap; keep "GET VOUCHER" only as a decorative label if product still wants it visible, or drop it entirely. Docs
  • Add pressed / focused / disabled to the card frame. Vouchers are always tappable — the unified Voucher Card needs a pressed state variant (e.g. opacity 0.8 or scale 0.98), a focused state for keyboard navigation, and a disabled treatment for unavailable vouchers distinct from expired. State
  • Rename the duplicated Badge layers per state. All four badge layers are named Badge with no variant-qualifying name. After the family merge, there should be a single badge-slot layer; until then, name them badge-limited / badge-expiring / badge-used / badge-expired for clarity. Rename
Native equivalent Planned API

Native models the whole voucher family as a single EBVoucherCard with orientation + state axes, text properties, a logo image slot, and an optional badge overriding the state default. This horizontal card is orientation: .horizontal; its 4 states port 1:1 to the native VoucherState enum.

// Proposed API — unified Voucher Card (horizontal orientation)
EBVoucherCard(
    orientation: .horizontal,
    state: .limited,
    title: "Buy Load Globe Go90",
    price: "PHP 50.00",
    originalPrice: "PHP 90.00",
    validity: "Dec 25 2022 - Jan 5 2023"
) {
    Image("partner-globe")   // logo slot
        .resizable()
        .scaledToFit()
}
.onTapGesture { // navigate to voucher details }

// Expiring, with strikethrough price
EBVoucherCard(orientation: .horizontal, state: .expiring, title: "GrabFood ₱100 OFF", price: "PHP 50.00", originalPrice: "PHP 150.00", validity: "Ends today") {
    Image("partner-grabfood").resizable().scaledToFit()
}

// Used / expired — greyed treatment, not tappable
EBVoucherCard(orientation: .horizontal, state: .expired, title: "Shopee ₱50 OFF", price: "PHP 20.00", validity: "Expired Apr 1, 2026") {
    Image("partner-shopee").resizable().scaledToFit()
}

// Override the state-default badge
EBVoucherCard(
    orientation: .horizontal,
    state: .default,
    title: "Tim Hortons Breakfast",
    price: "PHP 120.00",
    badge: EBBadge("New", style: .brandHeavy)
) {
    Image("partner-timhortons").resizable().scaledToFit()
}
// Proposed API — unified Voucher Card (horizontal orientation)
EBVoucherCard(
    orientation = EBVoucherOrientation.Horizontal,
    state = EBVoucherState.Limited,
    title = "Buy Load Globe Go90",
    price = "PHP 50.00",
    originalPrice = "PHP 90.00",
    validity = "Dec 25 2022 - Jan 5 2023",
    onClick = { /* navigate to voucher details */ },
    logo = {
        Image(
            painter = painterResource(R.drawable.partner_globe),
            contentDescription = null,
            contentScale = ContentScale.Fit
        )
    }
)

// Expiring, with strikethrough price
EBVoucherCard(
    orientation = EBVoucherOrientation.Horizontal,
    state = EBVoucherState.Expiring,
    title = "GrabFood ₱100 OFF",
    price = "PHP 50.00",
    originalPrice = "PHP 150.00",
    validity = "Ends today",
    onClick = { },
    logo = { Image(painterResource(R.drawable.partner_grabfood), null) }
)

// Override the state-default badge
EBVoucherCard(
    orientation = EBVoucherOrientation.Horizontal,
    state = EBVoucherState.Default,
    title = "Tim Hortons Breakfast",
    price = "PHP 120.00",
    badge = EBBadge("New", style = EBBadgeStyle.BrandHeavy),
    onClick = { },
    logo = { Image(painterResource(R.drawable.partner_timhortons), null) }
)
Property Mapping (Proposed)

Current shape is 4 state variants + 2 booleans with hardcoded content. The table below shows the target shape after the family consolidation — each row captures what the proposed EBVoucherCard replaces.

Current FigmaProposed FigmaSwiftUIComposeNotes
orientationorientation: EBVoucherOrientationorientation=EBVoucherOrientationvertical | horizontal — collapses 3 components into 1
state (4 values)state (5 values)state: EBVoucherStatestate=EBVoucherStateAdd default for active-but-unflagged vouchers
hardcoded "Buy Load Globe Go90"title (string)title: Stringtitle: StringProperty, not frozen
hardcoded "PHP 50.00"price (string)price: Stringprice: StringProperty, not frozen
crossedValue (boolean, "PHP 90.00" frozen)originalPrice (string)originalPrice: String?originalPrice: String?Visibility=whether string is non-nil
hardcoded "Validity: …"validity (string)validity: String?validity: String?Consumer supplies the date range
raster GCash logologo Slottrailing closurelogo: @Composable () -> UnitPartner brand mark, instance-swappable
badge (boolean, state-derived label)badge Slot (optional)badge: EBBadge?badge: EBBadge?State drives the default; consumer can override
"GET VOUCHER" rotated text— (remove or decorative)onTap: () -> VoidonClick: () -> UnitCard is the tap target
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingReworkTwo parallel partner-image subtrees (voucher vs Voucher Image V1) for active vs greyed states. Four badge layers all named Badge. Content block uses generic container / price names but text layers are unnamed.
C2Variant & Property NamingReworkState enum drives bg + label colors + badge style + badge text all at once. Title, price, original price, validity are frozen strings. crossedValue boolean should be a nullable originalPrice string.
C3Token CoverageReadyAll colors bound to main/vouchers/color/{default|expired}/* and main/badge/{information|negative|muted}/{heavy|light}/*. Typography uses named text styles. Shadow uses app/shadow/shadow-low.
C4Native MappabilityReworkParallel to 2 other voucher components with divergent schemas. Native is one EBVoucherCard, not three. Strings/logo need property-ification before any 1:1 mapping.
C5Interaction State CoverageRework4 state variants are good but no default, no pressed/focused/disabled on the card frame, and the "GET VOUCHER" CTA is a non-interactive rotated text label rather than a Button.
C6Asset & Icon QualityReworkPartner logo is raster (imgLogoNoText, GCash PNG). Perforated ticket edge uses a raster mask (imgPerforate). Should be vector / SVG path.
C7Code Connect LinkabilityReworkCannot map with hardcoded strings and no logo slot. Linkability requires the family consolidation + property-ification first.
Variants Inventory (4 total)

Single axis: state. All 4 variants render at 336 × 111. Booleans badge (default true) and crossedValue (default true) apply uniformly across states.

Node IDVariantDimensionsBadge stylePartner-image treatment
5119:1787state=limited336 × 111information/heavy #2340A9, label "Limited"Full-color bg (bg/color-bg-primary #005CE5) + white GCash logo
5119:1807state=expiring336 × 111negative/heavy #D61B2C, label "Expiring"Full-color bg (bg/color-bg-primary #005CE5) + white GCash logo
5119:1827state=used336 × 111muted/light #C2C5CA, label "Used"Greyed overlay (bg/color-bg-overlay-weak rgba(2,14,34,0.24)), mix-blend-multiply
5119:1847state=expired336 × 111muted/light #C2C5CA, label "Expired"Greyed overlay (bg/color-bg-overlay-weak rgba(2,14,34,0.24)), mix-blend-multiply

Notable nested nodes (duplicated per variant):

Node IDLayerKindNotes
5119:1806Badge "Limited"Badge instanceinformation/heavy style
5119:1826Badge "Expiring"Badge instancenegative/heavy style
5119:1846Badge "Used"Badge instancemuted/light style
5119:1866Badge "Expired"Badge instancemuted/light style
5119:1799 / 5119:1819voucherPartner-image subtree (active)Used for limited / expiring — full-color treatment
5119:1839 / 5119:1859Voucher Image V1Partner-image subtree (greyed)Used for used / expired — overlay-weak treatment
1.0.0 — April 2026 Initial
Initial Assessment · node 5119:1786
Assessed with Restructure verdict. 4-state variant (limited / expiring / used / expired) is the canonical state axis for the voucher family — the only one of three parallel voucher cards that models state. Text content, partner logo, and perforated edge are hardcoded / raster. Open
Design Decision
Proposed family-level merge. Collapse this, Vertical Voucher (5119:1635), and Horizontal Voucher (5121:4533) into a single Voucher Card with orientation + state axes. Port this component's state coverage to the unified schema. Target: 2 × 5=10 variants instead of 3 divergent components. Open
Design Decision
Voucher Details Product LayerN/AComponent link

A 336×704 single-instance symbol that renders the entire voucher-details screen: merchant header (Logo 40px + brand name + "All branches" + a LimitedBadge), voucher content (title + current amount + slashed original amount + validity), a dashed strip divider, a multi-line voucher description, an optional Terms & Conditions plain-text section with a "See full promo mechanics" link, and an optional embedded Terms & Conditions Accordion with 4 check-marked list rows. The component has no variants — only four booleans toggle optional subtrees: accordion, badge, slashedAmount, tCWithTextLink. Every color resolves to main/vouchers/color/default/*, main/badge/*, main/accordion/*, and main/list-item/* tokens from the composed primitives.

Product Layer — ship as a screen recipe, not a component
A 336×704 single-instance symbol with no variants is not a DS primitive — it is a screen. DS primitives are reusable across many contexts with meaningful variant axes; Voucher Details exists exactly once and only toggles which optional child subtree renders. The DS should ship the primitives it composes from (EBLogo, EBBadge, EBAccordion, EBListItem) and publish this screen as a recipe in product documentation. The shared "amount + slashed original" row and the dashed strip divider are the only pieces worth extracting — and those belong in a product-layer VoucherAmountRow + TicketDivider, still outside the core DS.
Current Figma

One 336×704 symbol. Four boolean toggles show or hide optional subtrees — badge (merchant "Limited" pill), slashedAmount (original price with strikethrough), tCWithTextLink (plain-text T&C section with "See full promo mechanics" link), and accordion (embedded Terms & Conditions Accordion instance, expanded by default with 4 list rows).

Primitives Composed
SubtreeDS primitiveNodeNotes
Merchant avatarLogo 40px27:190607Instance-swapped for brand identity
"Limited" pillBadge (Information / Heavy)21:111526Hardcoded "Limited" string
Amount rowCustom layerCurrent amount + slashed original — not a DS primitive
Strip dividerRaster image fillDashed horizontal line rendered as an image — should be a stroke pattern
T&C plain textCustom layerDuplicates the Accordion's expanded body
T&C accordionTerms & Conditions Accordion (itself flagged Remove)5119:5447Wraps canonical Accordion (16870:9288) with 4 List Item rows
DS Health
Reusable
Fail
Not reusable — the component is the voucher-details screen. It has no abstraction; every text string ("Brand", "Voucher Title", "PHP 200.00", "Limited", "All branches", the 4 T&C rows) is baked in. Any consumer must detach to change anything.
Self-contained
Fail
Every meaningful piece — Logo, Badge, Accordion, List Item — is owned by another DS component. What's unique here (the amount row and strip divider) is custom layers, not tokens or logic. The component carries no DS-level behavior of its own.
Consistent
Fail
Schema is 4 booleans that flip child visibility — not states, not variants, not semantic props. Worse, two of them (tCWithTextLink, accordion) render overlapping content (plain-text T&C vs accordion T&C) in parallel. No real voucher turns both on.
Composable
Warn
The composition of primitives is clean — Logo 40px, Badge, Accordion, and List Item are all instance-swapped correctly and inherit their own tokens. That's exactly why this should be product code, not a DS atom.
Behavior
AspectiOSAndroidFigmaNotes
ContainerScrollView with VStackColumn inside verticalScrollRoot frameScreen-level scroll, not a component
Merchant logoEBLogo(size: .lg)EBLogo(size=EBLogoSize.Lg)Instance of Logo 40pxCanonical Logo primitive
"Limited" pillEBBadge("Limited", style: .informationHeavy)EBBadge("Limited", style=InformationHeavy)Instance of BadgeString is hardcoded today
Amount rowCustom product viewCustom product composableLocal layerCurrent amount + slashed original — product-layer concern
Strip dividerCanvas stroke patternCanvas dash patternRaster image fillShould be a stroke, not a raster asset
T&C accordionEBAccordion(title: "Terms & Conditions")EBAccordion(title="Terms & Conditions")InstanceCanonical Accordion with ForEach of EBListItem
"See full promo mechanics"Button(role: .link)TextButtonInline colored spanNeeds a real link primitive or product-owned LinkText
Open Issues
  • Screen masquerading as a DS component. A 336×704 single-instance symbol with four optional-content booleans is a screen, not a primitive. DS primitives are reusable across many contexts with meaningful variant axes; this exists exactly once and only toggles which optional child renders. C1 · Layer Structure & Naming
  • Booleans are child-visibility switches, not states.accordion, badge, slashedAmount, tCWithTextLink each mean "render this optional subtree." They are not semantic props (e.g. hasLimitedOffer, hasOriginalPrice) and not behavioral states. 2^4=16 possible combinations of which only ~4 are legitimate in real product screens. C2 · Variant & Property Naming
  • Overlapping T&C display paths.tCWithTextLink renders a plain-text Terms & Conditions block with a "See full promo mechanics" link; accordion renders a full Terms & Conditions Accordion with 4 list rows. Both describe the same information and can be toggled on simultaneously. Real screens pick one. C2 · Variant & Property Naming
  • Native handoff is a View/Screen, not a Component. SwiftUI would model this as VoucherDetailsView — a scrollable parent that composes EBLogo, EBBadge, a product-owned amount row, EBAccordion, and EBListItem. There is no value in mapping the screen as a single-symbol Code Connect entry. C4 · Native Mappability
  • Missing interaction states. Voucher details is interactive on mobile — accordion expands/collapses, the "See full promo mechanics" link navigates, the voucher itself may have a primary "Use Voucher" CTA on the real screen. The symbol ships only one static frame and happens to use expanded=yes for the embedded accordion. C5 · Interaction State Coverage
  • Strip divider is a raster image fill. The dashed horizontal line between voucher content and description is rendered as an imported image (imgStrip), not a stroke-dasharray or a vector pattern. It will not scale with density, re-color, or adapt to dark mode. C6 · Asset & Icon Quality
  • Not linkable via Code Connect. A 4-boolean screen symbol cannot map 1:1 to a meaningful native API. Linkability belongs at the primitive level (Logo, Badge, Accordion, ListItem) — each of which already has its own Code Connect track. C7 · Code Connect Linkability
Design Recommendations
  • Retire from the sticker sheet; publish as a product-screen recipe. Move Voucher Details out of the DS file and into a product documentation page titled "Voucher Details Screen". The recipe shows how to compose EBLogo, EBBadge, the amount row, the strip divider, and EBAccordion with a ForEach of EBListItem. The DS owns primitives; product owns screens. Docs
  • Extract the shared amount row into a product-layer component. Current amount + slashed original amount is a genuinely reusable product pattern (appears in every voucher variant and on other commerce screens). Lift it into a VoucherAmountRow(current: "PHP 200.00", original: "PHP 180.00") living in the product library — not the core DS. Composition
  • Replace the raster strip divider with a stroke pattern. The dashed horizontal rule between voucher content and description is rendered as a raster image. Replace it with a stroke-based divider (stroke-dasharray in SVG, Canvas on native) so it scales, retints, and adapts to dark mode without a new asset. Asset
  • Collapse the dual Terms & Conditions paths into one. The symbol ships both a plain-text T&C section and a full accordion T&C section as independent booleans. Pick one pattern for the product — recommendation: keep the accordion (collapsible saves vertical space on small screens) and drop tCWithTextLink entirely, moving the "See full promo mechanics" link into the accordion body or next to the CTA. Composition
  • Promote user-facing strings to properties (if this symbol survives as a product component). If the design team keeps Voucher Details as a product-layer component, hoist "Brand", "All branches", "Voucher Title", "PHP 200.00", "PHP 180.00", validity range, description, and the badge label into named text props so product teams do not detach to change copy. Property
  • Native mapping lives at the primitive level. Document that Code Connect mappings for Voucher Details are not added at the screen level. Each composed primitive — EBLogo, EBBadge, EBAccordion, EBListItem — carries its own mapping. The screen itself ships as a SwiftUI View / Compose screen-level composable inside product code. Docs
Native equivalent Planned API

No dedicated EBVoucherDetails component on either platform. The screen is composed in product code from DS primitives plus a product-layer amount row and ticket divider. Example shows the canonical recipe.

// Product-layer screen — lives in app code, not the DS package
struct VoucherDetailsView: View {
    let voucher: Voucher

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {

                // Merchant header
                HStack(spacing: 8) {
                    EBLogo(image: voucher.merchantLogo, size: .lg)
                    VStack(alignment: .leading) {
                        Text(voucher.merchant).ebTextStyle(.primaryLabelBase)
                        Text(voucher.locationCopy).ebTextStyle(.secondaryBoldCaption)
                    }
                    Spacer()
                    if let label = voucher.limitedLabel {
                        EBBadge(label, style: .informationHeavy)
                    }
                }.padding()

                // Amount block (product-layer component)
                VoucherAmountRow(
                    title: voucher.title,
                    current: voucher.currentAmount,
                    original: voucher.originalAmount,
                    validity: voucher.validity
                )

                TicketDivider()   // product-layer, stroke-based

                Text(voucher.description)
                    .ebTextStyle(.secondaryBoldBase)
                    .padding()

                EBAccordion(title: "Terms & Conditions") {
                    VStack(alignment: .leading, spacing: 6) {
                        ForEach(voucher.rules) { rule in
                            EBListItem(rule.text, indicator: .check)
                        }
                    }
                }
            }
        }
    }
}
// Product-layer screen — lives in app code, not the DS package
@Composable
fun VoucherDetailsScreen(voucher: Voucher) {
    Column(modifier = Modifier.verticalScroll(rememberScrollState())) {

        // Merchant header
        Row(
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(12.dp)
        ) {
            EBLogo(image = voucher.merchantLogo, size = EBLogoSize.Lg)
            Column(modifier = Modifier.weight(1f)) {
                Text(voucher.merchant, style = EBTextStyle.PrimaryLabelBase)
                Text(voucher.locationCopy, style = EBTextStyle.SecondaryBoldCaption)
            }
            voucher.limitedLabel?.let { EBBadge(it, style = EBBadgeStyle.InformationHeavy) }
        }

        // Amount block (product-layer composable)
        VoucherAmountRow(
            title = voucher.title,
            current = voucher.currentAmount,
            original = voucher.originalAmount,
            validity = voucher.validity
        )

        TicketDivider()   // product-layer, stroke-based

        Text(
            voucher.description,
            style = EBTextStyle.SecondaryBoldBase,
            modifier = Modifier.padding(12.dp)
        )

        EBAccordion(title = "Terms & Conditions") {
            Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
                voucher.rules.forEach { rule ->
                    EBListItem(content = rule.text, indicator = EBListIndicator.Check)
                }
            }
        }
    }
}
Property Mapping

No 1:1 Code Connect mapping — the screen is not a primitive. Each composed primitive maps via its own component. The current Figma booleans correspond to product-level state on the Voucher model, not to component props.

Figma booleanProduct modelNative effectNotes
badgevoucher.limitedLabel: String?Conditional EBBadge renderDrive from data, not a boolean flag
slashedAmountvoucher.originalAmount: Money?Conditional strikethrough textDrive from data — null means no discount
tCWithTextLinkDropOverlaps the accordion path; eliminate
accordionvoucher.rules: [Rule]EBAccordion + ForEach EBListItemAlways shown when rules exist
Criteria Scorecard
IDCriterionStatusNotes
C1Layer Structure & NamingN/AScreen, not a component — layer-naming discipline applies per primitive, not here.
C2Variant & Property NamingN/AFour boolean visibility switches, no variant axis. Not a schema worth normalising.
C3Token CoverageReadyEvery color resolves to main/vouchers/*, main/badge/*, main/accordion/*, or main/list-item/* tokens from composed primitives.
C4Native MappabilityN/AMaps to a product-layer View/Screen, not a component. No 1:1 DS-to-native handoff.
C5Interaction State CoverageN/AInteraction lives on primitives (Accordion expand/collapse, link tap). Screen-level state is product concern.
C6Asset & Icon QualityReworkStrip divider is a raster image fill — should be a stroke pattern.
C7Code Connect LinkabilityN/ANot linkable as a unit. Primitives carry their own mappings.
Variants Inventory (1 total)

Single symbol, no variant axes. Four boolean toggles (accordion, badge, slashedAmount, tCWithTextLink) drive optional child subtrees but do not generate variants in the component set.

Node IDDimensionsDefault booleans
5119:5368336 × 704accordion=true, badge=true, slashedAmount=true, tCWithTextLink=true
1.0.0 — April 2026 Initial
Initial Assessment · node 5119:5368
Assessed as Product Layer. 336×704 single-symbol screen composition with four optional-content booleans — not a DS primitive. Composes canonical Logo 40px, Badge, Accordion, and List Item. Open
Design Decision
Recommendation: retire from the DS file. Ship as a product-screen recipe. Extract the shared amount row + strip divider into product-layer VoucherAmountRow and TicketDivider. Replace the raster strip with a stroke pattern. Drop the tCWithTextLink path to eliminate overlap with the embedded accordion. Open
Design Decision