A service tile — asset slot, label and optional description — used in the home Menu Grid and customizable shortcut surfaces, with an optional New badge and Add or Remove edit overlays.
4692:21582 in the 2026 Working File, and every finding from the original assessment has landed. The State=Disbaled typo is corrected, Type is split into Badge and Action, Inactive and Disabled render differently, a Pressed state ships across all eight combinations, description is a real Figma Slot, and layer naming follows §3 and §7 throughout with both slots kebab-cased per §4. Three things are settled as decisions rather than fixes: badges are suppressed during edit mode, so 32 is the complete matrix rather than 32 of 48; the Container frame in the Horizontal variants is what creates the side-by-side layout and is correctly absent from Vertical; and the 12×12 overlay tap target is specified at 44×44pt / 48×48dp with its own accessibility label. The Add and Remove glyph construction is parked as accepted debt in the shared icon source. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.Used inside the home Menu Grid (the 4×N icon-and-label grid above the bills/transfer shortcuts) and inside the "Customize your home" reordering screen, where Add/Remove overlays appear over the icon during edit mode. Vertical orientation is the dominant home-grid usage; horizontal is reserved for list-style surfaces (e.g. the recent-services drawer).
Type axis that used to fuse badge with action is now two independent properties.Preamble → Label → Description, and both slots kebab-cased per §4 as Asset-Slot and Description-Slot. The two structural differences are deliberate and recorded: Container exists only in Horizontal because only Horizontal needs a row frame, and the matrix stops at 32 because badges are suppressed while an edit overlay is present.Asset Slot and Description Slot are both real Figma Slots, so a consumer supplies their own artwork and can show a label without inheriting placeholder description copy.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | Label at #072592 on a #F6F9FD asset slot. |
| Pressed | Yes | Yes | State=Pressed | Label deepens to #071969; asset slot darkens to #EEF2F9. Released on finger-up. |
| Inactive | Yes | Yes | State=Inactive | Label at #6780A9. Visible and still tappable — the service exists but is temporarily unavailable. |
| Disabled | N/A | N/A | State=Disabled | Label at #C2CFE5, the system disabled foreground. Not tappable. |
| New badge | N/A | N/A | Badge=New | A 29×12 Badge instance over the asset slot. Decay rule — first tap, or N days — is still unspecified. |
| Add / Remove overlay | Yes | Yes | Action=Add | Remove | A 12×12 control in the asset slot corner, a separate tap target from the tile. Hit area is not annotated. |
-
State=Disbaledtypo corrected. v2.0: Rebuilt on node4692:21582in the 2026 Working File. All 32 variants readState=Disabled. The misspelling would have propagated into Code Connect prop names and from there into generated native constants, where it is far more expensive to unwind. (C2 · Rename) C2 · Variant & Property Naming -
Typesplit intoBadgeandAction. v2.0: The axis that bundled a content flag with an overlay action is gone. The schema is nowState×Orientation×Badge = None | New×Action = None | Add | Remove, all PascalCase per §1 with Title Case values per §5. Each axis names one thing. (C2 · Property) C2 · Variant & Property Naming - Inactive and Disabled now render differently. v2.0: The two states had been visually identical. They now separate cleanly —
Inactiveholds the label at#6780A9, a readable muted blue for a service that is visible but temporarily unavailable, whileDisableddrops to#C2CFE5, the system disabled foreground. A user can tell which situation they are looking at. (C5 · State) C5 · Interaction State Coverage - Pressed state added. v2.0:
State=Pressedships across all 8 orientation/badge/action combinations — the label deepens to#071969and the asset slot darkens#F6F9FD→#EEF2F9. Tiles are the primary tap target on the home grid, so touch-down feedback was the most-felt gap in the previous assessment. (C5 · State) C5 · Interaction State Coverage - Description promoted to a real content slot. v2.0:
Description Slotis a genuineSLOTnode rather than a baked text layer with placeholder copy. Home-grid usages that only need a label no longer carry description content they have to remember to clear.Asset Slotis likewise a real Slot. (C4 · Slot) C4 · Native Mappability - Layer naming pass landed, and both slots are kebab-case. v2.1: Verified on the live node.
#preamble→Preamble,#label→Label, and the wrapping framespreamble/content/border→Preamble/Content/Border. The two slots also moved onto the §4 convention asAsset-SlotandDescription-Slot— kebab-case for slots, PascalCase for frames, which is exactly the distinction §4 draws. Text layers now sit in the §7 hierarchy asPreamble → Label → Description. One layer was missed and is tracked below. (C1 · Rename) C1 · Layer Structure & Naming - Rename sweep completed —
#description→Description. v2.2: Verified on the live node (4692:21589). Every text layer now reads without the legacy sigil, in the §7 hierarchy asPreamble → Label → Description, withAsset-SlotandDescription-Slotkebab-cased per §4. No layer in the set carries the old convention. (C1 · Rename) C1 · Layer Structure & Naming - Badges are suppressed in edit mode — matrix confirmed complete at 32. v2.2: Closed by owner decision.
BadgeandActionare deliberately mutually exclusive: while a user is customising their home screen, a red New badge competes with the Add/Remove affordance for the same corner of the tile and for the same attention, so the badge is suppressed for the duration of edit mode. The 16Badge=New×Action=Add|Removecombinations are therefore unsupported rather than unbuilt, and 32 is the complete matrix. Native implementations should hide the badge whenever an edit overlay is present rather than stacking them. Recorded so a consumer meeting the gap in the variant picker finds a rule instead of assuming an omission. (C2 · Property) C2 · Variant & Property Naming -
Containerin the Horizontal variants confirmed intentional. v2.2: The asymmetry is structural, not an oversight. Horizontal lays the asset and the text side by side, which needs its own auto-layout frame to hold that row; Vertical stacks the same children directly and needs no wrapper. Adding aContainerto Vertical would introduce a frame that does nothing, and removing it from Horizontal would break the layout it exists to create. Native implementations should readOrientationas the layout switch — a row versus a column — rather than expecting one tree shape across both. (C1) C1 · Layer Structure & Naming - Overlay tap-area specified. v2.2: The Add and Remove overlays render at 12×12 in the corner of
Asset-Slot, and each is a separate tap target from the tile beneath it — tapping the tile opens the service, tapping the overlay adds or removes it. The glyph stays 12×12 visually; the touch target must be expanded around it to 44×44pt on iOS and 48×48dp on Android, centred on the glyph and extending beyond the tile bounds where necessary. On iOS use a transparent.contentShape(Rectangle())sized to the target rather than growing the visible circle; on Android set the minimum touch target on the clickable modifier rather than padding the icon. The overlay must sit above the tile in hit-test order so its region wins, and it needs its own accessibility label — “Add {service}” / “Remove {service}” — separate from the tile’s. Without this an implementer either ships an untappable control or wraps the whole tile, and wrapping the tile breaks edit mode. This spec lives here rather than as a Figma annotation: the review has read-only Figma access, so the note itself still needs adding in the file. (C5 · A11y) C5 · Interaction State Coverage - Add and Remove glyph construction deferred. v2.2: Parked by owner decision rather than fixed. The overlays’ source component is built from
Ellipse 53plusRectangle 2620andRectangle 2621— two white bars over a#12AF80circle — rather than a vector glyph on the icon grid, so the default shape names carry no meaning and the plus will not scale or recolor like the rest of the icon set. It sits in the shared icon source rather than in Service Item, so fixing it is the icon owner’s call and affects every consumer equally. Recorded as a known, accepted debt rather than closed as correct. (C6 · Asset) C6 · Asset & Icon Quality
| Role | Element | Token | Value |
|---|---|---|---|
| Default | Asset placeholder | — | #F6F9FD |
| Pressed | Asset placeholder | — | #EEF2F9 |
| Default | Label | — | #072592 |
| Inactive | Label | — | #6780A9 |
| Pressed | Label | — | #071969 |
| Disabled | Label | — | #C2CFE5 |
| Default | Preamble | — | #90A8D0 |
| Default | Description | — | #445C85 |
| Inactive | Description | — | #90A8D0 |
| Pressed | Description | — | #0A2757 |
| Disabled | Description | — | #C2CFE5 |
| Default | Border | — | #D7E0EF |
| Default | New badge background | — | #D61B2C |
| Default | New badge label | — | #FFFFFF |
| Default | Add mark | — | #12AF80 |
| Default | Remove mark | — | #D61B2C |
| Default | Mark glyph | — | #FFFFFF |
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:service-item:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.serviceitem.*
State, Orientation, Badge and Action are the set’s four variant properties. Asset-Slot and Description-Slot are Figma Slots; Label and Preamble are text layers read from 4692:21583. The Preamble, Description-Slot and Border layers are hidden in every variant — the booleans that show them are not readable with the plugin.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| State — Default, Inactive, Pressed, Disabled | .ebInactive(true) · .disabled(true) · Pressed is the platform touch-down | inactive = true · enabled = false · Pressed from interactionSource |
| Orientation — Vertical, Horizontal | .ebOrientation(.vertical / .horizontal) | orientation = EBServiceItemOrientation.Vertical / Horizontal |
| Badge — None, New | .ebBadge(.new) | badge = EBServiceItemBadge.New |
| Action — None, Add, Remove | .ebAction(.add / .remove) { } | action = EBServiceItemAction.Add / Remove { } |
| Asset-Slot (slot) | asset: Image | asset = { Icon(…) } |
| Description-Slot (slot, hidden by default) | .ebDescription("…") | description = "…" |
| Label (text) | EBServiceItem("Label", …) | label = "…" |
| Preamble (text, hidden by default) | .ebPreamble("…") | preamble = "…" |
EBServiceItem("Label", asset: Image("service"))
.ebOrientation(.vertical) EBServiceItem(
label = "Label",
asset = { Icon(…) },
orientation = EBServiceItemOrientation.Vertical
) EBServiceItem("Label", asset: Image("service"))
.ebOrientation(.horizontal) EBServiceItem(
label = "Label",
asset = { Icon(…) },
orientation = EBServiceItemOrientation.Horizontal
) | Requirement | iOS | Android |
|---|---|---|
| Tile as a button | Whole tile in Button; accessibilityLabel is the label plus "new" when the badge shows. | Modifier.clickable { onTap() }.semantics(mergeDescendants = true) on the tile. |
| Add / Remove mark | A second Button with accessibilityLabel("Add to favourites") / "Remove"; the 12 pt mark needs a 44 pt hit area. | Separate IconButton with contentDescription; 48 dp touch target around the 12 dp mark. |
| Inactive vs disabled | Inactive keeps the button trait and adds .accessibilityValue("unavailable"); Disabled uses .disabled(true). | Inactive stays clickable with stateDescription = "unavailable"; Disabled sets enabled = false. |
| Minimum target | 64 × 72 pt tile ≥ 44 pt. | 64 × 72 dp tile ≥ 48 dp. |
Do
Use Vertical in the service grid, Horizontal in a single-column list.
Don't
Don’t mix orientations in one grid.
Do
Show Badge New for the first launch of a service only.
Don't
Don’t pair New with an Add or Remove mark — the set does not build it.
Do
Use Add and Remove only inside edit mode of the favourites grid.
Don't
Don’t leave a Remove mark on a tile outside edit mode.
Do
Keep the label to one line; the tile is 64 px wide.
Don't
Don’t wrap a two-word service name — shorten it.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Needs Refinement | Preamble, Asset-Slot, Content › Label / Description-Slot, Border — read on 4692:21583. One variant (4692:21601) still names its preamble text #preamble; the Preamble, Description and Border layers are hidden in every variant. |
| C2 | Variant & Property Naming | Ready | State, Orientation, Badge, Action with Title Case values; the Disbaled typo is gone. |
| C3 | Token Coverage | Ready | Label → Primary/Label/Fine, Preamble → Secondary/Heavy/Fine, Description → Secondary/Bold/Small Caption. Colour variable bindings are not readable with the plugin — confirm in Dev Mode. |
| C4 | Native Mappability | Ready | Four enums, two slots, two strings — maps 1:1 onto EBServiceItem. |
| C5 | Interaction State Coverage | Ready | Default, Inactive, Pressed and Disabled built for both orientations, every badge and every action. |
| C6 | Asset & Icon Quality | Ready | Asset-Slot is a Slot; the New badge is a Badge instance; Add and Remove are 12 × 12 vector instances. |
4 State × 2 Orientation × (Badge None, Badge New, Action Add, Action Remove) = 32 variants; New never pairs with an Action
| Orientation | State × Badge / Action | Count |
|---|---|---|
| Vertical | Default, Inactive, Pressed, Disabled × None, New, Add, Remove | 16 |
| Horizontal | Default, Inactive, Pressed, Disabled × None, New, Add, Remove | 16 |
View full State × Orientation × Badge × Action breakdown (32 rows)
| State | Orientation | Badge | Action | Node | Dimensions |
|---|---|---|---|---|---|
| Default | Vertical | None | None | 4692:21583 | 64 × 72 |
| Default | Vertical | New | None | 4692:21591 | 64 × 72 |
| Default | Vertical | None | Add | 4692:21601 | 64 × 72 |
| Default | Vertical | None | Remove | 4692:21610 | 64 × 72 |
| Inactive | Vertical | None | None | 4692:21619 | 64 × 72 |
| Inactive | Vertical | New | None | 4692:21627 | 64 × 72 |
| Inactive | Vertical | None | Add | 4692:21637 | 64 × 72 |
| Inactive | Vertical | None | Remove | 4692:21646 | 64 × 72 |
| Pressed | Vertical | None | None | 4703:18264 | 64 × 72 |
| Pressed | Vertical | New | None | 4703:18387 | 64 × 72 |
| Pressed | Vertical | None | Add | 4711:18442 | 64 × 72 |
| Pressed | Vertical | None | Remove | 4711:18624 | 64 × 72 |
| Disabled | Vertical | None | None | 4692:21775 | 64 × 72 |
| Disabled | Vertical | New | None | 4692:21783 | 64 × 72 |
| Disabled | Vertical | None | Add | 4692:21793 | 64 × 72 |
| Disabled | Vertical | None | Remove | 4692:21802 | 64 × 72 |
| Default | Horizontal | None | None | 4692:21655 | 120 × 64 |
| Default | Horizontal | New | None | 4692:21664 | 120 × 64 |
| Default | Horizontal | None | Add | 4692:21675 | 120 × 64 |
| Default | Horizontal | None | Remove | 4692:21685 | 120 × 64 |
| Inactive | Horizontal | None | None | 4692:21695 | 120 × 64 |
| Inactive | Horizontal | New | None | 4692:21704 | 120 × 64 |
| Inactive | Horizontal | None | Add | 4692:21715 | 120 × 64 |
| Inactive | Horizontal | None | Remove | 4692:21725 | 120 × 64 |
| Pressed | Horizontal | None | None | 4711:18705 | 120 × 64 |
| Pressed | Horizontal | New | None | 4711:18714 | 120 × 64 |
| Pressed | Horizontal | None | Add | 4711:18726 | 120 × 64 |
| Pressed | Horizontal | None | Remove | 4711:18739 | 120 × 64 |
| Disabled | Horizontal | None | None | 4692:21735 | 120 × 64 |
| Disabled | Horizontal | New | None | 4692:21754 | 120 × 64 |
| Disabled | Horizontal | None | Add | 4692:21744 | 120 × 64 |
| Disabled | Horizontal | None | Remove | 4692:21765 | 120 × 64 |
Orientation — Vertical, Horizontal — with State, Badge and Action as panel controls; geometry, colours and the three text styles read off 4692:21583, 4692:21655 and the State variants against the exports. Padding, gap, alignment and colour tokens are not readable with the plugin and stay —.Preamble, Description-Slot and Border are hidden in every variant, and 4692:21601 names its preamble text #preamble where its siblings say Preamble. Filed from the scorecard for the Component Review.State=Disbaled typo corrected; Type split into Badge and Action; Inactive and Disabled now render differently; Pressed state added; Description promoted to a real content slot; Layer naming pass landed, and both slots are kebab-case; Rename sweep completed — #description → Description; Badges are suppressed in edit mode — matrix confirmed complete at 32; Container in the Horizontal variants confirmed intentional; Overlay tap-area specified; Add and Remove glyph construction deferred.Type × State × Orientation. Powers the home Menu Grid and the customize-home edit mode. Documentedbadge + action axes, distinguish Inactive from Disabled. Openbadge + action. Open