An illustrated centered popup used for promos, success moments, and notable announcements.
Default / 2 CTA / Version 2 (C2). Hero image is a raster placeholder with "Replace me" overlay instead of a swappable image slot (C6). No destructive/error/loading state coverage (C5). Code Connect mappings not registered (C7).Contexts are illustrative. Final screens will reference actual GCash patterns. Visual Popup overlays the app surface to confirm critical actions or onboard users to a new feature.
main/modal-popup/color/bg), Shadow/Depth 0, radius/radius-2, and 24px padding. Composes Button instances rather than redefining button styles.Default (generic), 2 CTA (count), Version 2 (version). Native enums need a single semantic axis — e.g. single-cta / dual-cta / dismissible. C2| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Single CTA | Yes | Yes | Type=Default | Hero (180px) + title + description + primary CTA. Use for info or single-action confirm. |
| Dual CTA | Yes | Yes | Type=2 CTA | Adds a secondary outline + tertiary text button below primary. Use for cancel/confirm pairs. |
| Dismissible (V2) | Yes | Yes | Type=Version 2 | Preamble label + title with close icon + content-first layout. Use for onboarding/tutorial popups. |
| Destructive / Error / Loading | N/A | N/A | — | No variants for destructive confirms or async/loading states. C5 |
- Variant naming mixes paradigms.
Default(generic),2 CTA(count), andVersion 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
- Collapse
Typeto one semantic enum. Values:single-cta,dual-cta,dismissible. Eliminates the version/count/default mix and maps cleanly to anEBVisualPopupKindenum. Property - Replace the raster
Modals Assetwith a swappable Image slot. A component placeholder that product teams can instance-swap with their illustration — matches the pattern Avatar uses for itsimagetype. Slot - Add a
destructivemode. Whether as a boolean or akind=destructivevariant — lets destructive confirms (Cancel / Logout / Delete) use red CTAs without bespoke overrides. State - Add a
loadingstate 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
Hero image (320 × 180, 16:9) + title + 2-line description + single primary CTA. Use for informational modals or single-action confirms ("Okay").
Modal popup ships display-only color tokens — no pressed/disabled states (the popup itself doesn't have interaction states; CTAs handle that via Button tokens).
| Role | Token | Value |
|---|---|---|
| Modal background | main/modal-popup/color/bg | #FFFFFF |
| Title label | main/modal-popup/color/label | #0A2757 |
| Description label | main/modal-popup/color/label-primary | #6780A9 |
| Preamble (V2) | main/modal-popup/color/label-preamble | #90A8D0 |
| Close icon (V2) | main/modal-popup/color/icon-close | #6780A9 |
| V2 inner container | bg/color-bg | #F6F9FD |
Same hero + title + description as Default, then a secondary outline button on top of a tertiary text button. Use for confirm/cancel pairs.
| Role | Token | Value |
|---|---|---|
| Default / 2 CTA width | — | 320px |
| Version 2 width | — | 312px |
| Hero image (Default / 2 CTA) | — | 320 × 180 (16:9) |
| Hero image (V2) | — | 280 × 180, 10px radius |
| Body padding | space/space-24 | 24px |
| CTA group padding (vertical) | space/space-24 | 24px |
| 2 CTA gap between buttons | space/space-8 | 8px |
| V2 inner container padding | space/space-16 | 16px h, 16t / 24b |
| Corner radius | radius/radius-2 | 6px |
| Shadow | Shadow/Depth 0 | 0 0 4px #E8EEF2C9 |
| Close icon (V2) | — | 24 × 24 |
Onboarding/tutorial layout. The popup itself is a single light-gray (<code>bg/color-bg</code>) 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.)
| Role | Token | Spec |
|---|---|---|
| Title | Primary/Headlines/Section | Proxima Soft Bold · 22 / 26 |
| Description | Secondary/Default/Base | BarkAda Medium · 14 / 20 |
| Preamble (V2) | Primary/Label/Tiny | Proxima Soft Bold · 10 / 10 · +0.25 |
| CTA label | Primary/Label/Large | Proxima Soft Bold · 18 / 18 · +0.25 |
iOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios"
Android — Gradle (Kotlin DSL)
dependencies { implementation("com.eastblue.ds:visual-popup:1.0.0") }
| Figma Property | SwiftUI | Compose |
|---|---|---|
| Type=Default | .ebKind(.singleCTA) | kind = EBVisualPopupKind.SingleCTA |
| Type=2 CTA | .ebKind(.dualCTA) | kind = EBVisualPopupKind.DualCTA |
| Type=Version 2 | .ebKind(.dismissible) | kind = EBVisualPopupKind.Dismissible |
| Hero image (raster) | heroImage: Image | heroImage: Painter |
| CTA buttons | primary / secondary / tertiary: EBButton | primary / secondary / tertiary: @Composable |
// Default — single CTA EBVisualPopup( title: "Cash In Successful", description: "₱500.00 added to your wallet.", heroImage: Image("cash-in-success"), primary: EBButton("Okay") { /* dismiss */ } ) .ebKind(.singleCTA) // 2 CTA — confirm/cancel EBVisualPopup( title: "Cancel transaction?", description: "This cannot be undone.", heroImage: Image("warning-illustration"), primary: EBOutlinedButton("Confirm") { /* confirm */ }, secondary: EBTextButton("Go Back") { /* dismiss */ } ) .ebKind(.dualCTA) // Version 2 — onboarding with close icon EBVisualPopup( preamble: "NEW", title: "Save your receipts", description: "Tap any transaction to save its receipt.", heroImage: Image("receipt-tutorial"), primary: EBButton("Got it") { /* dismiss */ }, onClose: { /* dismiss */ } ) .ebKind(.dismissible)
// Default — single CTA EBVisualPopup( kind = EBVisualPopupKind.SingleCTA, title = "Cash In Successful", description = "₱500.00 added to your wallet.", heroImage = painterResource(R.drawable.cash_in_success), primary = { EBButton("Okay", onClick = { /* dismiss */ }) } ) // 2 CTA — confirm/cancel EBVisualPopup( kind = EBVisualPopupKind.DualCTA, title = "Cancel transaction?", description = "This cannot be undone.", heroImage = painterResource(R.drawable.warning), primary = { EBOutlinedButton("Confirm", onClick = { /* confirm */ }) }, secondary = { EBTextButton("Go Back", onClick = { /* dismiss */ }) } ) // Version 2 — onboarding with close icon EBVisualPopup( kind = EBVisualPopupKind.Dismissible, preamble = "NEW", title = "Save your receipts", description = "Tap any transaction to save its receipt.", heroImage = painterResource(R.drawable.receipt_tutorial), primary = { EBButton("Got it", onClick = { /* dismiss */ }) }, onClose = { /* dismiss */ } )
| Requirement | iOS | Android |
|---|---|---|
| Modal trait / role | Present via .sheet or .alert — VoiceOver announces as modal | Dialog announces as modal; TalkBack focus trapped inside |
| Focus trap | Automatic with .sheet | Automatic with Dialog — set dismissOnClickOutside = false for confirm popups |
| Close button label (V2) | .accessibilityLabel("Close") | contentDescription = "Close" |
| Hero image | If decorative: .accessibilityHidden(true). If informative: provide a label. | Same — contentDescription = null for decorative, otherwise describe |
| Tap targets | CTAs use Button which meets HIG 44pt | CTAs meet Material 48dp |
| Destructive role | Currently undefined — needs role: .destructive when state lands | Currently undefined — needs Button destructive colors when state lands |
Do
Use Visual Popup for critical confirms, success states with celebration, and onboarding moments — places where the hero image adds emotional weight.
Don't
Use for inline form errors or transient notifications — those belong in Toast, Banner, or inline error patterns, not a blocking modal.
Do
Use the Default variant when the popup has one obvious next step (Okay, Got it, Continue).
Don't
Use 2 CTA when one button is clearly more important than the other — that's still a Default with the secondary action elsewhere.
Do
Use Version 2 (with close icon) only for onboarding/tutorial popups where the user can dismiss without taking action.
Don't
Add a close icon to confirm/destructive popups — forces the user to consciously choose the CTA.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic names: Modals Asset, body, header, CTA - Base Button Group, Close. |
| C2 | Variant & Property Naming | Requires Rework | Property values mix paradigms: Default / 2 CTA / Version 2. Should be one semantic axis. |
| C3 | Token Coverage | Ready | All colors, spacing, radii, shadow, and typography bound to tokens. |
| C4 | Native Mappability | Ready | Maps to .sheet / .alert on iOS and Dialog / AlertDialog on Android. |
| C5 | Interaction State Coverage | Requires Rework | No destructive, error, or loading variants. Close affordance only on Version 2. |
| C6 | Asset & Icon Quality | Requires Rework | Hero is a flat raster placeholder with "Replace me" overlay. Should be a swappable Image slot. |
| C7 | Code Connect Linkability | Needs Refinement | No CLI mappings registered yet. |
| Type | Width | Hero | CTAs | Node ID |
|---|---|---|---|---|
| Default | 320px | 320 × 180 (raster) | 1 primary | 18477:23789 |
| 2 CTA | 320px | 320 × 180 (raster) | 1 outline + 1 text | 18477:23797 |
| Version 2 | 312px | 280 × 180 (raster, in container) | 1 primary + close icon | 18477:23806 |
Default (generic), 2 CTA (count), Version 2 (version). Should be a single semantic axis (single-cta / dual-cta / dismissible).
OpenModals Asset image. Should be a swappable Image slot via instance swap.
Open