A multi-line text input for longer-form entry — label, body, and character counter.
main/text-area/*). SwiftUI exposes multi-line via axis: .vertical on TextField; Compose exposes it via singleLine=false. Fold into Input Field with a multiline / lineLimit prop so the DS maps 1:1 to the native primitive.Typical mobile contexts: feedback forms, message composers, notes, support request descriptions.
multiline flag than by shipping a parallel component.label slot, no helperText/error-message slot, no characterCount slot — validation and labeling are pushed onto every consuming screen.isFilled=yes/no (C2, same anti-pattern Input Field already fixed). state=active instead of focused matches Input Field but diverges from the broader DS verb set. Tokens duplicated under main/text-area/* with identical values to main/input-field/*.expand-icon frame holds a fixed raster glyph rather than a swappable node.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | state=default | 1px #D7E0EF border, white bg. Resize glyph shown. |
| Active (Focused) | Yes | Yes | state=active | 2px #005CE5 border. Should rename to focused to match platform vocabulary. |
| Error | Yes | Yes | state=error | 2px #D61B2C border. No inline error-message slot. |
| Disabled | Yes | Yes | state=disabled | #EEF2F9 bg, border hidden, text #C2CFE5. |
- Boolean property uses Yes/No.
isFilled=yes/nocannot map to SwiftBoolor KotlinBooleanwithout a translation layer. Input Field already fixed this — Text Area should follow. C2 · Variant & Property Naming - Desktop resize-handle glyph baked into a mobile component. The
text-area icon(12×12px, bottom-right) mirrors the browser-onlyresize: bothaffordance. NativeTextField(axis: .vertical)/OutlinedTextField(maxLines = n)auto-grow without a user-facing handle — the glyph has no native equivalent and should not ship in mobile variants. C4 · Native Mappability - Resize handle is a raster PNG, once per state. Four separate PNG assets are referenced for the same 12×12px glyph. Even if the handle survives, it should be a single vector instance tinted by
main/text-area/color/{state}/icon-resizer. C6 · Asset & Icon Quality - No
labelorhelperTextslot. Labels and validation messages are handled by the consumer, so every screen re-implements the anatomy. Native multi-line text fields expose both as first-class parameters. C5 · Interaction State Coverage - No
characterCount/ limit affordance. Multi-line entry is the canonical use case for character limits (comments, reviews, message composers). There is no count slot and nomaxLengthhook — the DS cannot represent limit state today. C5 · Interaction State Coverage - Tokens duplicated under
main/text-area/*. Every value inmain/text-area/color/*mirrorsmain/input-field/color/*exactly. If Text Area folds into Input Field, this namespace collapses; if it stays, the two token sets should alias a sharedmain/field/*collection to prevent drift. C1 · Layer Structure & Naming - Code Connect mappings not registered. Blocked by property naming and the multi-line-vs-Input Field decision. Cannot register until the family shape is finalized. C7 · Code Connect Linkability
- Fold Text Area into Input Field as a
multiline/lineLimitprop. SwiftUI models this asTextField(text:, axis: .vertical).lineLimit(3...6); Compose models it asOutlinedTextField(singleLine = false, maxLines = 6). Both are the same primitive with a single flag. Mirroring that in Figma collapses 8 variants into 0 net new variants on Input Field (add amultilineboolean to the existing 8-variant matrix) and removes the duplicated token namespace. Family - Rename
isFilledto usetrue/false. Same fix Input Field already shipped in 1.1.0. Required for SwiftBool/ KotlinBooleanmapping. Rename - Rename
state=activetostate=focused. Matches SwiftUI@FocusStateand ComposeFocusRequestervocabulary. Apply to Input Field and the whole Form Elements family at once so the rename lands once. Rename - Drop the desktop resize handle on mobile variants. If Text Area survives as a sibling, remove the
expand-iconframe — it has no native equivalent on iOS/Android. Keep it only if a web/desktop DS variant is in scope, gated behind a platform axis. Asset - Add a
helperTextslot and acharacterCountslot beneath the field. Multi-line is the canonical character-count surface. Expose a supporting-text row that can host either error copy, hint copy, or a count — matching Material 3'ssupportingText/counterpattern. If Text Area folds into Input Field, this slot lives on Input Field and serves both single- and multi-line. Slot - Alias
main/text-area/*to a sharedmain/field/*collection. If the family stays split for any reason, the two token sets must reference a single source so border/bg/placeholder/disabled colors never drift. Preferable outcome: deletemain/text-area/*outright after consolidation. Token
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