A message composer that arranges a leading action slot, a composed Input Field and a trailing send slot into one row.
5536:31209 in the 2026 Working File, and the restructure has landed in full: renamed Chat Composer, rebuilt as a composed pattern around a real Input Field instance, with leadingAction and trailingAction as genuine Figma Slots holding their icon instances directly, both action glyphs vectorised from the shared icon library, and a send control that dims when there is nothing to send. The schema is now isActive × hasValue — two independent booleans carrying the §2 prefix with Title Case values, naming what actually differs between variants rather than the effect that follows. Disabled and error states are scoped out for this release by owner decision. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.Canonical contexts: chat threads, customer-support conversations, peer-to-peer messaging, and comment composers docked to the bottom of a scroll view.
isActive × hasValue — both §2 booleans in lowerCamelCase with Title Case values per §5 — naming the field’s focus and content rather than the effects that follow from them. Structure matches: real Figma Slots holding their instances directly on both sides, library icon instances, and a composed Input Field. Note for handoff: what this component calls isActive is what its Form Elements siblings express as State=Focused.Input Field instance. It arranges DS parts rather than reimplementing them, and a consumer can swap either action without detaching.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default (inactive) | Yes | Yes | active=no | Inner field shows 1px #D7E0EF border, placeholder text "Say hi!". |
| Active (focused) | Yes | Yes | active=yes | Inner field shows 2px #005CE5 border. Text color switches from placeholder to filled. |
| Filled (has content) | N/A | N/A | (not represented) | No distinct variant. Inner field's isFilled is pinned by the active toggle; the composer can't model "typed but unfocused". |
| Error | N/A | N/A | (not represented) | Input Field supports Error; Chat Field does not expose it. |
| Disabled / send-disabled | N/A | N/A | (not represented) | No disabled state for the composer, and no way to dim the send icon when the field is empty. |
- Renamed to Chat Composer and rebuilt as a composed pattern. v2.0: Rebuilt on node
5536:31209in the 2026 Working File. The component no longer redraws a text field — it composes a realInput Fieldinstance between two action slots, which is exactly the restructure the previous assessment asked for. It is now a layout that arranges existing DS parts rather than a primitive competing with one, and a fix to Input Field reaches it for free. (C4 · Composition) C4 · Native Mappability -
leadingActionandtrailingActionare real Figma Slots. v2.0: Both are genuineSLOTnodes rather than frames standing in for them, so a consumer swaps in their own attach control or send affordance without detaching. This also gives the native handoff two named content slots to bind rather than fixed children. (C1 · Slot) C1 · Layer Structure & Naming - Action glyphs are vectors, not rasters. v2.0: Both PNGs are gone. The leading control is an
Add_Fullinstance carrying ashape_fullboolean operation, and the trailing control is aSend Message Mediuminstance — both from the shared icon library, so they recolor from tokens and stay crisp at any density. Confirmed visually as well as structurally. (C6 · Asset) C6 · Asset & Icon Quality - Send-disabled visual added. v2.0: The trailing send control now dims when there is nothing to send — a pale blue paper plane at
sendEnabled=falseagainst the full#005CE5atsendEnabled=true, alongside the field showing placeholder copy rather than a value. This is the recommendation applied: an empty composer no longer offers a send affordance that looks live. Verified by export rather than from the layer tree. (C5 · State) C5 · Interaction State Coverage -
active=yes/noretired; variant count doubled. v2.0: The old two-variant set with ayes/noboolean is replaced by two axes over four variants — a state axis carryingdefaultandactive(the field border moving#D7E0EF→#005CE5), and a send-enablement boolean using realtrue/falsevalues. The set now depicts focus and content independently rather than collapsing them. Property casing still needs a pass and is tracked below. (C2 · Property) C2 · Variant & Property Naming - Trailing action slot dropped its generic wrapper. v2.0:
trailingActionholds its icon instance directly, with no intermediate frame namedcontainer. The leading slot still carries one, tracked below. (C1) C1 · Layer Structure & Naming - Property schema rebuilt as two booleans. v2.1: Verified on the live node.
state = default | activeandsendEnabledare replaced byisActive = False | True×hasValue = False | True. Both carry the §2is/hasprefix in lowerCamelCase with Title Case values per §5, and expressing focus as a boolean rather than a two-value enum is the better fit — there were only ever two positions, and the two axes are genuinely independent: a composer can be focused and empty, or unfocused and full. (C2 · Rename) C2 · Variant & Property Naming - Axis renamed to name the cause, not the effect. v2.1:
sendEnabled→hasValue. The axis now describes what actually differs between the variants — the field holds content — with the send control’s appearance following from it. That removes the impossible combination the old name allowed, where a designer could setsendEnabled=trueon an empty composer, and aligns the vocabulary with Text Area. (C2 · Rename) C2 · Variant & Property Naming - Action slots evened up. v2.1: The
containerframe is gone fromleadingAction, which now holds itsAdd_Fullinstance directly, matchingtrailingAction. Both slots are structurally identical, so a consumer swapping either one meets the same shape. (C1) C1 · Layer Structure & Naming -
hasValueas an explicit axis — recorded exception. v2.1: The family rule set on Text Area is thathasValueearns a variant axis where the filled state changes geometry, and is derived in code where it changes only color — which is why Search Field has no such boolean. Chat Composer is 88px in all four variants, so it looks like it should derive too. It keeps the axis deliberately: unlike a bare field, the composer owns a separate interactive control whose enabled state has to be specified for handoff, and the send button’s two appearances are a spec a developer needs to see rather than infer. Recorded here so the apparent contradiction reads as a considered exception. (C2 · Docs) C2 · Variant & Property Naming - Disabled and error states scoped out for now. v2.1: Confirmed by the owner — the composer ships focus and content only. Unavailable conversations are out of scope for this release rather than unhandled, so a native implementation should not expect a dimmed composer to bind to. When offline, muted, rate-limited or blocked-recipient cases are taken on, they arrive together as one state pass rather than being added piecemeal. Recorded so the absence reads as scope rather than omission. (C5) C5 · Interaction State Coverage
The composer owns only two tokens (bg, icon). Every other color is inherited from the nested Input Field's main/input-field/* collection — another motivation for rebuilding the composer as a composition rather than a primitive.
| Role | Token | DEFAULT (active=no) | ACTIVE (active=yes) |
|---|---|---|---|
| Composer bg | main/chat-field/color/bg | #FFFFFF | #FFFFFF |
| Leading icon (plus) | main/chat-field/color/icon | #005CE5 | #005CE5 |
| Trailing icon (send) | main/chat-field/color/icon | #005CE5 | #005CE5 |
| Field bg | main/input-field/{state}/bg | #FFFFFF | #FFFFFF |
| Field border | main/input-field/{state}/border | #D7E0EF (1px) | #005CE5 (2px) |
| Field placeholder | main/input-field/default/placeholder | #90A8D0 | #90A8D0 |
| Field text (filled) | main/input-field/default/text | #0A2757 | #0A2757 |
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:chat:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.chat.*
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| (text content) | text: Binding<String> | value: String |
| active (yes/no) | @FocusState | interactionSource |
| (leading slot) | leadingAction: () -> Void | leadingAction: () -> Unit |
| (trailing slot) | trailingAction: () -> Void | trailingAction: () -> Unit |
| (line growth) | axis: .vertical, lineLimit(1...5) | singleLine = false, maxLines = 5 |
| (placeholder) | placeholder: String | placeholder: String |
EBChatComposer( placeholder: "Say hi!", text: $message, onAttach: { presentAttachSheet() }, onSend: { send(message) } )
EBChatComposer( value = message, onValueChange = { message = it }, placeholder = "Say hi!", onAttach = { presentAttachSheet() }, onSend = { send(message) } )
EBChatComposer( placeholder: "Say hi!", text: $message, onAttach: attach, onSend: send, sendEnabled: !message.isEmpty )
EBChatComposer( value = message, onValueChange = { message = it }, placeholder = "Say hi!", onAttach = attach, onSend = send, sendEnabled = message.isNotEmpty() )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target (icon buttons) | 44 × 44 pt — 32px frame pads to 44pt hit area | 48 × 48 dp — 32px frame pads to 48dp hit area |
| Leading icon label | .accessibilityLabel("Attach") | contentDescription = "Attach" |
| Trailing icon label | .accessibilityLabel("Send") | contentDescription = "Send" |
| Send-disabled announcement | VoiceOver reads "Send, dimmed" via .accessibilityHint("Enter a message first") | TalkBack reads disabled state via semantics { disabled() } |
| Keyboard submit | .onSubmit { send() } on the nested TextField | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send) |
Do
Dock the composer to the bottom of the chat scroll view with the keyboard inset, and let the field auto-grow within lineLimit(1...5) / maxLines = 5.
Don't
Use Chat Field for single-line structured inputs (name, phone, amount). Use Input Field or the typed form-element sibling instead.
Do
Dim the trailing send icon when the field is empty. Reflects system chat conventions (iMessage, WhatsApp, Messenger) and prevents empty-send.
Don't
Swap the plus icon for unrelated actions (navigation, close). Leading slot is reserved for content-entry affordances: attach, camera, mic, emoji.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Needs Refinement | Both icon frames share the generic name container. Should be leading / trailing. |
| C2 | Variant & Property Naming | Requires Rework | active=yes/no — boolean naming and semantic intent both wrong; the property duplicates the inner field's State=Active. |
| C3 | Token Coverage | Ready | main/chat-field/color/bg and main/chat-field/color/icon resolved. Spacing (space/space-8, 12, 16, 24) and radius (radius/radius-2) bound. |
| C4 | Native Mappability | Requires Rework | No native primitive for "chat composer"; maps to HStack { Button + TextField + Button }. Shipping as a single component hides the composition. |
| C5 | Interaction State Coverage | Requires Rework | Only Active/Inactive. Missing Error, Disabled, isFilled, and send-disabled states that the use case requires. |
| C6 | Asset & Icon Quality | Requires Rework | Both leading (plus) and trailing (send) glyphs are raster PNGs; must vectorize and bind to main/chat-field/color/icon. |
Single axis: active (no/yes). Both variants are 360×88px.
| active | Size | Inner field border | Node ID |
|---|---|---|---|
| no | 360×88 | 1px #D7E0EF | 23:145916 |
| yes | 360×88 | 2px #005CE5 | 23:145922 |
leadingAction and trailingAction are real Figma Slots; Action glyphs are vectors, not rasters; Send-disabled visual added; active=yes/no retired; variant count doubled; Trailing action slot dropped its generic wrapper; Property schema rebuilt as two booleans; Axis renamed to name the cause, not the effect; Action slots evened up; hasValue as an explicit axis — recorded exception; Disabled and error states scoped out for now.active axis. First component in the new Chat group. Anatomy: leading plus icon (32px raster) + nested Input Field + trailing send icon (32px raster) in a 360×88 container.
DocumentedEBChatComposer wrapping EBInputField + two EBIconButton slots) and rename to Chat Composer / Message Composer. Drops the ambiguous active boolean and inherits Input Field's full state matrix.
Openactive=yes/no cannot map to Swift Bool / Kotlin Boolean, and the property duplicates the inner field's State=Active.
OpenAdd_Full (plus) and Send Message Medium (paper-plane) ship as PNG references instead of vectors, on a 32×32 frame.
Opencontainer — Leading and trailing icon wrappers both use the same generic layer name; Code Connect cannot distinguish them.
Open