A full-bleed banner item for a horizontal carousel — background image, heading, description, and a button. 24 versions across size, appearance, top element, and pressed state.
Carousel Item, and every issue from the first assessment is closed. Settings are now Size, Appearance, TopElement, and isPressed — a full 2 × 2 × 3 × 2 grid with all 24 versions built, no gaps. Background and ⤷ LeadingIcon are real slots, the chevron is a vector instance inside a proper Button, and pressed ships as a dim overlay. Code Connect stays unmapped because the native library doesn't exist yet.Carousel - Item is one card in a horizontal swipe carousel — typically a promotional banner stack on the Home or Dashboard screen. The center item is emphasized; side items peek in at reduced opacity/scale. In today's Figma file, those visual states exist as separate components (Item, Item - Center, Item - Side) rather than being driven by the carousel container.
Background is a slot, so any team drops in its own artwork without detaching. One component now covers what took three.Overlay instance rather than left to the consumer.Background, content, top-container, text-container, Button_New. Settings follow the naming guidelines: Size, Appearance, and TopElement in PascalCase, isPressed as an is-prefixed boolean with true / false values.Trailing Container slot. Stacks into a horizontal scroller; the carousel container itself is still undocumented and tracked as a recommendation.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | Size=Default | 282 × 160. Background slot, heading, description, and a button with a trailing chevron. |
| Small | Yes | Yes | Size=Small | 282 × 146. Same anatomy, 14px shorter. These are the two heights the old Center and Side components used, so a carousel that shrinks its side items swaps versions rather than applying a transform. |
| Dark appearance | Yes | Yes | Appearance=Dark | For dark artwork — heading in #ffffff, description in #f6f9fd at 80%. |
| Light appearance | Yes | Yes | Appearance=Light | For pale artwork — dark text. Note the inversion from the old mode setting: what was mode=Light Text is now Appearance=Dark. |
| Top element | Yes | Yes | TopElement=Icon | Preamble | None | Chooses what sits above the heading — a 24 × 24 ⤷ LeadingIcon slot, a preamble line, or nothing. |
| Pressed | Yes | Yes | isPressed=true | An Overlay instance drops a dim layer over the background at #020e22 / 24%. Text colours stay put. |
| Loading | N/A | N/A | Not built | No skeleton. Carousel Card ships one — deferred for now, tracked as a family-consistency item. |
- The heading and description can be set on a copy. Only the preamble was a real setting. The headline and body text were plain layers, so anyone placing the card had to detach it to change the words.
HeadingandDescriptionare now settings. C2 · Variant & Property Naming - The preamble setting is named after its layer. It was called
Text, which said nothing about what it fills. Renamed toPreamble, matching#preambleand the rest of the Carousel family. C2 · Variant & Property Naming - Every colour is a named colour. Both appearances carry their full token set, confirmed by design — including the surfaces,
bg/color-bg-brandon Dark andbg/color-bg-info-weakeston Light. C3 moves to Ready. C3 · Token Coverage - The three sibling components are now one. v2.0: rebuilt on node
5776:37969. Item, Center, and Side collapsed into a singleCarousel Item, and what was carousel position is now aSizesetting —Default(282 × 160) andSmall(282 × 146). C1 · Layer Structure & Naming - Property names follow the guidelines. v2.0:
Size,Appearance, andTopElementare PascalCase variant properties;isPressedis anis-prefixed boolean with lowercasetrue/false; the slot is⤷ LeadingIcon. The old mix ofhasPreamble,with Icon, andLight Textis gone. C2 · Variant & Property Naming -
type=Headline Onlyis gone. v2.0: replaced byTopElement = Icon | Preamble | None, which names what actually changes — the element above the heading. C2 · Variant & Property Naming -
modebecame a real appearance setting. v2.0: nowAppearance = Dark | Light, picking the whole text colour set at once instead of overriding layers. Watch the inversion — the oldmode=Light Textis the newAppearance=Dark. C4 · Native Mappability - The background is a slot. v2.0:
Backgroundis a SLOT on all 24 versions, wrapping anAssetinstance. Teams drop in their own artwork without detaching. C4 · Native Mappability - The chevron is a vector instance. v2.0: now
Chevron Right Small, riding in the button'sTrailing Containerslot. The rastershape_fullis gone, and it ships once rather than once per appearance. C6 · Asset & Icon Quality - The icon placeholder is a slot. v2.0:
⤷ LeadingIcon(5804:42380) is an empty 24 × 24 slot in place of the drawn grey circle. Any DS icon or Avatar drops in. C6 · Asset & Icon Quality - The card has a pressed state. v2.0:
isPressedships on all 24 versions. AnOverlayinstance dims the background with a#020e22layer at 24%, leaving text colours untouched. C5 · Interaction State Coverage
- Settle the
Button_Newname. The button instance inside every version is namedButton_New, which reads like a migration artifact. Held for the final reviewer to decide — it belongs to the Button component, not this one. Rename - Document the carousel container. Peek behaviour, snap-to-centre, the page indicator, and auto-advance belong on a dedicated
EBCarouselcontainer — not on each item. On iOS that's.scrollTransition; on Android,graphicsLayerkeyed offHorizontalPagerpage offset. Family - See siblings: Carousel Card — the narrow 140-wide card for the same rails. Keep slot naming and state coverage aligned across both. Family
- Decide whether Carousel Item needs a loading state. v2.1: Closed — decided against. Approved designs do not list a loading state for Carousel Item, and the recommendation came from an older asset. Carousel Card ships skeletons as its own approved pattern; that is not a precedent for this component. State
- Announce as a link or button. v2.1: Applied — the whole card is the tap target, matching the behaviour already shipping in the app. The Accessibility table documents it for both platforms, along with page-change announcements for the surrounding carousel. A11y
- Note why
SizeusesDefaultandSmall. v2.1: Applied — recorded in the v2.0.0 changelog entry:DefaultandSmallare a deliberate exception to theXS–XLscale, kept so the emphasis stays on the default behaviour rather than on a size step. Docs - Audit the colour token bindings. v2.1: Applied — design confirmed every binding on both appearances. Dark uses
bg/color-bg-brandwithtext/color-text-inverseand-inverse-weak; Light usesbg/color-bg-info-weakestwithtext/color-text,-strong,-weakerand-primary. The text link icon binds toborder/color-border-primaryand its inverse. Token - Consolidate Carousel - Item + Center + Side into a single
Carousel Item. v2.0: Applied — one component replaces three. Position became theSizesetting. Family - Add a
backgroundslot. v2.0: Applied —Backgroundis a SLOT on all 24 versions. Slot - Replace
modewith anappearanceenum. v2.0: Applied —Appearance = Dark | Lightnow picks the whole text colour set. Property - Rename
type=Headline Only. v2.0: Applied — replaced byTopElement = Icon | Preamble | None. Rename - Add a leading icon slot. v2.0: Applied —
⤷ LeadingIcon, an empty 24 × 24 slot. Slot - Vectorize the chevron. v2.0: Applied —
Chevron Right Smallinstance inside the button'sTrailing Containerslot. Asset - Add pressed state. v2.0: Applied —
isPresseddims the background via anOverlayinstance at#020e22/ 24%. State
Appearance=Dark is a dark surface with light text — the old mode=Light Text. Pressed reuses these colours and adds a dim layer over the whole card.
| Role | Token | Value |
|---|---|---|
| Surface | bg/color-bg-brand | #1972F9 |
| #preamble | text/color-text-inverse-weak | #F6F9FD · 80% |
| #heading | text/color-text-inverse | #FFFFFF |
| #description | text/color-text-inverse-weak | #F6F9FD · 80% |
| Text link label | text/color-text-inverse | #FFFFFF |
| Text link icon | border/color-border-primary-inverse | #FFFFFF |
| Pressed overlay | — dim layer added by isPressed | #020E22 · 24% |
Appearance=Light is a pale surface with dark text — the old mode=Dark Text. Same pressed behaviour.
| Role | Token | Value |
|---|---|---|
| Surface | bg/color-bg-info-weakest | #E5F1FF |
| #preamble | text/color-text-strong | #072592 · 60% |
| #heading | text/color-text | #0A2757 |
| #description | text/color-text-weaker | #6780A9 |
| Text link label | text/color-text-primary | #005CE5 |
| Text link icon | border/color-border-primary | #005CE5 |
| Pressed overlay | — dim layer added by isPressed | #020E22 · 24% |
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. Note Appearance names the surface, not the text — Dark is a dark card with light copy.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| Size — Default, Small | size: EBCarouselItemSize = .default | size: EBCarouselItemSize = Default |
| isPressed — true, false | isPressed: Bool = false — driven by the press gesture | isPressed: Boolean = false — driven by the press gesture |
| Appearance — Dark, Light | appearance: EBAppearance — .dark is a dark surface with light text | appearance: EBAppearance — Dark is a dark surface with light text |
| TopElement — Icon, Preamble, None | topElement: EBCarouselItemTop = .icon | topElement: EBCarouselItemTop = Icon |
| ⤷ LeadingIcon (slot) — shown when TopElement is Icon | @ViewBuilder leadingIcon: (() -> Icon)? | leadingIcon: (@Composable () -> Unit)? = null |
| Preamble (text) — shown when TopElement is Preamble | preamble: String? | preamble: String? = null |
| Heading (text) | heading: String | heading: String |
| hasDescription — true, false | description: String? — nil hides it | description: String? = null |
| Description (text) | description: String? | description: String? = null |
| Background (slot) | @ViewBuilder background: () -> Background | background: @Composable BoxScope.() -> Unit |
| hasTextLink — true, false | textLink: String? — nil hides the button | textLink: String? = null |
EBCarouselItem( appearance: .dark, heading: "Heading", description: "This is a description for this banner.", textLink: "Button", leadingIcon: Image(systemName: "star.fill") ) { Image(banner.asset) }
EBCarouselItem( appearance = EBAppearance.Dark, heading = "Heading", description = "This is a description for this banner.", textLink = "Button", leadingIcon = { Icon(Icons.Filled.Star, null) } ) { AsyncImage(model = banner.asset, contentDescription = null) }
EBCarouselItem( appearance: .light, heading: "Heading", description: "This is a description for this banner.", textLink: "Button", leadingIcon: Image(systemName: "star.fill") ) { Image(banner.asset) }
EBCarouselItem( appearance = EBAppearance.Light, heading = "Heading", description = "This is a description for this banner.", textLink = "Button", leadingIcon = { Icon(Icons.Filled.Star, null) } ) { AsyncImage(model = banner.asset, contentDescription = null) }
| Requirement | iOS | Android |
|---|---|---|
| Whole-card tap target | Wrap in Button with combined accessibilityLabel (preamble + heading + description + action). | Modifier.clickable().semantics(mergeDescendants = true). |
| Page-change announcements | Announce current page index via accessibilityValue on the carousel container. | semantics { contentDescription = "Banner 2 of 5" } on the pager. |
| Contrast on image | Designer ensures 4.5:1 between text + underlying image area; optional scrim overlay. | Same — ensure WCAG AA against the image region. |
| Reduce motion | Skip scale + opacity transitions when UIAccessibility.isReduceMotionEnabled. | Check Settings.Global.TRANSITION_ANIMATION_SCALE; skip graphicsLayer animation. |
| Min touch target | 282×160 ≫ 44 pt ✓ | 282×160 ≫ 48 dp ✓ |
Do
Pick <code>Appearance</code> by the surface you want, not the text. <code>Dark</code> is a dark card with light copy.
Don't
Don't map it from the old <code>mode</code> setting by memory. <code>mode=Light Text</code> is <code>Appearance=Dark</code> — the two are named for opposite things.
Do
Drop artwork into the <code>Background</code> slot from the product's own asset pipeline.
Don't
Don't bake artwork into the component. The slot is why one card serves every campaign.
Do
Check the copy against the artwork you ship. The card tints the surface, not the image.
Don't
Don't rely on the appearance alone for contrast — a busy photo can leave white text unreadable even on <code>Dark</code>.
Do
Use <code>Small</code> where the rail is tight; it keeps the same width and trims the description.
Don't
Don't reach for it to fit longer copy. It gives the description less room, not more.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | One component replaces three. All 24 versions share Background, content, top-container, text-container. |
| C2 | Variant & Property Naming | Ready | Size, Appearance, TopElement in PascalCase; isPressed boolean with lowercase values. Size=Default|Small is a deliberate exception to the XS–XL scale. |
| C3 | Token Coverage | Ready | Every colour is bound and confirmed by design. Dark sits on bg/color-bg-brand, Light on bg/color-bg-info-weakest, with the text roles on text/color-text-* and the link icon on border/color-border-primary. |
| C4 | Native Mappability | Ready | Background and ⤷ LeadingIcon map to @ViewBuilder / @Composable slots. Size and Appearance are plain enums. |
| C5 | Interaction State Coverage | Ready | Default and pressed built across all 24. No loading state — deferred, tracked as a family-consistency item. |
| C6 | Asset & Icon Quality | Ready | Vector Chevron Right Small instance; ⤷ LeadingIcon slot; Background slot. |
2 Size × 2 isPressed × 2 Appearance × 3 TopElement = 24 versions, all built — a complete grid with no gaps.
| Appearance | Versions | Count |
|---|---|---|
| Dark | 2 Size × 3 TopElement × 2 isPressed | 12 |
| Light | 2 Size × 3 TopElement × 2 isPressed | 12 |
View full Appearance × Size × TopElement breakdown (24 rows)
| Appearance | Size | TopElement | isPressed | Node |
|---|---|---|---|---|
| Dark | Default | Icon | false | 5804:39914 |
| Dark | Default | Icon | true | 5821:44302 |
| Dark | Default | Preamble | false | 5804:42210 |
| Dark | Default | Preamble | true | 5821:44312 |
| Dark | Default | None | false | 5821:44670 |
| Dark | Default | None | true | 5821:44814 |
| Dark | Small | Icon | false | 5804:42531 |
| Dark | Small | Icon | true | 5821:43953 |
| Dark | Small | Preamble | false | 5804:42540 |
| Dark | Small | Preamble | true | 5821:43973 |
| Dark | Small | None | false | 5821:44680 |
| Dark | Small | None | true | 5821:44825 |
| Light | Default | Icon | false | 5804:41501 |
| Light | Default | Icon | true | 5821:43983 |
| Light | Default | Preamble | false | 5804:42291 |
| Light | Default | Preamble | true | 5821:44003 |
| Light | Default | None | false | 5821:44690 |
| Light | Default | None | true | 5821:44836 |
| Light | Small | Icon | false | 5804:42549 |
| Light | Small | Icon | true | 5821:43993 |
| Light | Small | Preamble | false | 5804:42558 |
| Light | Small | Preamble | true | 5821:44013 |
| Light | Small | None | false | 5821:44700 |
| Light | Small | None | true | 5821:44847 |
com.eastblue.ds.carouselitem.*. 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.Heading and Description are now settings. This is what makes the release a minor rather than a patch.Text, which said nothing about what it fills. Renamed to Preamble, matching #preamble and the rest of the Carousel family.mode and type vocabulary. They are now Dark and Light, the two values of the driving Appearance setting.Appearance names the surface where mode named the text, so the old mode=Light Text is the new Appearance=Dark. Both colour tables and the Property Mapping now say so, because reading it from memory gets it backwards.Primary/Label/Fine, Primary/Headlines/Block, Secondary/Bold/Caption, Primary/Label/Small — each resolved through the style database rather than written by hand. Both Size values were read; Small trims the description box but keeps the same styles.bg/color-bg-brand on Dark and bg/color-bg-info-weakest on Light.8942:79573 and 8942:79589.Background and Overlay are positioned beneath content, so a press dims the artwork only.TopElement=Preamble and the description field only when hasDescription is on. Both now appear and disappear with those settings.mode, type and hasPreamble, with the background, icon and chevron all listed as baked in. It now maps all eleven settings. Installation, Usage Snippets and Usage Guidelines were empty and are written.10 while its own description claimed 24, using a multiplier built from settings that no longer exist. It now reads 24 — the real grid, with a summary and a full breakdown.18543:2806 to 5776:37969. The assessment was repointed at the time; this entry records the move itself, which had gone unwritten.Carousel - Item, Carousel Item - Center and Carousel Item - Side all shipped the same schema and differed only in height. They collapsed into a single Carousel Item, and what had been carousel position became a Size setting — Default (282 × 160) and Small (282 × 146).hasPreamble, with Icon and Light Text is gone. Size, Appearance and TopElement are PascalCase, isPressed is an is-prefixed boolean with lowercase true / false, and the slot reads ⤷ LeadingIcon.type=Headline Only actually rendered a preamble, a heading and a button. It became TopElement = Icon | Preamble | None, which names what really changes — the element above the heading.mode only swapped layer colours. It is now Appearance = Dark | Light, picking the whole set at once. Note the inversion when reading old files: the former mode=Light Text is the new Appearance=Dark.Background is a Figma Slot on all 24 versions.shape_full, once per appearance. It is now a Chevron Right Small instance riding in the DS button's Trailing Container slot — one copy, crisp at any size.⤷ LeadingIcon is now an empty 24 × 24 slot, so any DS icon or Avatar drops straight in.isPressed now dims the card with an Overlay instance at #020E22 / 24%, leaving text colours untouched.Size × isPressed × Appearance × TopElement gives 24 versions with no gaps, against 10 before. Size=Default|Small is a deliberate exception to the XS–XL scale, kept to put the emphasis on default behaviour.hasPreamble vs with Icon); Headline Only mis-named. Openmode should be appearance. Open