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 bg/color-bg-primary circle — drop any DS icon in. |
| Discount | Yes | Yes | Variant=Discount | Taller banner (140 × 152) over a white content block with title and #amount. The card hugs its content — 140 × 212 with the placeholder copy. |
| Loading | Yes | Yes | isLoading=true | Banner and text become flat #eef2f9 blocks. Each loading version is the same height as the card it replaces — Default 214, Discount 212 — so nothing jumps when the content arrives. 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. |
- The Discount card sizes to its own text now. Its height was pinned to a fixed number, so a longer title overflowed instead of pushing the card taller. It hugs its content now — 212 tall with the placeholder copy, more when the title runs to two lines. C4 · Native Mappability
- The loading card is the same height as the real one. The skeletons were 212 and 211 where the loaded cards were 214 and 212, so the layout shifted the moment content arrived. Both now match the card they stand in for. C4 · Native Mappability
- The invisible white backgrounds are gone. Every version carried a white fill switched off — nothing drew it, but it read as a surface to anyone inspecting the layers. All eight are clean. C1 · Layer Structure & Naming
- The title and description can be set on a copy. Only the price was a real setting. The heading and body text were plain layers, so anyone placing the card had to detach it to change the words.
TitleandDescriptionare now settings, matchingAmount. C2 · Variant & Property Naming - The banner dimmer is a plain colour on purpose. It is the one value not tied to a named colour. That is deliberate — the overlay is tuned per asset so the badge and text above it stay readable on any image. Not a gap. C3 · Token Coverage
- 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
- 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
- Audit the colour token bindings. v1.1: Applied — design confirmed every binding. Text roles use
text/color-text-strong,-weaker,-weakand-weakest; Discount's title istext/color-texton abg/color-bg-mainsurface; the icon circle isbg/color-bg-primary; the violator istext/color-text-inverseonbg/brand-bg-primary. The banner dimmer stays a plain colour by design. Token - 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 a pressed state. v2.0: Applied — the card gained a pressed state. Focus is not part of the set: these are mobile components, so focus is handled natively rather than as a version. 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
Title
Description here.
Description here.
The card paints no surface of its own. Both text roles shift one step weaker when isPressed=true.
| Role | Token | Default | Pressed | Disabled |
|---|---|---|---|---|
| #title | text/color-text-strong · pressed text/color-text-weak | #2340A9 | #445C85 | – |
| #description | text/color-text-weaker · pressed text/color-text-weakest | #6780A9 | #90A8D0 | – |
| Violator label | text/color-text-inverse | #FFFFFF | #FFFFFF | – |
| Violator background | bg/brand-bg-primary | #1972F9 | #1972F9 | – |
| Banner dimmer | — plain colour by design | #E6E1EF · 40% · Multiply | #E6E1EF · 40% · Multiply | – |
Same palette as Default, plus the circular ⤷ Icon slot behind whichever DS icon is dropped in.
| Role | Token | Default | Pressed | Disabled |
|---|---|---|---|---|
| #title | text/color-text-strong · pressed text/color-text-weak | #2340A9 | #445C85 | – |
| #description | text/color-text-weaker · pressed text/color-text-weakest | #6780A9 | #90A8D0 | – |
| ⤷ Icon fill | bg/color-bg-primary | #005CE5 | #005CE5 | – |
| Violator label | text/color-text-inverse | #FFFFFF | #FFFFFF | – |
| Violator background | bg/brand-bg-primary | #1972F9 | #1972F9 | – |
| Banner dimmer | — plain colour by design | #E6E1EF · 40% · Multiply | #E6E1EF · 40% · Multiply | – |
Label here
Label here
PHP 200.00
Discount carries a real white content surface so it can sit on non-white backgrounds. Its title uses a darker token than the other two versions, and is deliberately smaller so a two-line label fits.
| Role | Token | Default | Pressed | Disabled |
|---|---|---|---|---|
| Surface | bg/color-bg-main | #FFFFFF | #FFFFFF | – |
| #title | text/color-text · pressed text/color-text-weak | #0A2757 | #445C85 | – |
| #amount | text/color-text-strong · pressed text/color-text-weakest | #2340A9 | #90A8D0 | – |
| Violator label | text/color-text-inverse | #FFFFFF | #FFFFFF | – |
| Violator background | bg/brand-bg-primary | #1972F9 | #1972F9 | – |
| Banner dimmer | — plain colour by design | #E6E1EF · 40% · Multiply | #E6E1EF · 40% · Multiply | – |
iOS — Swift Package Manager
.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")
Android — Gradle (Kotlin DSL)
implementation("com.eastblue.ds:carousel:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.carousel.*
Planned API — the native library does not exist yet. Snippets show the intended shape, not shipped code.
Figma properties mapped to the intended native parameters, in the order the Figma property panel lists them.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| Variant — Default, With Icon, Discount | variant: EBCarouselCardVariant | variant = EBCarouselCardVariant.Default / WithIcon / Discount |
| isLoading — true, false | isLoading: Bool = false | isLoading: Boolean = false |
| isPressed — true, false | isPressed: Bool = false — driven by the press gesture | isPressed: Boolean = false — driven by the press gesture |
| hasViolator — true, false | showViolator: Bool = true | showViolator: Boolean = true |
| ⤷ Violator (slot) | @ViewBuilder violator: () -> Violator | violator: @Composable () -> Unit |
| ⤷ Icon (slot) — With Icon only | @ViewBuilder icon: (() -> Icon)? | icon: (@Composable () -> Unit)? = null |
| Banner (slot) | @ViewBuilder banner: () -> Banner | banner: @Composable BoxScope.() -> Unit |
| Title (text) | title: String | title: String |
| Description (text) — Default, With Icon | description: String? | description: String? = null |
| Amount (text) — Discount | amount: String? | amount: String? = null |
EBCarouselCard( variant: .default, title: "Title", description: "Description here. Description here.", violator: "New" ) { Image(ad.banner) }
EBCarouselCard( variant = EBCarouselCardVariant.Default, title = "Title", description = "Description here. Description here.", violator = "New" ) { AsyncImage(model = ad.banner, contentDescription = null) }
EBCarouselCard( variant: .withIcon, title: "Title", description: "Description here. Description here.", violator: "New", icon: Image(systemName: "star.fill") ) { Image(ad.banner) }
EBCarouselCard( variant = EBCarouselCardVariant.WithIcon, title = "Title", description = "Description here. Description here.", violator = "New", icon = { Icon(Icons.Filled.Star, null) } ) { AsyncImage(model = ad.banner, contentDescription = null) }
EBCarouselCard( variant: .discount, title: "Label here", amount: "PHP 200.00", violator: "New" ) { Image(ad.banner) }
EBCarouselCard( variant = EBCarouselCardVariant.Discount, title = "Label here", amount = "PHP 200.00", violator = "New" ) { AsyncImage(model = ad.banner, contentDescription = null) }
| 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×214 — comfortably above 44 pt ✓ | 140 dp × 214 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. |
Do
Use <code>Discount</code> when the price is the point — a voucher, a deal, a promo tile.
Don't
Don't use it for editorial cards. Its title is smaller so two lines of label fit, which makes headlines read as body text.
Do
Put campaign artwork in the <code>Banner</code> slot from the product's own asset pipeline.
Don't
Don't add campaign images to the design system library — the slot exists so they never have to be.
Do
Let the card size to its own text. Title and description both wrap.
Don't
Don't pin a height. All three versions hug their content, and a fixed height clips a longer title.
Do
Turn <code>hasViolator</code> off when there is nothing to flag.
Don't
Don't repurpose the violator for a price or a discount — that is what <code>Amount</code> is for.
| 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 | Ready | Every colour is bound and confirmed by design: text roles on text/color-text-*, the icon circle on bg/color-bg-primary, the violator on bg/brand-bg-primary, Discount's surface on bg/color-bg-main. The #e6e1ef banner dimmer is a plain colour on purpose — it is tuned per asset so the badge and text above it stay readable. |
| 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. |
3 Variant × 2 isLoading × 2 isPressed = 12 combinations, of which 8 are built. With Icon has no loading version — a skeleton hides the icon, so it reuses Default's. The three loading-and-pressed pairings cannot occur.
| Variant | isLoading | isPressed | Height | Node |
|---|---|---|---|---|
| Default | false | false | 214 | 5653:42029 |
| With Icon | false | false | 214 | 5655:42597 |
| Discount | false | false | 212 | 5655:42551 |
| Default | false | true | 214 | 5670:43340 |
| With Icon | false | true | 214 | 5670:43348 |
| Discount | false | true | 212 | 5670:43358 |
| Default | true | false | 214 | 5663:43019 |
| Discount | true | false | 212 | 5663:43020 |
com.eastblue.ds.carouselcard.*. Under the packaging rule settled on 2 September the package is the family — Carousel — so it is com.eastblue.ds.carousel.*. One package per family keeps imports stable when a family gains a component. The Gradle artifact keeps the slug and is unchanged.Title and Description are now settings alongside Amount. This is what makes the release a minor rather than a patch.text/color-text-strong, -weaker, -weak and -weakest; Discount sits on bg/color-bg-main; the icon circle is bg/color-bg-primary; the violator is text/color-text-inverse on bg/brand-bg-primary. The colour-token audit recommendation is applied and C3 moves to Ready.isLoading, a setting, not a version, and it displaced Discount, which had no card at all. The cards are now the three real versions, on the real nodes; the old 23:1213xx ids exist nowhere in the set.Heading/Small and Body/Small, previously documented, are not design system styles at all.type setting, with the banner and text listed as hardcoded. It now maps all ten settings. Installation, Usage Snippets and Usage Guidelines were empty and are written; the variant count was 3 and is 8.Banner slot. All three now render the same canonical call.23:121311 to 5655:42547. The assessment was repointed at the time; this entry records the move itself, which had gone unwritten.Carousel - Discount Card became a Variant value rather than its own component, taking the Carousel family from five components to three.type mixed content shape with loading state. It split into Variant (Default / With Icon / Discount), isLoading and isPressed — three independent settings, and the matrix went from 3 versions to 8._space_12 instance was faking a gap. Auto-layout spacing does the job and has a native equivalent.content.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