A single tab inside Tabs — label, swappable Icon slot, optional Counter and red dot. 24 variants across State (Default/Hover/Disabled) × Orientation (Vertical/Horizontal) × Size (Medium/Large) × Placement (Leading/Trailing) × isSelected.
isActive? renamed isSelected with lowercase true/false, a real Icon Slot replaces the placeholder circle in both orientations, the counter is a Counter INSTANCE on tokenised #EEF2F9, and a State axis (Default / Hover / Disabled) now ships alongside isSelected — 24 variants. Only Code Connect registration remains.Tab Items appear inside the Tabs container. See the Tabs in-context preview for the full screen layout.
Counter instance on #EEF2F9 rather than a locally drawn pill with raw hex, so token changes propagate.State × Orientation × Size × Placement × isSelected. Booleans are lowercase true/false, matching the DS-wide standard, and State is kept separate from isSelected the way Radio Button models it. Placement is scoped to Horizontal by design, since Vertical stacks the icon above the label.Icon Slot SLOT (32px vertical / 24px horizontal) lets consumers swap in any icon, and the counter instances the canonical component so its updates propagate. Nests inside Tabs as an instance.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Selected | Yes | Yes | isSelected=true | Brand #005CE5 indicator border on the container, brand label. |
| Unselected | Yes | Yes | isSelected=false | Muted label #6780A9, neutral #E5EBF4 border. |
| Horizontal | Yes | Yes | Orientation=Horizontal | Label beside the icon, 24px Icon Slot. Placement puts the icon leading or trailing. |
| Vertical | Yes | Yes | Orientation=Vertical | Icon above the label, 32px Icon Slot. Only Placement=Leading ships. |
| Counter | Yes | Yes | Counter instance | Pill on #EEF2F9, radius 99999. A real instance, so Counter updates propagate. |
| Red dot | Yes | Yes | red-dot | 6px #D61B2C notification dot. Modelled as a fixed layer by design rather than a boolean or slot. |
| Hover (pressed) | Yes | Yes | State=Hover | Touch-down feedback — container stroke darkens to #2340A9, the DS pressed navy. Named Hover here; maps to the platform pressed binding (configuration.isPressed / InteractionSource) since touch has no hover. Ships at isSelected=true. |
| Disabled | Yes | Yes | State=Disabled | Non-interactive tab. Ships at isSelected=false — the current tab is not disabled in practice. |
- Code Connect mappings not registered. All asset, composition, naming, and state blockers are resolved. Registration is unblocked but the SwiftUI / Compose mappings are not yet wired and the native component does not exist — snippets remain a Planned API. Note for whoever wires it:
State=Hovermaps to the pressed/isPressedbinding on both platforms. C7 · Code Connect Linkability
- v2.0: Property
isActive?renamedisSelected— the trailing?is gone from the generated type. Values remain capitalised and are tracked separately. (C2) - v2.0: Icon placeholder replaced with a real Figma
Icon SlotSLOT — 32 × 32 vertical, 24 × 24 horizontal, both empty and swappable. The hardcoded greyicon-placeholdercircle is gone. (C6) - v2.0: Counter is now a
CounterINSTANCE rather than a locally drawn pill, so updates to the canonical component propagate to every Tab Item. (C6) - v2.0: Counter colours moved off raw hex — the instance ships on
#EEF2F9, matching the DS counter token, replacing the hardcoded#ECF1FA/#0F3390pair. (C3) - v2.0: Leading-icon behaviour unified across orientations — both Vertical and Horizontal now carry the same
Icon Slot, and icon position is expressed by thePlacementproperty rather than a vertical-always-renders rule plus ahasLeadingIconboolean. (C2) - v2.0:
red-dotas a fixed 6px#D61B2CELLIPSE confirmed intentional — kept as a layer rather than promoted to a boolean property or slot. (C6) - v2.1:
isSelectedvalues renamedTrue/False→true/falseacross all 12 variants — Tab Item is now on the DS-wide boolean standard alongside Radio Button, Select Item, and Select. Verified with no stray property values or conflicting variants. (C2) - v2.1: Interaction states added — a
Stateaxis (Default / Hover / Disabled) takes the set from 12 to 24 variants.Stateis kept orthogonal toisSelectedrather than collapsed into it, because the two vary independently:isSelectedis which tab is active,Stateis the transient interaction. Same model as Radio Button. (C5) - v2.1:
State=Hoverretained deliberately rather than renamedPressed. The variants are painted#2340A9(the DS pressed navy) and behave as the touch-down state; the term is a naming convention only. Tab Item is the sole component usingHover— Button, Radio Button, Select Item, and Select Group all sayPressed— so Code Connect should bind it to the platform pressed state. (C2) - v2.1: State coverage confirmed intentional —
Hoverships only atisSelected=trueandDisabledonly atisSelected=false, giving 24 of 36 theoretical cells. Reviewed and accepted as the useful set. (C5) - v2.1:
Placementbeing meaningful only for Horizontal confirmed intentional — Vertical stacks the icon above the label, so a trailing (below-label) variant is not a layout GCash uses. (C2)
- Register Code Connect mapping to
EBTabItem. WireState,Orientation,Size,Placement, andisSelected1:1 to the SwiftUI / Compose API. MapState=Hoverto the platform pressed binding (configuration.isPressed/InteractionSource), since touch has no hover. Docs
- Rename
isActive?→selectedwithtrue/false. v2.1: Fully applied — the property isisSelected(trailing?gone) and all 12 variants now carry lowercasetrue/false, matching Radio Button, Select Item, and Select. Code Connect can map straight toBool. Rename - Unify the leading-icon slot. v2.0: Applied — both orientations carry the same
Icon Slot, with icon position expressed byPlacementinstead of a vertical-only render plus ahasLeadingIconboolean. Property - Replace the local counter pill with an instance. v2.0: Applied —
Counteris a real INSTANCE, so its updates propagate to every Tab Item. Composition - Replace
icon-placeholderwith a swappable Icon slot. v2.0: Applied — a real FigmaIcon SlotSLOT ships in both orientations. Slot - Add tokens for the counter. v2.0: Applied — the counter instance ships on
#EEF2F9, matching the DS counter token rather than raw hex. Token - Add
pressedanddisabledstates. v2.1: Applied — aStateaxis now ships (Default / Hover / Disabled), taking the set from 12 to 24 variants.Stateis orthogonal toisSelected, matching the Radio Button model rather than collapsing the two. State
Icon above label. 32px icon, 16/16 label (Primary/Label/Base). Active + inactive shown side-by-side.
Two states: active and inactive. Counter colors are currently hardcoded (flagged as C3).
| Role | Token | Active | Inactive |
|---|---|---|---|
| Label | main/tab/color/{active|inactive}/label | #005CE5 | #6780A9 |
| Bottom border | main/tab/color/{active|inactive}/border | #005CE5 | #E5EBF4 |
| Counter bg | — (hardcoded) | #ECF1FA | #ECF1FA |
| Counter label | — (hardcoded) | #0F3390 | #0F3390 |
| Red dot | — (raster) | Red 6×6px indicator |
Icon above label. 32px icon, 18/18 label (Primary/Label/Large). Optimized for 414px screens.
| Role | Token |
|---|---|
| Vertical padding (top) | 12px |
| Vertical padding (bottom) | 16px |
| Horizontal padding (left/right) | 12px |
| Icon size (vertical) | 32 × 32 |
| Icon size (horizontal leading) | 24 × 24 |
| Icon → label gap (vertical) | 12px |
| Counter size | 18px h × padding 6h / 4v |
| Counter radius | 99999px (pill) |
| Red dot size | 6 × 6 (top-right) |
| Bottom border | 2px solid |
| Horizontal small width | 105px |
| Horizontal large width | 112px |
Label-first row. Optional leading icon (24px) and trailing counter (18px pill). Red dot anchored to top-right.
| Role | Token | Spec |
|---|---|---|
| Small | Primary/Label/Base | Proxima Soft Bold · 16 / 16 · +0.25 |
| Large | Primary/Label/Large | Proxima Soft Bold · 18 / 18 · +0.25 |
| Counter | — (hardcoded) | Proxima Soft Bold · 12 / 12 · +0.5 |
Same anatomy as horizontal small but with 18/18 label and 112px cell width.
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:tabs:1.0.0") }
| Figma Property | SwiftUI | Compose |
|---|---|---|
| isActive?=Yes/No | selected: Bool | selected: Bool |
| orientation=vertical/horizontal | orientation: EBTabOrientation | .orientation(.vertical) |
| size=small/large | size: EBTabSize | .size(.small) |
| hasLeadingIcon + vertical always-on icon | leading: Icon? | leading: Image? |
| hasCounter=true/false | counter: Int? | counter: Int? |
| hasRedDot=true/false | showBadge: Bool | showBadge: Bool |
| — | label: String | title: String |
// Vertical tab with icon EBTabItem(label: "Overview", leading: Image("overview")) .selected(true) // Horizontal tab with counter EBTabItem(label: "Inbox", counter: 12) .orientation(.horizontal) .selected(false) // Horizontal tab with red-dot indicator EBTabItem(label: "Alerts", showBadge: true) .orientation(.horizontal)
// Vertical tab with icon EBTabItem( label = "Overview", leading = painterResource(R.drawable.overview), selected = true ) // Horizontal tab with counter EBTabItem( label = "Inbox", orientation = EBTabOrientation.Horizontal, counter = 12, selected = false ) // Horizontal tab with red-dot indicator EBTabItem( label = "Alerts", orientation = EBTabOrientation.Horizontal, showBadge = true )
| Requirement | iOS | Android |
|---|---|---|
| Selected state | .accessibilityAddTraits(.isSelected) | selected = true in semantics |
| Counter value | .accessibilityValue("\(count) unread") | contentDescription includes the count |
| Red dot | .accessibilityLabel("New") or append to label | Append to contentDescription |
| Tap target | Icon-only cells need 44pt hit area | Cells meet 48dp with padding |
Do
Use vertical orientation when tabs have icons + short labels (iconic nav). Use horizontal when labels are longer or icons aren't semantically needed.
Don't
Mix orientations within the same Tabs container.
Do
Use counter for numeric indicators (unread counts, items). Use showBadge for "new / unseen" indicators where the count is unimportant.
Don't
Stack counter + red-dot on the same tab — the counter already communicates "there's something here."
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic: container, icon-label, icon, label, counter-container, red-dot. |
| C2 | Variant & Property Naming | Requires Rework | isActive? Yes/No + inconsistent leading-icon slot across orientations. |
| C3 | Token Coverage | Requires Rework | Counter bg #ECF1FA and label #0F3390 are hardcoded. |
| C4 | Native Mappability | Ready | Custom cell on iOS, Material Tab content on Android. |
| C5 | Interaction State Coverage | Requires Rework | Pressed and disabled states not defined. |
| C6 | Asset & Icon Quality | Requires Rework | Placeholder circle instead of Icon slot; counter is duplicated, not a Badge instance. |
| C7 | Code Connect Linkability | Needs Refinement | Not mapped. |
| isActive? | orientation | size | Node ID |
|---|---|---|---|
| Yes | vertical | small | 18482:33263 |
| No | vertical | small | 18482:33270 |
| Yes | vertical | large | 18482:33277 |
| No | vertical | large | 18482:33284 |
| Yes | horizontal | small | 18482:33291 |
| No | horizontal | small | 18482:33300 |
| Yes | horizontal | large | 18482:33309 |
| No | horizontal | large | 18482:33318 |
isActive? has a ? and uses Yes/No. Leading-icon slot behaves differently across orientations.
Open#ECF1FA bg and #0F3390 label are raw hex. Should use tokens or instance the Badge component.
Open