A pill-shaped selector carrying a label and an optional chosen value, with optional leading and trailing icons and a slot for an attached dropdown.
5595:39596 in the 2026 Working File, and every recommendation from the previous assessment has landed. Two overlapping components are consolidated into one 24-variant set — hasValue × State × hasLeadingIcon × hasTrailingIcon — with a property schema following §1, §2 and §5, identical layer naming across all 24 variants on the §3 vocabulary, Pressed and Disabled states with properly muted disabled text, the offset frames and colored _space_* spacers removed, and dropdown group replaced by a real Dropdown-Slot. Scope is confirmed dropdown-only, so Pressed means finger-down rather than doubling as a selection; the leading Placeholder is a deliberate swap target; and the 80% pressed-label opacity is the same intentional treatment confirmed elsewhere in the system. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.Contexts are illustrative. Final screens will reference actual GCash patterns.
Dropdown-Slot attaches external content without the chip needing to know what it is.hasValue × State × hasLeadingIcon × hasTrailingIcon — and all 24 variants now carry identical layer names on the §3 vocabulary, so each text layer exposes as a single property. The Disabled state mutes its text in line with Counter and Search Field rather than diverging.Dropdown-Slot is a real Figma Slot on the twelve variants that signal a dropdown, ContentRow composes label and value independently, and the leading position takes a swappable instance.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Active / Selected | Yes | Yes | Filter · type=primary | Filled blue background, white label. Used when a filter is applied. |
| Inactive (light) | Yes | Yes | Filter · type=light | Light gray pill, gray label. Used for unapplied filters or tag readouts. |
| Inactive (outline) | Yes | Yes | Filter · type=outline | White pill, gray border, gray label. |
| Dropdown trigger | Yes | Yes | Filter w/ Dropdown · default | Light style with chevron. Used for sort/filter pickers. |
| Pressed / Disabled / Error | N/A | N/A | — | Not defined in Figma. C5 |
- Consolidated into one component on the Working File. v2.0: Rebuilt on node
5595:39596. What were two overlapping chip components are now a single 24-variant set —hasValue(2) ×State(3) ×hasLeadingIcon(2) ×hasTrailingIcon(2) — with every combination present and no gaps. This is the rename-and-consolidate recommendation applied. (C2 · Family) C2 · Variant & Property Naming - Property schema rebuilt on the naming guidelines. v2.0: The old yes/no values and the “with active time” enum are gone. Booleans now use the
hasprefix with lowerCamelCase per §2 —hasValue,hasLeadingIcon,hasTrailingIcon, the last two replacing the ambiguoushasLeading/hasTrailing— andStateis PascalCase per §1 with Title Case values per §5. Every axis now says what it controls. (C2 · Rename) C2 · Variant & Property Naming - Pressed and Disabled states added. v2.0:
State = Default | Pressed | Disabledreplaces the single default state. Pressed fills the pill#005CE5; Disabled fills it#EEF2F9; Default is white with a#D7E0EFborder. (C5) C5 · Interaction State Coverage - Layer naming rebuilt throughout. v2.0: The two frames named
offsetand the#labelsigil are gone. Every variant now readsPill→LeadingIcon·ContentRow→Label·Value·TrailingIcon, following the §3 vocabulary. Two layers were missed in the sweep and are tracked below; the rest is done. (C1 · Rename) C1 · Layer Structure & Naming -
dropdown groupis now a real Figma Slot. v2.0:Dropdown-Slotis a genuineSLOTnode rather than a frame standing in for one, kebab-case per §4, and it appears on exactly the twelve variants wherehasTrailingIcon=True— the chevron is what signals a dropdown, so slot and affordance are consistent by construction. A consumer attaches their own menu without detaching. (C1 · Slot) C1 · Layer Structure & Naming - Colored spacer instances removed. v2.0: The
_space_4and_space_8instances filled#00FF66and#FFFF00are gone from the layer tree; spacing is carried by auto-layout. A native implementation now sees the children that actually exist. (C1) C1 · Layer Structure & Naming - Rename sweep completed across all 24 variants. v2.1: Verified on the live node.
label→Label(5595:39613) andTrailing Icon→TrailingIcon(5595:39661). Every variant now carries identical layer names —Pill→LeadingIcon·ContentRow→Label·Value·TrailingIcon— soLabelandValueeach expose as a single text property across the whole set rather than fragmenting. (C1 · Rename) C1 · Layer Structure & Naming - Disabled state now mutes its text. v2.1:
Labeldrops to#C2CFE5andValueto#9BC5FD, replacing the full-strength#6780A9and#005CE5that made a disabled chip read as an available action. The muted blue on the value is a nice touch — it keeps the label/value hierarchy legible while removing the affordance, rather than flattening both to one grey. Consistent with Counter and Search Field, which both mute to#C2CFE5. (C5 · Token) C5 · Interaction State Coverage - Scope confirmed dropdown-only — no selected state needed. v2.2: Chip is a dropdown trigger, not a filter toggle.
hasValue=Trueis what a made choice looks like, andDropdown-Sloton the twelvehasTrailingIcon=Truevariants is what the component exists to attach.Pressedtherefore means what it says — a transient finger-down state released on lift — and does not double as a persistent selection. Recorded so the absence ofisSelectedreads as scope rather than an omission: a screen needing an on/off filter pill reaches for a different component rather than pressing this one into service. (C5 · State) C5 · Interaction State Coverage - Leading
Placeholderconfirmed a deliberate swap target. v2.2: The 24×24Placeholderin eachLeadingIconframe is the intended instance-swap point for consumer content — the same pattern Detail Hero uses. The asymmetry with the trailing side is intentional rather than an oversight: the trailing position is a fixedChevron Downplus a namedDropdown-Slot, because what attaches there is a menu the chip must anchor, while the leading position takes an arbitrary icon the chip only has to reserve room for. Two different jobs, two different mechanisms. Attested rather than verified — instance-swap property definitions are not readable through the review tooling. (C6 · Composition) C6 · Asset & Icon Quality - Pressed label opacity confirmed intentional. v2.2:
Labelat#F6F9FD80% against the solid#005CE5pill is deliberate — it holds the label back so the chosenValuereads as the brighter of the two while the chip is held down, preserving the same hierarchy the Default state gets from#6780A9against#005CE5. Consistent with the treatment already confirmed on Page Banner and Detail Hero, and covered by the same owner decision: the composited values are token-bound rather than local overrides. Attested rather than verified — opacity token bindings are not readable through the review tooling. (C3 · Token) C3 · Token Coverage
Three style modes, each with bg, label, and icon tokens. Dropdown adds label-link and chevron.
| Role | Element | Token | Value |
|---|---|---|---|
| Filled | bg | main/filter/color/primary/bg | #005CE5 |
| — | label | main/filter/color/primary/label | #FFFFFF |
| — | icon | main/filter/color/primary/icon | #F6F9FDB8 |
| Light | bg | main/filter/color/secondary/bg | #EEF2F9 |
| — | label | main/filter/color/secondary/label | #6780A9 |
| — | icon | main/filter/color/secondary/icon | #7E96BE |
| — | selected value | main/filter/color/secondary/label-link | #005CE5 |
| — | chevron | main/filter/color/secondary/chevron | #005CE5 |
| Outline | border | main/filter/color/tertiary/border | #D7E0EF |
| — | label | main/filter/color/tertiary/label | #6780A9 |
| — | icon | main/filter/color/tertiary/icon | #7E96BE |
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:chip:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.chip.*
| Figma Property | SwiftUI | Compose |
|---|---|---|
| style | .ebStyle(.filled / .light / .outline) | style = EBChipStyle.* |
| leading | leading: EBChipLeading? | leading: @Composable (() -> Unit)? |
| trailing | trailing: EBChipTrailing? | trailing: EBChipTrailing? |
| selectedValue | selectedValue: String? | selectedValue: String? |
| label | title: String | label: String |
| onTap / onClose | action / onRemove | onClick / onRemove |
// Applied filter — brand fill, avatar + close EBChip("Filter Name", leading: .avatar(EBAvatar(initials: "DM")), trailing: .close, onRemove: { /* remove filter */ }) .ebStyle(.filled) // Inactive filter — light, label only EBChip("Category") .ebStyle(.light) // Dropdown trigger with selected value EBChip("Sort by", selectedValue: "Conservative first", trailing: .chevron, action: { /* open dropdown */ }) .ebStyle(.light)
// Applied filter — brand fill, avatar + close EBChip( label = "Filter Name", style = EBChipStyle.Filled, leading = { EBAvatar(initials = "DM") }, trailing = EBChipTrailing.Close, onRemove = { /* remove filter */ } ) // Inactive filter — light, label only EBChip( label = "Category", style = EBChipStyle.Light ) // Dropdown trigger with selected value EBChip( label = "Sort by", selectedValue = "Conservative first", style = EBChipStyle.Light, trailing = EBChipTrailing.Chevron, onClick = { /* open dropdown */ } )
| Requirement | iOS | Android |
|---|---|---|
| Tap target | 32px height is below HIG 44pt — wrap in a 44pt-tall hit area | 32dp height is below Material 48dp — expand touch target via Modifier.minimumInteractiveComponentSize() |
| Role | .accessibilityAddTraits(.isButton) | Use semantics { role = Role.Button } |
| Close button label | .accessibilityLabel("Remove filter: \(label)") | contentDescription = "Remove filter: $label" |
| Dropdown indicator | .accessibilityHint("Double-tap to change") | Announce chevron via role + state |
| Selected state | .accessibilityAddTraits(.isSelected) for style=filled | selected = true in semantics for active filters |
Do
Use style=filled for applied/active filters, style=light for inactive filters and dropdown triggers, style=outline when the surface beneath is already light gray.
Don't
Mix filled and light chips in the same filter row without intent — it signals "one filter is selected," so using both styles for unrelated reasons misleads the user.
Do
Pair the close affordance with applied filters so users can remove them with a single tap.
Don't
Add a close icon to dropdown-trigger chips — the chevron already indicates the tap behavior; a close icon implies removal instead of picking.
Do
Keep the chip label to one or two words. Use selectedValue to show the picked option separately.
Don't
Stretch chips to fill width — they're meant to fit their content. If a control needs full width, use a Button or Select Field instead.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic names: container, Placeholder, Close, Chevron Down. |
| C2 | Variant & Property Naming | Requires Rework | Split across two components. with icon uses yes/no. Dropdown uses type="with active time". |
| C3 | Token Coverage | Ready | All colors, radii, spacing, and typography bound to tokens. |
| C4 | Native Mappability | Ready | Maps to custom pill on iOS and FilterChip/InputChip on Android. |
| C5 | Interaction State Coverage | Requires Rework | No pressed / disabled / error states. Selected is implied by style=filled. |
| C6 | Asset & Icon Quality | Requires Rework | Leading slot is a hardcoded 24px gray circle — should be a swappable Avatar/Icon instance. |
| Source | Style | Leading | Trailing | Node ID |
|---|---|---|---|---|
| Filter | Filled (primary) | Avatar | Close | 18336:22244 |
| Filter | Filled (primary) | — | — | 18336:22253 |
| Filter | Light | Avatar | Close | 18336:22257 |
| Filter | Light | — | — | 18336:22266 |
| Filter | Outline | Avatar | Close | 18336:22270 |
| Filter | Outline | — | — | 18336:22279 |
| Filter w/ Dropdown | Light | — | Chevron | 18336:22292 |
| Filter w/ Dropdown | Light (w/ selected value) | — | Chevron | 18336:22284 |
dropdown group is now a real Figma Slot; Colored spacer instances removed; Rename sweep completed across all 24 variants; Disabled state now mutes its text; Scope confirmed dropdown-only — no selected state needed; Leading Placeholder confirmed a deliberate swap target; Pressed label opacity confirmed intentional.type + with icon. Dropdown uses type="with active time". Booleans are yes/no. Should consolidate to style / leading / trailing + optional selectedValue.
Open