A multi-line text input for longer-form entry — label, body, and character counter.
4781:35856 in the 2026 Working File. State = Default | Focused | Error | Disabled × hasValue = False | True gives eight variants, the label and subtext are shared instances rather than redrawn, focus and error borders are distinct, the character counter matches its sample exactly, and the local layers now read ValueContainer → Value. Two family questions were settled here rather than left to recur: hasValue is the pattern for fields whose filled state changes geometry, and the error color on a validation message belongs to the shared Subtext Message component. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.Typical mobile contexts: feedback forms, message composers, notes, support request descriptions.
State × hasValue schema mirrors Input Field's by design — family consistency rather than duplication.FormGroup Header for labelling and a Subtext Message for supporting copy and the character count. Nothing external required to render a complete field.State = Default | Focused | Error | Disabled × hasValue, PascalCase per §1 with Title Case values — and hasValue is now the documented family pattern for fields whose filled state changes geometry. Local layers follow the §3 vocabulary as ValueContainer → Value, the label and subtext come from shared instances, and the character counter matches its sample content.FormGroup Header and Subtext Message instances, so label and supporting-text changes propagate from one source across the family rather than being re-authored per component.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | State=Default | 1px #D7E0EF border, white bg. No resize glyph — removed in v2.2. |
| Focused | Yes | Yes | State=Focused | #005CE5 border. Matches SwiftUI @FocusState / Compose FocusRequester vocabulary. |
| Error | Yes | Yes | State=Error | #D61B2C border. Validation copy goes in the Subtext Message row below. |
| Disabled | Yes | Yes | State=Disabled | #EEF2F9 bg, border hidden, text #C2CFE5. |
- Code Connect mappings not registered. Blocked — no native library exists yet. The property schema is clean and every layer is semantically named, so mapping is a mechanical step once the library lands. C7 · Code Connect Linkability
- Boolean property migrated to True/False. v2.0: Rebuilt on node
4781:35856in the 2026 Working File.isFilledis now a real Figma boolean withTrue/Falsevalues, mapping directly to SwiftBool/ KotlinBoolean. Matches the fix Input Field shipped in 1.1.0. (C2 · Rename) C2 · Variant & Property Naming -
State=Activerenamed toFocused. v2.0: The interaction axis is nowState = Default | Focused | Error | Disabled, matching SwiftUI@FocusState/ ComposeFocusRequestervocabulary and theFocusedvalue in §5 of the Property Naming Guidelines. Consistent with Search Field. (C2 · Rename) C2 · Variant & Property Naming - Label and supporting-text slots added. v2.0: Each variant now composes a
FormGroup Headerinstance above the field and aSubtext Messageinstance below it, so labelling and validation copy are part of the component rather than re-implemented per screen. Delivered as shared instances rather than bespoke slots — a better outcome than recommended. (C5 · Slot) C5 · Interaction State Coverage - Character-count affordance added. v2.0: The
Subtext Messageinstance carries a trailing counter (0/100) alongside the supporting-text row, matching Material 3'ssupportingText+counterpattern. The DS can now represent limit state. (C5 · Slot) C5 · Interaction State Coverage - Confirmed as a standalone component. v2.1: Closed by owner decision — Text Area stays its own component rather than folding into Input Field as a
multilineflag. The original consolidation case rested on Text Area duplicating Input Field's schema with no anatomy of its own; it now composes sharedFormGroup HeaderandSubtext Messageinstances and owns a character-count affordance Input Field doesn't need. The parallelState × isFilledschema is deliberate family consistency, not duplication. (Family) C4 · Native Mappability -
State=Errorexception confirmed. v2.1: Closed by owner decision at family level —Errorstays on theStateaxis across Form Elements as a deliberate exception to the State/Status rule, matching how most design systems model form-field validation. Keeps Text Area at 8 variants rather than the 12 a split would require. Applies equally to Search Field and Amount Text Field. (C2 · Property) C2 · Variant & Property Naming - Desktop resize handle removed. v2.2: The
text-area iconframe is gone from all eight variants, taking the browser-onlyresize: bothaffordance with it. NativeTextField(axis: .vertical)/OutlinedTextField(maxLines = n)auto-grow without a user-facing grip, so the component now maps cleanly. Field heights normalised to 50px and every variant is a uniform 94px tall. Also closes the raster-vs-vector question, which is moot now the glyph is gone. (C4 · C6 · Asset) C4 · Native Mappability - Token namespace resolved — generic tokens applied. v2.2: Closed by owner confirmation. The component-scoped
main/text-area/*namespace was dropped in favour of the shared generic token scale, so there is no longer a parallel token set that can drift frommain/input-field/*. Same direction Search Field took. (C1 · Token) C1 · Layer Structure & Naming - Character counter corrected in the filled variants. v2.2: The four
hasValue=Truevariants now read71/100, matching their body copy, instead of the previous0/100. (C1) C1 · Layer Structure & Naming - Boolean renamed
isFilled→hasValue. v2.3: The variant axis is nowState×hasValue. Resolves the catalog gap in the right direction — §2 assigns thehasprefix to presence of content, andhasValuepairs with theValuetext property in §3, whereisFilledread as a visual descriptor. Input Field and Amount Text Field should follow so the family stays aligned. (C2 · Rename) C2 · Variant & Property Naming - Nested duplicate
text-containerremoved. v2.3:text-containernow holds#text-labeldirectly in every variant — the redundant same-size wrapper inside it is gone, and the layer tree is unambiguous. (C1) C1 · Layer Structure & Naming - Sample content matches
hasValuein all eight variants. v2.4: EveryhasValue=Falsevariant now reads Write your message… with a0/100counter, and everyhasValue=Truevariant carries the body sentence with71/100— verified across all eight by text scan. Field heights follow the content: 34px for one line of placeholder, 50px for two lines of copy. Closes the last C1 finding. (C1) C1 · Layer Structure & Naming - Character-count sample content corrected. v2.4: Verified on the live node. The counter now reads
0/100in thehasValue=Falsevariants and71/100in thehasValue=Truevariants, matching the 71-character sample string exactly. Previously the count and the content disagreed, which made the counter look decorative rather than bound. (C1) C1 · Layer Structure & Naming - Local layer names cleaned up. v2.5: Verified on the live node.
text-container→ValueContainerand#text-label→Valueacross all eight variants, so the entered text exposes as a singleValueproperty matching the §3 vocabulary. The bordered frame keeps the nameText Area; it is the only frame at that level and Figma already nests it under the component, so the shadowing is accepted rather than churned. The remaining#labeland#subtextlayers belong to the sharedFormGroup HeaderandSubtext Messageinstances and are their owner’s to rename. (C1 · Rename) C1 · Layer Structure & Naming -
hasValuesettled as a family rule. v2.5: Both approaches in Form Elements are correct, and the rule that separates them is layout. Where the filled state changes the component’s geometry it needs an explicit boolean, because the two shapes cannot be one variant: Text Area is 78px empty and 94px filled, sohasValueearns its place and the set is eight. Where filled changes only color and affordance — Search Field, whose height is constant at 56px — the state is derived from value presence in code and the set stays at four. A consumer can now tell from the geometry alone which pattern a field follows.hasValuegoes into the §2 approved boolean catalog on that basis. (C2 · Family) C2 · Variant & Property Naming - Error-state subtext coloring settled. v2.5: The family rule is that a validation message carries the error color — Amount Text Field turns its
HelperText#D61B2Cand is the reference. Text Area’s subtext is not a local layer: it is the sharedSubtext Messageinstance, which also carries the character counter, so the error color belongs to that component rather than to this one. Recorded here as a shared-component follow-up rather than a Text Area defect, so the difference reads as a known boundary instead of an inconsistency. (C5 · Token) C5 · Interaction State Coverage
Idle state with gray border. Resize-handle glyph sits in the bottom-right regardless of fill.
All four color roles are bound to main/text-area/color/{state}/* tokens. Every value mirrors the equivalent main/input-field/color/* token — motivation for consolidation.
| Role | Token | DEFAULT | ACTIVE | ERROR | DISABLED |
|---|---|---|---|---|---|
| Border | text-area/color/{state}/border | #D7E0EF | #005CE5 | #D61B2C | hidden |
| Background | text-area/color/{state}/bg | #FFFFFF | #FFFFFF | #FFFFFF | #EEF2F9 |
| Text (filled) | text-area/color/{state}/text | #0A2757 | #0A2757 | #0A2757 | #C2CFE5 |
| Placeholder | text-area/color/{state}/placeholder | #C2CFE5 | #C2CFE5 | #C2CFE5 | #C2CFE5 |
| Resize glyph | text-area/color/{state}/icon-resizer | #D7E0EF | #D7E0EF | #D7E0EF | #D7E0EF |
Focused state with 2px blue border. Rename target: <code>focused</code>.
Validation error state with 2px red border. No inline error-message slot — copy is the consumer's responsibility.
Non-interactive state with gray fill and muted text. Border hidden.
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:form-elements:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.form.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| (text content) | text: Binding<String> | value: String |
| isFilled (yes/no) | — | — |
| (multi-line default) | axis: .vertical | singleLine = false |
| (auto-grow range) | .lineLimit(3...6) | maxLines = 6 |
| state = default | — | — |
| state = active | @FocusState | interactionSource |
| state = error | .ebError(true) | isError = true |
| state = disabled | .disabled(true) | enabled = false |
| isExpandable | — | — |
EBInputField("Tell us more", text: $value, axis: .vertical) .lineLimit(3...6)
EBInputField( value = text, onValueChange = { text = it }, placeholder = "Tell us more", singleLine = false, maxLines = 6 )
EBInputField("Tell us more", text: $value, axis: .vertical) .lineLimit(3...6) .ebError(true)
EBInputField( value = text, onValueChange = { text = it }, placeholder = "Tell us more", singleLine = false, maxLines = 6, isError = true )
EBInputField("Tell us more", text: $value, axis: .vertical) .lineLimit(3...6) .disabled(true)
EBInputField( value = text, onValueChange = { text = it }, placeholder = "Tell us more", singleLine = false, maxLines = 6, enabled = false )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 x 44 pt (per-line height 22pt, container ≥44pt) | 48 x 48 dp |
| Accessibility label | .accessibilityLabel("Comment") | contentDescription |
| Error announcement | VoiceOver reads error via .accessibilityValue | TalkBack reads error via semantics { error() } |
| Character-count announcement | Announce remaining via .accessibilityValue when a limit is set | Expose via supportingText semantics |
Do
Use for free-form responses expected to exceed one line — feedback, comments, messages, notes.
Don't
Use for short structured inputs (name, phone, code) — Input Field's single-line default is more appropriate and faster to fill.
Do
Pair with a visible label above the field and a helper-text row below for character counts or format hints.
Don't
Rely on the desktop resize handle on mobile — mobile fields auto-grow within lineLimit/maxLines and the handle has no native behavior.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Needs Refinement | Duplicate token namespace main/text-area/* mirrors main/input-field/* exactly. Text layer structure is clean. |
| C2 | Variant & Property Naming | Requires Rework | isFilled=yes/no (same anti-pattern Input Field already resolved). state=active should be focused. |
| C3 | Token Coverage | Ready | All colors bound to main/text-area/color/*. Spacing and radius tokens resolved. |
| C4 | Native Mappability | Requires Rework | Exists as a distinct component but native platforms treat multi-line as a single TextField with axis: .vertical / singleLine=false. Desktop resize handle has no native equivalent. |
| C5 | Interaction State Coverage | Needs Refinement | All 4 interaction states present. Missing slots: label, helper/error text, character count. |
| C6 | Asset & Icon Quality | Requires Rework | Resize glyph is a raster PNG referenced four times (once per state) instead of a single vector instance. |
| C7 | Code Connect Linkability | Not Mapped | Blocked by the consolidation decision and property renames. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Requires Rework | isFilled=yes/no cannot map to native booleans |
| Component identity | Requires Rework | Native platforms have no standalone TextArea primitive; consolidation into Input Field is required first |
| Native component file | Needs Refinement | Proposed target: EBInputField with multi-line flag |
4 state values × 2 isFilled values.
| state | isFilled | Height | Node ID |
|---|---|---|---|
| default | yes | 62px | 3070:21242 |
| default | no | 46px | 3070:21239 |
| active | yes | 62px | 3070:21243 |
| active | no | 46px | 3070:21238 |
| error | yes | 62px | 3070:21244 |
| error | no | 46px | 3070:21240 |
| disabled | yes | 62px | 3070:21241 |
| disabled | no | 46px | 3070:21237 |
state (default/active/error/disabled) × isFilled (yes/no). Multi-line sibling of Input Field within the Form Elements group.
Documentedmultiline / lineLimit prop to match SwiftUI axis: .vertical and Compose singleLine=false.
OpenisFilled=yes/no instead of true/false. Same anti-pattern Input Field already resolved.
Openmain/text-area/color/* values mirror main/input-field/color/* exactly; candidate for aliasing or deletion after consolidation.
Open