A titled rail that scrolls Ad Space cards sideways, with an optional button in the header. One version, driven by a content slot.
Header holding #title and an optional button, over a ⤷ CarouselSlot that holds the cards. Names match Ad Space exactly, and the slot is the single control for what appears in the rail. It is badged Needs Refinement for one reason: nothing inside it is actually about ads. A title, a button, and a row of cards is the generic rail pattern, so the ad-specific framing is worth revisiting once the Carousel family is reviewed.Sits on dashboard and category surfaces as a sponsored rail — a section heading, an optional "Show more" button, and a horizontally scrolling row of Ad Space cards with the next one peeking at the right edge.
⤷ CarouselSlot takes whatever card is dropped into it. Every ad surface that needs a scrolling row uses this one component rather than a bespoke frame.Button - XSmall rather than a hand-built shape.Header, #title, and a ⤷ -prefixed slot. The duplicate hand-built button and the spacer node that used to sit in the header are both gone.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| hasCTA=false | N/A | N/A | default | Heading only. This is the default the component ships with. |
| hasCTA=true | N/A | N/A | Button - XSmall | Adds the "Show more" button at the right of the heading row. |
| ⤷ CarouselSlot | N/A | N/A | content slot | Selects which Ad Space version fills the rail. Promo is the default. |
| Peek | N/A | N/A | overflow clip | The rail is wider than the shell, so the next card is cut off at the right edge to signal scrollability. |
- One button in the heading, not two. The header used to carry a hand-built "Show more" — an
offsetframe, a spacer, and an arrow — alongside a realButton - XSmallinstance saying the same thing. The hand-built one is gone. C1 · Layer Structure & Naming - The spacer node is gone. A
_space_8instance was faking a gap between the label and the arrow. Spacer nodes have no native equivalent — auto layout spacing does the job — and it went with the hand-built button. C4 · Native Mappability - Names match Ad Space.
header-contentbecameHeader, and the emptyheaderframe wrapping it was removed. The heading text is#title, the same name Ad Space uses for the same job. C1 · Layer Structure & Naming - The one-value setting is gone. The component was a set with a single
Type=Defaultversion — a dropdown with nothing to choose. It is now a plain component; what varies is the slot content andhasCTA. C2 · Variant & Property Naming - Banner filling the rail edge to edge is intentional. A 320-wide Banner in a 360-wide shell leaves no room for the next card, so nothing peeks — Promo shows about 96px and Receipt about 20px. Confirmed as intended: Banner is the full-width option, and the rail still scrolls. No change to the inset or the gap. C1 · Layer Structure & Naming
- The next card now actually peeks. With Banner cards in the rail, the second card started exactly on the shell's right edge — nothing showed, so the rail read as a single card. Promo is now the default slot content, which leaves about 96px of the next card visible. C1 · Layer Structure & Naming
- Revisit whether this should be ad-specific. Strip the name away and this is a heading, an optional button, and a row of cards — the generic rail. Swap Ad Space for Carousel Card and it is the dashboard rail. Keeping it ad-specific was a deliberate call this pass, because calling it Carousel would collide with Carousel Card and Carousel Item. Settle it as a family decision when the Carousel family is reviewed, so the design system does not end up with one rail per card type. Family
- Give the heading a way to switch off.
hasCTAhides the button, but the heading itself is always on. Rails that sit under an existing section heading would repeat it. AhasHeaderboolean, matching thehasprefix already used forhasCTA, would cover that case. Property - Make the button a slot if the label ever varies. The button is a fixed
Button - XSmallinstance today. If "Show more" ever becomes "See all" or "View offers", promote it to⤷ ActionSlot, the same pattern Tooltip uses, so the label and icon can change without detaching. Slot - Confirm the heading colour is token-bound.
#titlerenders#072592and the button label#005CE5. Both should resolve to named colours rather than raw values. Worth a Dev Mode check, since the read-only tools used for this assessment report the hex without showing whether a token sits behind it. Token - Treat the rail as a list for assistive tech. Expose the rail as a single horizontally scrollable group with the heading as its label, and each card as an element within it, so screen reader users can skip the whole rail instead of stepping through every ad. A11y
- See siblings: Ad Space supplies every card in the rail. Carousel Card and Carousel Item are the non-ad equivalents and are the reason this component is not simply called Carousel. Family
The frame paints a real white surface. The cards inside the rail carry their own colours — see Ad Space.
| Role | Token | Value |
|---|---|---|
| Surface | bg/color-bg-main | #FFFFFF |
| #title | text/color-text-stronger | #072592 |
| CTA label | main/button/tertiary/brand/enabled/label | #005CE5 |
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:ad-space:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.adspace.*
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.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| Title (text) | title: String | title: String |
| hasCTA — true, false | action: EBAdCarouselAction? — nil hides the button | action: (() -> Unit)? = null |
| ⤷ CarouselSlot (slot) | @ViewBuilder content: () -> Content | content: @Composable RowScope.() -> Unit |
EBAdCarousel( title: "Title", action: EBAdCarouselAction("Show more") { /* action */ } ) { ForEach(ads) { ad in EBAdSpace(.promo, header: ad.headline) { Image(ad.creative) } } }
EBAdCarousel( title = "Title", action = { /* action */ } ) { ads.forEach { ad -> EBAdSpace(variant = AdSpaceVariant.Promo, header = ad.headline) { AsyncImage(model = ad.creative, contentDescription = null) } } }
EBAdCarousel(title: "Title") { ForEach(ads) { ad in EBAdSpace(.promo, header: ad.headline) { Image(ad.creative) } } }
EBAdCarousel(title = "Title") { ads.forEach { ad -> EBAdSpace(variant = AdSpaceVariant.Promo, header = ad.headline) { AsyncImage(model = ad.creative, contentDescription = null) } } }
| Requirement | iOS | Android |
|---|---|---|
| Rail is one navigable group | accessibilityElement(children: .contain) on the rail, labelled by #title | Modifier.semantics { collectionInfo = CollectionInfo(1, itemCount) } |
| Heading is exposed as a heading | .accessibilityAddTraits(.isHeader) | Modifier.semantics { heading() } |
| Button reachable without scrolling the rail | Button precedes the rail in the accessibility order | Button precedes the rail in the traversal order |
| Clipped card is still reachable | Scroll to reveal on accessibility focus | bringIntoViewRequester on focus |
Do
Use Promo cards when the rail should read as scrollable — about 96px of the next card stays visible.
Don't
Don't expect a peek from Banner cards; at 320 wide they fill the rail by design.
Do
Let the heading describe the whole rail, and switch off the card titles inside it.
Don't
Don't repeat the same wording in the rail heading and in every card.
Do
Turn on the button only when there is somewhere further to go.
Don't
Don't ship a "Show more" that leads back to the same surface.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Two levels, names matching Ad Space. Banner filling the rail edge to edge is intentional. |
| C2 | Variant & Property Naming | Ready | The one-value Type setting was removed. hasCTA follows the boolean prefix convention. |
| C3 | Token Coverage | Needs Refinement | Confirmed by design: surface bg/color-bg-main, #title text/color-text-stronger, CTA label main/button/tertiary/brand/enabled/label. |
| C4 | Native Mappability | Ready | The spacer instance is gone. Heading plus scrolling row maps to a lazy row with a header on both platforms. |
| C5 | Interaction State Coverage | Not Applicable | The container has no states of its own. Pressed behaviour belongs to the cards and the button. |
| C6 | Asset & Icon Quality | Needs Refinement | The button's arrow uses a shape_full boolean operation — delegated to the iconography team, not fixable here. |
A single component, not a component set — 1 variant. hasCTA is a boolean property and ⤷ CarouselSlot is a slot, so neither multiplies the matrix. The two rows below are the states that boolean produces.
| hasCTA | Header | Node |
|---|---|---|
| false | #title only | 5703:38564 |
| true | #title + Button - XSmall | 5703:38564 |
Primary/* layer is Proxima Soft and every Secondary/* layer is BarkAda, but the preview never said so: the root declared no font-family at all, which resolves through .eb-preview-scope to body — the documentation font, BarkAda. Both text layers here are Primary — #title at Primary/Label/Large and the CTA label at Primary/Label/Small — so the whole preview was in the wrong face. There is no Secondary text on this component, so the root declaration settles it.com.eastblue.ds.adcarousel.*. Under the packaging rule settled on 2 September the package is the family — Ad Space — so it is com.eastblue.ds.adspace.*. One package per family keeps imports stable when a family gains a component. The Gradle artifact keeps the slug and is unchanged.hasCTA defaults to false. v1.1.1 recorded the opposite, reading the toggle's on state in the Figma file as the component default. It was set on to show the full scope for preview. The panel control, the spec-card fallback snippet and the Usage Snippets order all now start from the button hidden.Title property now has a control. v1.1.0 recorded it as blocked on a shared renderer change; the renderer gained a control: 'input' kind later the same day, so the heading is editable from the spec-card panel. That row is superseded, not deleted.hasCTA=false "default" while the Figma panel ships the button on. The two snippets are now "With the button (default)" and "Heading only (hasCTA=false)", in that order.ad.image, the Code tab used $0.image, and the live getSnippet used Image(ad.creative). All three now match the generated form.hasCTA=false the shipped default. The Figma panel shows it on. Overview is not this run's scope — logged for a Component Review pass.Layout cut to the canonical keys — Side inset, Heading to rail, Card gap and Next card visible came out in favour of Padding H, Padding V, Gap and Alignment.#title is Primary/Label/Large and the CTA label is Primary/Label/Small, replacing the previously documented Primary/Bold/Heading and its font rows.bg/color-bg-main, #title text/color-text-stronger, CTA label main/button/tertiary/brand/enabled/label. The previous text/color-text-heading and text/color-text-link paths were unverified. Unlike Ad Space, the frame here paints a real white fill, so the surface row is correct as written.⤷ CarouselSlot is a slot, so the panel drops it and hasCTA is the only control — a toggle, defaulting to true as the Figma panel does.getSnippet in demos/ad-carousel.js rebuilds both snippets from the card's controls, so turning hasCTA off drops the action argument.gcash/east-blue-ios and com.gcash.eastblue:components:1.0.0 became AY-Org/eb-ds-ios and com.eastblue.ds:ad-carousel:1.1.3, and an Import block was added. Property Mapping was regrouped and Code Connect emptied.2 by counting hasCTA's two states; a boolean property does not multiply the variant matrix.#title gained a Title text property. The panel previously exposed only hasCTA and ⤷ CarouselSlot, so the heading could not be set from an instance while Ad Space exposed one for the same job. Design added it during this pass, named to match Ad Space, default value Title. It sits first in the panel.Header frame stays 18px and the button overflows it, centred. The preview now matches.SpecCard.astro renders select and toggle only, so the heading cannot be edited from the panel. Blocked on a shared renderer change — the same gap logged on Ad Space.Button - XSmall instance stays._space_8 spacer instance deleted with the hand-built button.header-content renamed to Header, the redundant header wrapper removed, and the heading text renamed to #title.Type setting removed — the component set became a plain component, which is why the node changed.