A tappable card used inside a horizontally scrolling carousel — banner, title, description, and an optional price. Comes in three versions: Default, With Icon, and Discount.
type setting became three independent ones — Variant (Default / With Icon / Discount), isLoading, and isPressed. Banner, icon, and violator are real slots. Discount Card merged in as a Variant value, taking the Carousel family from 5 components to 3. The follow-up pass cleared the last of the layer-naming drift: all eight versions now share one anatomy and one vocabulary. Code Connect stays unmapped because the native library doesn't exist yet — that's a dev-side dependency, not a design gap.Carousel Card lives in a horizontal scroller — typically a "Featured" or "For You" rail on a home or category screen. Cards are peeked (part of the next one visible) to signal scrollability.
Title
Description here.
Description here.
Asset, so dropping in real media replaces both.Banner, content, title, description, amount. Settings follow the naming guidelines: Variant in PascalCase, isLoading and isPressed as is-prefixed booleans. Discount's smaller, darker title is the multi-line label style, applied deliberately.⤷ Icon is an empty slot, so DS icons drop straight in. Cards stack cleanly into a horizontal scroller and the skeleton is first-class.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | Variant=Default | Banner slot + title + 2-line description. 140 × 214. |
| With Icon | Yes | Yes | Variant=With Icon | Default layout plus a 34 × 34 ⤷ Icon slot at the bottom-left of the banner. The slot ships empty on a #005ce5 circle — drop any DS icon in. |
| Discount | Yes | Yes | Variant=Discount | Taller banner (140 × 152) over a white content block with title and #amount. 140 × 226. |
| Loading | Yes | Yes | isLoading=true | Banner and text become flat #eef2f9 blocks. Built for Default (212) and Discount (211); With Icon reuses the Default skeleton, since a skeleton hides the icon anyway. |
| Pressed | Yes | Yes | isPressed=true | Title dims to #445c85 and description to #90a8d0. The banner does not change. |
| Focused | N/A | N/A | Not built | Not needed — this card ships on touch surfaces only. |
- Code Connect mappings not registered. Blocked — the native component library doesn't exist yet. Nothing to action on the design side. C7 · Code Connect Linkability
- The
typesetting no longer mixes content with loading state. v2.0: rebuilt on node5655:42547as three independent settings —Variant(Default / With Icon / Discount),isLoading, andisPressed. Picking a content version no longer forces a loading choice. C2 · Variant & Property Naming - The icon badge is a real slot. v2.0:
⤷ Icon(node5655:42609) is a 34 × 34 slot, empty by design. Any DS icon or Avatar drops straight in without detaching. It replaces the drawn#c2c6cfcircle. C6 · Asset & Icon Quality - The card has a pressed state. v2.0:
isPresseddims the title to#445c85and the description to#90a8d0. Focus was reviewed and dropped — this card ships on touch surfaces only. C5 · Interaction State Coverage - The spacing helper is out of the Discount card. v2.1:
_space_12removed from both Discount versions (5655:42551,5670:43358). Thecontentframe now runs#titlestraight into#amount, with the 12px gap carried by auto-layout spacing instead of a placed instance. C1 · Layer Structure & Naming - The loading version's layer names match the rest of the set. v2.2: both
senderbars are nowdescription(5663:43017,5663:43018), Discount's loading bars aretitleandamount, and both loading versions capitaliseBannerto match. The frame stays a plain frame rather than a slot, which is right for a skeleton — there is nothing to drop into a loading placeholder. C1 · Layer Structure & Naming - All eight versions name the content frame
content. v2.2: standardised oncontentrather thanblock-content, matching the wider design system —contentappears in 3,000+ instances against 23 forblock-content. Default, With Icon, Discount, and both loading versions now agree. C1 · Layer Structure & Naming - Discount's title treatment is intentional. v2.2: reviewed and dismissed. The smaller 14 / 16 size is the multi-line label text style, and the darker
#0a2757is deliberate — on a discount card the#amountcarries the main colour, so the title steps back. C3 · Token Coverage - The banner shadow is safe where it is. v2.2: reviewed and dismissed. Designers replace the image layer inside the
Assetinstance rather than the instance itself, sodimmerandshadowstay put as siblings and the icon keeps its contrast backing. C6 · Asset & Icon Quality - The banner placeholder is intentional. v2.0: reviewed and dismissed.
Banner(node5650:40680) is a slot, and thereplace-this-assetimage sits with its purple#e6e1efdimmer inside the swappableAssetinstance — real media replaces both at once. The matching recommendation to tokenise the dimmer was dropped for the same reason. C6 · Asset & Icon Quality
- Audit the colour token bindings. The review tooling reads raw hex and can't see which values are bound to variables, so C3 is recorded as unverified rather than passing. The one hex raised in review — the
#e6e1efdimmer — is an optional decorative overlay and is meant to be unbound. Run a token audit in Figma to confirm the rest and close C3 out. Token - Document the
⤷ Violatorslot. The slot ships a blue#1972f9"New" badge on all three content versions by design. Nothing in the docs mentions it, so designers won't know they can swap or clear it. Docs - Consolidate Carousel - Item + Carousel Item - Center + Carousel Item - Side into a single
Carousel Item. "Center" and "side" describe where an item sits in a peek carousel at runtime, not a version of the component. The carousel layout works out which item is centre — it shouldn't be baked in as a setting. Merge these 3 into oneCarousel Itemand let the parent carousel apply the peek transform. Family - Rename to clarify hierarchy.
Carousel Card(this component, now including Discount) handles the full-width banner pattern;Carousel Itemhandles the peek pattern. Keep the two names from overlapping once the Item family merges. Rename - Document the skeleton treatment as a shared DS convention. Carousel Card, Generic Card, and other card primitives all ship first-class skeletons — call out the pattern in the guidelines so card-family consistency holds as more components adopt it. Docs
- See siblings: Generic Card (horizontal list row) — same "card + skeleton + tappable" pattern, different layout. Keep skeleton treatment aligned across both. Family
- Consolidate Carousel Card + Carousel - Discount Card into a single
Carousel Card. v2.0: Applied — Discount is now aVariantvalue on this component rather than a separate one. The Carousel family drops from 5 components to 3. Family - Split
typeintovariant+isLoading. v2.0: Applied — shipped asVariant(Default / With Icon / Discount) plusisLoading, withisPressedadded alongside. Content and state are now separate choices. Property - Adopt Figma Slots for banner and icon badge. v2.0: Applied —
Bannerand⤷ Iconare both slots, and a third⤷ Violatorslot was added for the corner badge. Slot - Add pressed + focused states. v2.0: Applied —
isPresseddims the title and description. Focus was reviewed and dropped: this card ships on touch surfaces only. State - Replace the
_space_12helper with auto-layout spacing. v2.1: Applied — the instance is gone from both Discount versions and the 12px title-to-amount gap is now auto-layout spacing. Composition - Rename the loading version's leftover layers. v2.2: Applied — the
senderbars becamedescription/title/amount, andBanneris capitalised across both loading versions. Rename - Settle on one name for the content frame. v2.2: Applied — all eight versions use
content, chosen overblock-contentto match the 3,000+ existing instances elsewhere in the system. Rename
Banner image + title + 2-line description. The banner ships a placeholder PNG dimmed by a purple multiply layer — replace both with your real media.
Title
Description here.
Description here.
Promotional carousel card with heading, description, and pagination dots.
| Role | Token | Default |
|---|---|---|
| Surface | bg/color-bg-main | #FFFFFF |
| Heading | carousel/color/label-header | #2340A9 |
| Description | carousel/color/description | #6780A9 |
| Active dot | bg/color-bg-primary | #005CE5 |
| Inactive dot | bg/color-bg-strong | #EEF2F9 |
Default layout + a bottom-left icon badge on the banner. A gradient shadow along the lower third improves icon contrast against bright imagery.
Icon-led carousel card with a tinted icon container and stacked title/description.
| Role | Token | Default |
|---|---|---|
| Surface bg | main/card/bg | #FFFFFF |
| Icon container bg | main/card/icon/bg | #E8F1FF |
| Title | main/card/title | #0A2757 |
| Description | main/card/description | #3C4A5C |
Loading placeholder: banner becomes a flat light-gray block; title and description become bar placeholders. Card total height drops to 212 (vs 215 default) due to the 16 top gap in the content block.
Loading state — content slots become rounded grey rectangles on the card surface.
| Role | Token | Default |
|---|---|---|
| Skeleton bg | main/skeleton/bg | #EEF2F9 |
| Surface bg | main/card/bg | #FFFFFF |
| Figma Property | SwiftUI | Compose |
|---|---|---|
type: default | with icon | skeleton loader | variant: default | with-icon + isLoading: Boolean | variant: EBCarouselCardVariant + isLoading: Bool |
| (hardcoded raster) | image: Frame (slot) | image: () -> Image |
| (hardcoded text) | title: String | title: String |
| (hardcoded text) | description: String | description: String? |
| (hardcoded circle) | icon?: Icon (slot) | icon: EBIcon? |
| (hardcoded purple multiply) | overlay?: Color (token) | overlay: Color? |
| (not modeled) | onTap?: () -> Void | onTap: (() -> Void)? |
| Requirement | iOS | Android |
|---|---|---|
| Card as a button | Whole card wrapped in Button with combined accessibilityLabel (title + description). | Modifier.clickable { onClick() }.semantics(mergeDescendants = true) on the column. |
| Combined announcement | "Send Money, Locally or abroad, same day" — VoiceOver reads title then description. | Same reading order — TalkBack follows composition. |
| Image alt | If the banner carries meaning, pass accessibilityLabel on the image; otherwise mark as decorative. | contentDescription set when banner is content-bearing, null when decorative. |
| Min touch target | Card is 140×215 — comfortably above 44 pt ✓ | 140 dp × 215 dp — above 48 dp ✓ |
| Loading state | Announce "Loading" once on mount; suppress per-placeholder announcements. | contentDescription = "Loading" on the skeleton container. |
| Focus ring | Add a .focused() modifier → 2 px outline for tvOS/iPad keyboard. | Modifier.focusable() + border in border/focus token. |
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | All eight versions share one anatomy — Banner, content, title, description, amount. Names are semantic and consistent. |
| C2 | Variant & Property Naming | Ready | Variant in PascalCase; isLoading and isPressed as is-prefixed booleans. Follows the Property Naming Guidelines. |
| C3 | Token Coverage | Needs Refinement | The one raw hex flagged in review — the #e6e1ef dimmer — is an optional decorative overlay on the asset, so it is deliberately unbound. The read-only review tooling can't see variable bindings, so the wider set is recorded as unverified rather than failing; the token audit recommendation stays open to close it out. |
| C4 | Native Mappability | Ready | Maps to VStack / Column in a horizontal scroller. Slots map to @ViewBuilder / @Composable parameters. |
| C5 | Interaction State Coverage | Ready | Default, pressed, and loading all built. Focus reviewed and dropped — this card ships on touch surfaces only. |
| C6 | Asset & Icon Quality | Ready | Banner, ⤷ Icon, and ⤷ Violator are slots. The placeholder image is replaced inside the Asset instance, leaving dimmer and shadow in place. |
| C7 | Code Connect Linkability | Not Mapped | Blocked — the native component library doesn't exist yet. |
type (3) = 3 variants. default and with icon share the same overall dimensions (140 × 215); skeleton loader is 140 × 212 due to a different content padding.
| type | Node | Dimensions | Anatomy |
|---|---|---|---|
| default | 23:121312 | 140 × 215 | Banner + title + 2-line description. |
| with icon | 23:121322 | 140 × 215 | Default + gradient shadow + bottom-left 30×30 icon badge on banner. |
| skeleton loader | 23:121334 | 140 × 212 | Flat banner + 3 bar placeholders (title 16, desc 10, desc 10 @ 97w). |
type into variant + isLoading, adopt image + icon slots, drop the hardcoded purple dimmer, add pressed/focused states, and consolidate the 5-component Carousel family. OpenEBCarouselCardSkeleton. NotedCarousel Card (default / discount) and Carousel Item (peek). Openvariant for content, isLoading for state. Open