A single tab inside the Tabs component — label, optional leading icon, and active/inactive/disabled states.
isActive? → selected (true/false). Unify the leading-icon slot across orientations. Replace the hardcoded counter (and its raw hex colors) with an instance of the canonical Badge. Replace the placeholder circle with a swappable Icon slot. C2 C3 C6Tab Items appear inside the Tabs container. See the Tabs in-context preview for the full screen layout.
#ECF1FA, #0F3390) instead of tokens. C3isActive? has a trailing ? and uses Yes/No. The leading-icon slot behaves differently across orientations: vertical always renders one, horizontal exposes hasLeadingIcon boolean. C2icon-placeholder instead of a swappable Icon slot. C6| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Active | Yes | Yes | isActive?=Yes | Blue label, blue bottom border |
| Inactive | Yes | Yes | isActive?=No | Gray label, light gray bottom border |
| Pressed / Disabled | N/A | N/A | — | Not defined. C5 |
| Has counter | Yes | Yes | hasCounter=true (horizontal only) | Counter pill. Should instance Badge, not duplicate colors. C6 |
| Has red dot | Yes | Yes | hasRedDot=true | 6px red dot in top-right corner |
- Property
isActive?has a?in its name. And usesYes/Novalues. Rename toselectedwithtrue/falseto match Checkbox/Radio conventions. C2 · Variant & Property Naming - Leading-icon slot is inconsistent across orientations. Vertical always renders an icon; horizontal gates it on
hasLeadingIcon. Unify to a singleleading = none | iconprop that behaves identically in both orientations. C2 · Variant & Property Naming - Counter pill uses hardcoded hex values.
#ECF1FAbg,#0F3390label — not bound to tokens, so theme changes won't propagate. C3 · Token Coverage - Counter is drawn locally instead of instancing Badge. Breaks compositional inheritance — future Badge updates (color, sizing, overflow) won't reach Tab Item. C6 · Asset & Icon Quality
- Icon is a hardcoded placeholder circle. 32px (vertical) / 24px (horizontal) gray
icon-placeholder— should be a swappable Icon slot via instance swap. C6 · Asset & Icon Quality - No pressed / disabled states documented. Tap feedback and non-interactive state are critical for a tab atom — engineers are improvising them today. C5 · Interaction State Coverage
- Code Connect mappings not registered. Blocked until property rename, slot adoption, and state coverage land. C7 · Code Connect Linkability
- Rename
isActive?→selectedwithtrue/falsevalues. Matches SwiftBool/ KotlinBooleanfor Code Connect. Rename - Unify the leading-icon slot — replace the always-on vertical icon and the
hasLeadingIconboolean with a singleleading=none/iconslot that behaves identically across orientations. Property - Replace the local counter pill with a Badge instance Badge already ships tokenized colors and will inherit any future token changes. Same pattern used when Avatar Group was repointed to the canonical Avatar. Composition
- Replace
icon-placeholderwith a swappable Icon slot (instance-swap). Lets product teams drop in any icon without editing the master. Slot - Add
pressedanddisabledstates as explicit variants so engineers don't have to improvise tap feedback. State - Add tokens for the counter under
main/tab/counter/{bg,label}if keeping it standalone, or adopt Badge's existing tokens. Token
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