East Blue
KeepReady
Table Row Component link

A display-only table row — a leading label and however many data cells you drop into its Columns Slot. Comes in Header and Content roles, each with a disabled state.

Keep — rebuilt as one composable row
The old three-component setup (Table + Table - Item + Table - Label) is gone, and with it the no. of columns variant matrix. Table Row now composes Table Label and Table Cell as real instances through slots, so column count is whatever you put in the Columns Slot rather than a property to pick from. Settings are Role (Header / Content) and State (Default / Disabled) — 4 versions in place of 3 components and 14 variants. Rows are display-only by design, so there is no pressed or selected state. Two follow-up passes cleared the rest. The first moved #description to the 10px token, dimmed every text layer when disabled, and settled all three swappable areas on ⤷ …Slot. The second went through the two primitives: one setting name across all three components, text layers named after the properties that fill them, disabled built into Table Label and Table Cell rather than painted on by hand, and an empty version that is finally empty. Every colour is bound. Code Connect stays unmapped because the native library doesn't exist yet.
In Context

Sticker sheet shows Table instances stacked on a Template Screen to build a static 6-row pattern — 1 header row + 5 content rows. No scroll, no sort, no selection.

Title Header Column Column Column Label Desc Desc Desc Label Desc Desc Desc Label Desc Desc Desc Label Desc Desc Desc Label
Live Preview
Header
Title
Title
Title
Properties
Role
State
hasBorder
⤷ Table Label
Title
hasAsset
hasDescription
⤷ ColumnSlot
Table Cell ⤷ AssetSlot
Table Cells
DS Health
Reusable
Pass
The Columns Slot takes any number of Table Cell instances, and each cell carries its own Asset Slot and text. Column count, icons, and cell content are all the consumer's to set.
Self-contained
Pass
Row carries its own background, border, and typography per role. Nothing is locked at build time any more — the old fixed column matrix is gone.
Consistent
Pass
Role and State are PascalCase settings with Title Case values, and Default / Disabled is a valid subset of the standard interaction set. All three components in the family now use the same two names, and every text property matches the layer it fills. The no. of columns property, with its period, is gone.
Composable
Pass
Real composition at last — Table Label and Table Cell are placed as instances inside slots rather than redrawn. The old complaint was that both atoms were published but never actually slotted.
Behavior
State iOS Android Figma Property Notes
Header Yes Yes Role=Header 360 × 68 on a #F6F9FD ground. Label and cell text both use Primary/Label/Small, so the whole row is Proxima Soft.
Content Yes Yes Role=Content 360 × 70 on white. The label drops to Primary/Label/Fine and the cell text becomes Secondary/Light/Caption — the one place the row uses BarkAda.
Disabled Yes Yes State=Disabled Every text layer dims to text/color-text-disabled. The background does not change — a disabled Content row stays white and a disabled Header row stays #F6F9FD. Both primitives carry their own disabled version, so the row swaps to them.
Pressed N/A N/A Not built Not needed — rows are display-only and carry no tap target.
Selected N/A N/A Not built Not needed — no row selection in this pattern.
Issues
  • One name for one setting, across all three components. v2.2: Table Cell called its setting Type while Table Row and Table Label called the same thing Role. All three say Role now. It matters beyond tidiness: Role=Header is what becomes an accessibility heading on both platforms, so the name is describing a real role. C2 · Variant & Property Naming
  • Every text layer is named after the property that fills it. v2.2: the cell's text layer was #description — a name copied from Table Label, promising a second line the cell never had. It is #text now, filled by a new Text property. Table Label's #label became #title to match its Title property. Property name and layer name agree everywhere. C1 · Layer Structure & Naming
  • The empty version is actually empty. v2.2: Role=Empty on Table Cell held a stray blue checkmark — a copy of another component parked in the set, drawing an unflattened shape on an unbound #025ae9. Both primitives now draw nothing for Role=Empty, which is what a column with no value for that row should look like. C6 · Asset & Icon Quality
  • The disabled look lives in the primitives, not in each copy. v2.2: Table Label and Table Cell each publish their own Disabled version, so a disabled row swaps to them. Before, every copy had its colours painted over by hand — which meant the disabled look did not travel if either primitive were used anywhere else. C5 · Interaction State Coverage
  • Every colour is bound to a named system colour. v2.2: checked against Figma's selection colours. The set paints six colours and the panel lists exactly six names — bg/color-bg, bg/color-bg-main, border/color-border-weak, border/color-border, text/color-text and text/color-text-disabled. The icon placeholder was documented as a hardcoded #C2C6CF; it is border/color-border at #D7E0EF. Table Label's description sits on text/color-text-weaker. C3 · Token Coverage
  • One row replaces three components. v2.0: rebuilt on node 5734:37611 as Table Row. Table Label and Table Cell are now placed as real instances inside slots instead of being published and ignored. 3 components and 14 variants become 1 component and 4. C1 · Layer Structure & Naming
  • no. of columns is gone. v2.0: column count is no longer a property. The Columns Slot holds however many Table Cell instances you drop in, which is what data-driven was always meant to mean. The period in the property name goes with it. C2 · Variant & Property Naming
  • The icon placeholder is a slot. v2.0: every Table Cell carries a 24 × 24 Asset Slot holding a swappable Placeholder instance, and Table Label has one too. The hardcoded #C2C6CF circle and the icon=yes/no boolean are both gone. C6 · Asset & Icon Quality
  • The row maps to native primitives. v2.0: a slot-based row is just an HStack / Row of cells, so it no longer depends on a platform Table primitive that phones don't have. Rows stay 360 wide for now because they sit inside a fixed component group; they move to fill when the screens that need it are reworked. C4 · Native Mappability
  • The label description is readable now. v2.1: #description moved from 8 / 12 to the 10px token used elsewhere in the system — BarkAda SemiBold 10 / 15. 10px is the accepted middle ground; moving to the 12px token stays open as a recommendation. C3 · Token Coverage
  • The disabled state dims every text layer. v2.1: #description now takes #C2CFE5 in State=Disabled (5761:37774), matching #label and the cells. A disabled row dims as one thing. C5 · Interaction State Coverage
  • Slot layer names follow one pattern. v2.1: all three slots renamed to the ⤷ …Slot form — ⤷ ColumnSlot on the row, and ⤷ AssetSlot on both Table Label and Table Cell. Table Scheduling uses the same pattern. C1 · Layer Structure & Naming
  • Interaction states are intentionally minimal. v2.0: reviewed and settled. Rows are display-only — no tap target, no selection — so pressed, focused, and selected are not needed. State=Disabled is built for rows showing unavailable data. C5 · Interaction State Coverage
Recommendations
  • Move #description from the 10px token to the 12px one. 10px was chosen as a middle ground and matches other components today, so this isn't urgent. 12px is the target — it's the floor the rest of the system's body text sits on, and small supporting text is exactly where legibility is thinnest. A11y
  • Write the slot naming convention into the guidelines. The Table family now uses ⤷ …Slot — ⤷ ColumnSlot, ⤷ AssetSlot, ⤷ CurrencySlot. The carousel components use ⤷ without the suffix (⤷ LeadingIcon, ⤷ Violator) and no prefix at all on top-level slots (Banner, Background). Both are internally consistent; the library needs one rule so new slot-based components stop diverging. Docs
  • Document that rows are display-only. There is no tap target and no selection by design. Write it down so native developers wrap the row in a non-interactive container rather than inferring a missing state. Docs
  • Revisit the fixed 360 width when screens are reworked. Rows are 360 wide because they currently sit inside a fixed component group. The intent is to move to fill. Worth tracking so it doesn't get forgotten once those screens are edited. Composition
  • Introduce a shared label/value token set with Inline Text. v2.2.1: Closed — there is no longer a set to share. This row already draws from the system-wide text/* colours, and the inline-text/color/* tokens the recommendation wanted to converge on have been retired from the file. What is left belongs to Inline Text's own page, which still documents them. Token
  • Give disabled rows their own background. v2.2: No change needed — the row already does what the second half of this asked for. The recommendation said disabled reuses the Header #F6F9FD, so a disabled row and a header row read alike in a stack. Exporting the set shows otherwise: a disabled Content row is still white and a disabled Header row is still #F6F9FD. Disabled changes text colour and nothing else. The claim described our own preview, which had an invented background rule, rather than the component. Token
  • Audit the colour token bindings. v2.2: Applied — done from Figma's selection-colours panel, which the old tooling could not read. Six colours, six names, no gaps. Details in the Resolved tab. Token
  • Collapse the three-component family into one row primitive. v2.0: Applied — Table Row with a Columns Slot and a Role variant, exactly as proposed. 3 components and 14 variants become 1 component and 4. Property
  • Rename no. of columns to an integer, or drop it entirely. v2.0: Applied — dropped. Column count comes from the number of Table Cell instances in the slot. Rename
  • Replace the header icon placeholder with a named slot. v2.0: Applied — Asset Slot on both Table Label and Table Cell, and the icon=yes/no boolean is gone. Slot
  • Add row interaction states. v2.0: Applied as far as it goes — State=Disabled shipped. Pressed and selected were reviewed and dropped: rows are display-only. State
  • Reconsider whether Table belongs in a mobile-first DS. v2.0: Settled — Table stays, scoped to genuine multi-column data and rebuilt as a data-driven row. That was option (b) of the two paths originally offered. Family
  • Raise the label description above 8px. v2.1: Applied — #description is now BarkAda SemiBold 10 / 15, on the shared 10px token. The move to 12px stays open as a recommendation. A11y
  • Dim #description in the disabled state. v2.1: Applied — it takes #C2CFE5 alongside the label and cells. Token
  • Settle one slot naming convention for the component. v2.1: Applied — ⤷ ColumnSlot and ⤷ AssetSlot throughout. Settling it library-wide stays open as a Docs item. Rename
Roles
Header
Header
Title
Title
Title
Properties
State
hasBorder
Properties
Role Header
State Default
⤷ ColumnSlot 3 × Table Cell
hasBorder True
Colors
Background #F6F9FD
Border #E5EBF4
Label #0A2757
Cell text #0A2757
Cell asset #D7E0EF
Typography
#label Primary/Label/Small
Table Cell #text Primary/Label/Small
Layout
Height Hug · 68
Width 360
Radius 0
Padding H 24
Padding V 8 / 12
Gap 16
Alignment Left · Center
Colors by Role

Every paint in the set is bound. The six tokens here are exactly the six the Figma selection-colors panel lists for node 5734:37611 — nothing is off-token, including the cell asset placeholder.

Role Element Token Value
Header Background bg/color-bg #F6F9FD
— Border border/color-border-weak #E5EBF4
— Label text/color-text #0A2757
— Cell text text/color-text #0A2757
— Cell asset border/color-border #D7E0EF
Content Background bg/color-bg-main #FFFFFF
— Border border/color-border-weak #E5EBF4
— Label text/color-text #0A2757
— Cell text text/color-text #0A2757
— Cell asset border/color-border #D7E0EF
Disabled Label text/color-text-disabled #C2CFE5
— Cell text text/color-text-disabled #C2CFE5
— Background — unchanged –
— Cell asset — unchanged –
Content
Row Title
Data
Data
Data
Properties
State
hasBorder
Properties
Role Content
State Default
⤷ ColumnSlot 3 × Table Cell
hasBorder True
Colors
Background #FFFFFF
Border #E5EBF4
Label #0A2757
Cell text #0A2757
Cell asset #D7E0EF
Typography
#label Primary/Label/Fine
Table Cell #text Secondary/Light/Caption
Layout
Height Hug · 70
Width 360
Radius 0
Padding H 24
Padding V 12
Gap 16
Alignment Left · Center
Installation Planned API

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:table:1.0.0")

Import

import EastBlueDS
import com.eastblue.ds.table.*
Property Mapping

Table Row publishes four properties — Role, State, ⤷ ColumnSlot and hasBorder. The rows beneath them map the two nested primitives, Table Label and Table Cell, which ship inside every row and have no page of their own. Both carry their own Role / Type and State variants, driven by the row rather than set independently, so neither takes a parameter here. All three components name the axis Role and expose their text as properties. Both primitives also publish Role=Empty — a variant that draws nothing, for a column that has no value on this row. It takes no parameter of its own: natively it is an absent Column entry, or one whose text is empty.

Figma PropertySwiftUICompose
Role — Header, Content role: .header / .content role = EBTableRowRole.Header / Content
State — Default, Disabled .disabled(true) enabled = false
⤷ ColumnSlot (slot) — Table Cell instances columns: [Column] columns: List<Column>
hasBorder — true, false showsDivider: Bool = true showsDivider: Boolean = true
Table Label → Title (text) title: String title: String
Table Label → Description (text) description: String? description: String? = null
Table Label → hasDescription — true, false description == nil — nil hides the line description == null
Table Label → hasAsset — true, false leadingIcon == nil — nil hides the slot leadingIcon == null
Table Label → ⤷ AssetSlot (slot) leadingIcon: AnyView? leadingIcon: @Composable (() -> Unit)? = null
Table Cell → Role — Header, Content, Transaction, Empty Column.role: .header / .content / .transaction role = EBTableCellRole.Header / Content / Transaction
Table Cell → Text (text) Column.text: String Column.text: String
Table Cell → hasAsset — true, false Column.asset == nil — nil hides the slot Column.asset == null
Table Cell → ⤷ AssetSlot (slot) Column.asset: AnyView? Column.asset: @Composable (() -> Unit)?
SwiftUI
ios/Components/Table/EBTableRow.swift
Jetpack Compose
android/components/table/EBTableRow.kt
Usage Snippets Planned API
Header
// One Header row at the top of the table.
EBTableRow(
    role: .header,
    label: "Header",
    columns: [
        Column("Title", asset: Image("wallet")),
        Column("Title", asset: Image("bank")),
        Column("Title", asset: Image("card"))
    ]
)
// One Header row at the top of the table.
EBTableRow(
    role = EBTableRowRole.Header,
    label = "Header",
    columns = listOf(
        Column("Title", asset = { Icon("wallet") }),
        Column("Title", asset = { Icon("bank") }),
        Column("Title", asset = { Icon("card") })
    )
)
Content
// Column count must match the Header row above it.
EBTableRow(
    role: .content,
    label: "Row Title",
    columns: [
        Column("Data"),
        Column("Data"),
        Column("Data")
    ]
)
.disabled(isUnavailable)
// Column count must match the Header row above it.
EBTableRow(
    role = EBTableRowRole.Content,
    label = "Row Title",
    columns = listOf(
        Column("Data"),
        Column("Data"),
        Column("Data")
    ),
    enabled = !isUnavailable
)
Accessibility
RequirementiOSAndroid
Tabular semantics On iPad/macOS use Table; on iPhone use accessibilityElement(children: .combine) per row so each reads as "Label, value, value". Wrap in Modifier.semantics { collectionInfo = CollectionInfo(rowCount, columnCount) }.
Header vs content row Use .accessibilityAddTraits(.isHeader) on header rows. Use Modifier.semantics { heading() } on header rows.
Column headers without visible text If icon-only header columns exist, provide .accessibilityLabel. Set contentDescription on the icon slot.
Row-level tap If rows become tappable, wrap row in Button with .accessibilityHint. Wrap in Modifier.clickable with role = Role.Button.
Usage Guidelines

Do

Give every row in one table the same number of Table Cell instances.

Don't

Don't put 3 cells in the Header and 4 in the rows beneath it — the ⤷ ColumnSlot divides its 176px evenly and nothing aligns the two.

Do

Use Role=Header once, at the top of the table.

Don't

Don't reach for Header mid-table to emphasise a row — it reads as a second table starting.

Do

Put the row's subject in Table Label and its data in the cells.

Don't

Don't fold a fourth data point into the label to save a column — the label is 120px and fixed.

Do

Use Inline Text when a row carries a single value.

Don't

Don't use Table Row for a two-column label/value list — three cells share 176px, so each gets about 59px.

Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Ready One Table Row composing Table Label and Table Cell through slots, all named ⤷ …Slot. The cell's text layer is #text — it was #description, a name copied from Table Label that promised a second line the cell never had.
C2 Variant & Property Naming Ready All three components name the axis Role with Title Case values, and both primitives expose their text as properties — Title and Description on Table Label, Text on Table Cell, each matching its layer name. Role=Empty draws nothing on both, as the name says. The no. of columns property and its period are long gone.
C3 Token Coverage Ready Verified against the selection-colors panel: the set paints six colours and the panel lists exactly six tokens — bg/color-bg, bg/color-bg-main, border/color-border-weak, border/color-border, text/color-text, text/color-text-disabled. Table Label's #description is on text/color-text-weaker.
C4 Native Mappability Ready A slot-based row is an HStack / Row of cells — no platform Table primitive needed. Fixed 360 width is deliberate for now and moves to fill when the screens are reworked.
C5 Interaction State Coverage Ready Display-only by design, so pressed and selected are not needed. State=Disabled dims every text layer to text/color-text-disabled and leaves the row's own background alone. Both primitives publish their own Disabled variant, so the row swaps to them rather than overriding fills per instance.
C6 Asset & Icon Quality Ready 24 × 24 ⤷ AssetSlot on both primitives, holding a swappable Placeholder instance drawn on border/color-border.
Variants Inventory (4 total)

Role (2) × State (2) = 4 published variants. hasBorder sits on top of them without adding any, so Table Row has 8 combinations behind 4 versions. Column count is not a version either — the ⤷ ColumnSlot takes however many Table Cell instances you drop in — three in every published variant, each one Role=Header or Role=Content to match the row. This replaces the old three-component family, which published 9 + 3 + 2 variants between them.

RoleStateDimensionsBackgroundNode
HeaderDefault360 × 68#F6F9FD5734:37630
HeaderDisabled360 × 68#F6F9FD5761:37748
ContentDefault360 × 70#FFFFFF5734:37657
ContentDisabled360 × 70#FFFFFF5761:37773
2.2.1 — September 2026Patch
Corrections after shared facts moved · node 5734:37611
Table Cell gained a fourth role. Property Mapping listed Header, Content, Empty; the primitive now also publishes Role=Transaction (nodes 9567:96672 and 9567:96676), added during the Table Transaction review so that row could stop overriding the cell's text style. Nothing about Table Row changed — a component it references did, which is why its own checks all passed while the page went stale.
Docs
The Inline Text token recommendation is closed. It asked this row's label and value colours to converge with Inline Text's own set. Both sides moved instead: this row is on the system-wide text/* colours, and the inline-text/color/* tokens no longer exist. Nothing to reconcile from here.
Token
2.2.0 — September 2026Minor
Primitives aligned; Style, Code and Overview rebuilt to the content guides · node 5734:37611
One name for one setting, across all three components. Table Cell called its setting Type while Table Row and Table Label called the same thing Role. All three say Role now. It matters beyond tidiness: Role=Header is what becomes an accessibility heading on both platforms.
C2 Resolved
Every text layer is named after the property that fills it. The cell's text layer was #description, a name copied from Table Label that promised a second line the cell never had — it is #text now, filled by a new Text property. Table Label's #label became #title to match its Title.
C1 Resolved
The empty version is actually empty. Role=Empty on Table Cell held a stray blue checkmark — a copy of another component parked in the set, drawing an unflattened shape on an unbound #025ae9. Both primitives now draw nothing for Role=Empty.
C6 Resolved
The disabled look lives in the primitives, not in each copy. Table Label and Table Cell each publish their own Disabled version, so a disabled row swaps to them. Before, every copy had its colours painted over by hand, which meant the disabled look did not travel if either primitive were used elsewhere.
C5 Resolved
Every colour is bound to a named system colour. Checked against Figma's selection-colours panel, which the earlier tooling could not read — the set paints six colours and the panel lists exactly six names. The icon placeholder had been documented as a hardcoded #C2C6CF; it is border/color-border at #D7E0EF. This closes the standing recommendation to audit the bindings.
C3 Resolved
A recommendation turned out to describe our own preview. "Give disabled rows their own background" said disabled reuses the Header #F6F9FD, so the two read alike in a stack. Exporting the set shows otherwise: a disabled Content row is still white. The claim came from an invented CSS rule in the documentation preview, not from the component. Closed with no Figma change.
Token
The preview was drawing three things Figma does not. The asset placeholder was #C8D3E5 against a real #D7E0EF; disabled rows were given a #F6F9FD background the component never had; and the preview root drew in the documentation face rather than Proxima Soft. Corrected against the SVG export of the whole set.
Docs
Layout was documented from numbers that were never read. Heights said 37 / 65 / 56 where Figma has Hug · 68 and Hug · 70, and the four spec sections carried font specs instead of text style names. Rebuilt from the Auto layout panels and the resolved styles: Primary/Label/Small, Primary/Label/Fine and Secondary/Light/Caption.
Docs
Code tab rebuilt to the content guide. Property Mapping regrouped into prose rows, one per setting, and grown from 10 to 13 to cover both primitives; the missing Import block added; Usage Snippets split into one per Role; four usage guidelines written where there were none.
Docs
Native readiness moved to Ready. C1 through C6 all pass now. C7 stays Not Mapped and is excluded from the badge by rule — the native library does not exist yet.
Docs
Two versions of history were missing. The v2.0 and v2.1 work shipped together in 7f5cafa (August 2026) and never got changelog entries, while the Overview tab referenced both throughout. Both are written above, reconstructed from that commit and from the Overview's own version-tagged records.
Docs
2.1.0 — August 2026Minor
Follow-up pass on the rebuild · node 5734:37611
The supporting line got bigger. #description moved to the shared 10px text style, up from 8px. 12px is still the target and stays as a recommendation — it is the floor the rest of the system's body text sits on.
C3 Resolved
Disabled now dims every text layer. The description had been left at full strength while the label and cells dimmed, so a disabled row read as half-available. All of it takes text/color-text-disabled.
C5 Resolved
Swappable areas settled on one naming pattern. All three took the ⤷ …Slot form — ⤷ ColumnSlot on the row, ⤷ AssetSlot on both primitives. Settling it library-wide stays open as a documentation item.
C1 Resolved
2.0.0 — August 2026Major
2026 Working File · rebuilt as one composable row · node 5734:37611
Three components became one. Table, Table - Item and Table - Label published 9 + 3 + 2 versions between them, and only Table was ever placed on a screen. Table Row replaces all three with 4 versions, composing Table Label and Table Cell as real copies inside swappable areas rather than redrawing them.
C1 Resolved
The column-count setting is gone. no. of columns was a text setting with a period in its name, and it fixed the number of columns at build time. Column count is now however many Table Cell copies you drop into ⤷ ColumnSlot.
C2 Resolved
The header icon became a swappable area. It was a hardcoded #C2C6CF circle behind an icon=yes/no toggle. Both Table Label and Table Cell now carry a 24 × 24 ⤷ AssetSlot holding a swappable Placeholder.
C6 Resolved
The row maps to native primitives after all. The initial assessment doubted a table belonged in a mobile design system. A slot-based row is a stack of cells on both platforms — no platform Table needed — so Table stays, scoped to genuine multi-column data.
C4 Resolved
Disabled shipped; pressed and selected were dropped on purpose. Rows are display-only and carry no tap target, so those two states have nothing to show. State=Disabled was added for rows showing unavailable data.
C5 Resolved
1.0.0 — April 2026Major
Initial Assessment · node 47:326260
Family assessed — 3 published components (Table / Table - Item / Table - Label) with 9 + 3 + 2 variants. Only Table is consumed in screens. Documented
Initial
Three orphan components — Table - Item and Table - Label exist but are never placed directly. Recommend collapsing into one data-driven row. Open
C1 Open
no. of columns naming — String enum with a period in the property name. Open
C2 Open
No native mobile primitive — Mobile tables are almost always stacks of label/value pairs (Inline Text) — reconsider whether Table belongs in the DS at all. Open
C4 Open
No interaction states — No hover / pressed / selected / disabled state variants. Open
C5 Open
Header icon is a raw placeholder — Hardcoded #C2C6CF circle with no slot or instance swap. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open