KeepNeeds Refinement
Button Component link

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
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
State iOS Android Figma Property Notes
Default Yes Yes State=Default All four appearance modes fully defined.
Pressed Yes Yes State=Pressed Darker fill/border using pressed tokens.
Disabled Yes Yes State=Disabled Muted color tokens applied across all appearances.
Destructive Yes Yes Appearance mode: Destructive Red tokens via variable mode. Applies to all 3 styles (Filled/Outline/Text).
Focused (a11y) N/A N/A Mobile-only component. Focus rings rendered natively by iOS (UIKit/SwiftUI) and Android (Material a11y). No Figma state required.
Loading Yes Yes Native modifier Handled as an interaction modifier in native code — .ebLoading(true) (SwiftUI) / isLoading = true (Compose). Removed as a Figma state in v4.0.
Icon Only (a11y) Yes Yes Icon Placement=Icon Only Square target matches size height. Requires accessibilityLabel / contentDescription since no visible text.
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
DES DEV

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

Properties
State
Size
Icon Placement
Mode
Appearance
Properties
Style Filled
Appearance Default
State Default
Size Large
Colors
Default bg #005CE5
Default label #FFFFFF
Pressed bg #2340A9
Disabled bg #9BC5FD
Layout
Height 50px
Padding H 20px
Padding V 16px
Radius 99px
Typography
Font Proxima Soft Bold
Text Style Primary/Label/Large
Size 18px
Tracking 0.25px
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.

Role Token EnabledPressedDisabled
Default bg #005CE5 #2340A9 #9BC5FD
label #FFFFFF #FFFFFF #FFFFFF
Destructive bg #D81E1E #B01818 #F5A3A3
label #FFFFFF #FFFFFF #FFFFFF
White bg #FFFFFF #EEF2F9 #F5F7FA
label #005CE5 #005CE5 #005CE5
Subtle bg #E5F1FF #D2E5FF #EEF5FF
label #005CE5 #005CE5 #005CE5
Outline
DES DEV

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

Properties
State
Size
Icon Placement
Mode
Appearance
Properties
Style Outline
Appearance Default
State Default
Size Large
Colors
Default border #005CE5
Default label #005CE5
Pressed border #2340A9
Disabled border #9BC5FD
Layout
Height 50px
Padding H 20px
Padding V 16px
Border 1.5px solid
Radius 99px
Typography
Font Proxima Soft Bold
Text Style Primary/Label/Large
Size 18px
Tracking 0.25px
Colors by Appearance Mode

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

Role Token EnabledPressedDisabled
Default border #005CE5 #2340A9 #9BC5FD
label #005CE5 #2340A9 #9BC5FD
Destructive border #D81E1E #B01818 #F5A3A3
label #D81E1E #B01818 #F5A3A3
White border #005CE5 #2340A9 #9BC5FD
label #005CE5 #2340A9 #9BC5FD
Subtle border #005CE5 #2340A9 #9BC5FD
label #005CE5 #2340A9 #9BC5FD
Text
DES DEV

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

Properties
State
Size
Icon Placement
Mode
Appearance
Properties
Style Text
Appearance Default
State Default
Size Large
Colors
Default label #005CE5
Pressed label #2340A9
Disabled label #9BC5FD
Layout
Height 50px
Padding H 20px
Padding V 16px
Radius 99px
Typography
Font Proxima Soft Bold
Text Style Primary/Label/Large
Size 18px
Tracking 0.25px
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.

Role Token EnabledPressedDisabled
Default label #005CE5 #2340A9 #9BC5FD
Destructive label #D81E1E #B01818 #F5A3A3
White label #005CE5 #2340A9 #9BC5FD
Subtle label #005CE5 #2340A9 #9BC5FD
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…XSmall controlSize: .large / .regular / .small / .compact / .mini size = EBButtonSize.Large / Medium / Small / Compact / XSmall
State=Disabled .disabled(true) enabled = false
(Loading — runtime) .ebLoading(true) isLoading = true
Icon Placement=None (default — text only) (default — text only)
Icon Placement=Leading Label("…", systemImage: "…") leadingIcon = { Icon(…) }
Icon Placement=Trailing Label + trailing Image trailingIcon = { Icon(…) }
Icon Placement=Icon Only EBButton(icon: Image(…), accessibilityLabel: "…") EBButton(contentDescription = "…") { Icon(…) }
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 target 44 × 44pt 48 × 48dp
Focus ring Handled by UIKit/SwiftUI Handled by Material ripple
Icon-only buttons .accessibilityLabel("Send") contentDescription = "Send"
Destructive role role: .destructive — announced by VoiceOver Use semantics { role = Role.Button }
Loading state .accessibilityLabel("Loading") + disable tap semantics { stateDescription = "Loading" } + disable click
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
ID Criterion Status Notes
C1 Layer Structure & Naming Ready All layers use semantic names. container, #label, leadingIcon, trailingIcon consistent across all 60 variants.
C2 Variant & Property Naming Ready v3 clean orthogonal dimensions: Style (Filled/Outline/Text), Size (Large/Medium/Small/Compact/XSmall), State (Default/Pressed/Disabled). Appearance via 4 variable modes. leadingIcon/trailingIcon are SLOT nodes with Boolean component properties.
C3 Token Coverage Ready All color values connected to semantic tokens. Layout/sizing driven by button/size variable collection (height, padding-h, padding-v, font-size).
C4 Native Mappability Ready Maps to Button, OutlinedButton, TextButton. Destructive maps to role: .destructive / contentColor = errorColor.
C5 Interaction State Coverage Ready Default, Pressed, Disabled, Loading covered across all 60 variants. Focus ring is N/A — mobile OS handles natively. Loading uses dot indicators with disabled appearance colors.
C6 Asset & Icon Quality Ready Icon slots are Figma SLOT nodes accepting vector icon instances. Boolean properties control visibility.
C7 Code Connect Linkability Needs Refinement No CLI mappings registered yet. Property structure is clean and ready for mapping.
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
4.1.0 — April 2026Minor
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 2026Major
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 2026Minor
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 2026Minor
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 2026Major
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-assessmentMinor
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 2026Minor
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 2026Minor
Changes Applied via Figma MCP · node 17104:184842
Layer renamed: .base/button/smallcontainer — Resolves C1. Fixed
C1 Resolved
1.0.0 — March 2026Major
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