East Blue
KeepReady
Text Area Component link

A multi-line text input for longer-form entry — label, body, and character counter.

Keep — all findings resolved
Rebuilt on node 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.
In Context

Typical mobile contexts: feedback forms, message composers, notes, support request descriptions.

Live Preview
Placeholder
Properties
state
isFilled
isExpandable
DS Health
Reusable
Pass
Works anywhere multi-line text is needed, carries its own label and supporting-text anatomy, and is confirmed as a standalone component. The State × hasValue schema mirrors Input Field's by design — family consistency rather than duplication.
Self-contained
Pass
Carries its own border, fill, and text styles per state, plus a FormGroup Header for labelling and a Subtext Message for supporting copy and the character count. Nothing external required to render a complete field.
Consistent
Pass
Axes are clean — 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.
Composable
Pass
Composes shared 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.
Behavior
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.
Issues
  • Boolean property migrated to True/False. v2.0: Rebuilt on node 4781:35856 in the 2026 Working File. isFilled is now a real Figma boolean with True/False values, mapping directly to Swift Bool / Kotlin Boolean. Matches the fix Input Field shipped in 1.1.0. (C2 · Rename) C2 · Variant & Property Naming
  • State=Active renamed to Focused. v2.0: The interaction axis is now State = Default | Focused | Error | Disabled, matching SwiftUI @FocusState / Compose FocusRequester vocabulary and the Focused value 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 Header instance above the field and a Subtext Message instance 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 Message instance carries a trailing counter (0/100) alongside the supporting-text row, matching Material 3's supportingText + counter pattern. 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 multiline flag. The original consolidation case rested on Text Area duplicating Input Field's schema with no anatomy of its own; it now composes shared FormGroup Header and Subtext Message instances and owns a character-count affordance Input Field doesn't need. The parallel State × isFilled schema is deliberate family consistency, not duplication. (Family) C4 · Native Mappability
  • State=Error exception confirmed. v2.1: Closed by owner decision at family level — Error stays on the State axis 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 icon frame is gone from all eight variants, taking the browser-only resize: both affordance with it. Native TextField(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 from main/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=True variants now read 71/100, matching their body copy, instead of the previous 0/100. (C1) C1 · Layer Structure & Naming
  • Boolean renamed isFilled → hasValue. v2.3: The variant axis is now State × hasValue. Resolves the catalog gap in the right direction — §2 assigns the has prefix to presence of content, and hasValue pairs with the Value text property in §3, where isFilled read 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-container removed. v2.3: text-container now holds #text-label directly 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 hasValue in all eight variants. v2.4: Every hasValue=False variant now reads Write your message… with a 0/100 counter, and every hasValue=True variant carries the body sentence with 71/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/100 in the hasValue=False variants and 71/100 in the hasValue=True variants, 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 → ValueContainer and #text-label → Value across all eight variants, so the entered text exposes as a single Value property matching the §3 vocabulary. The bordered frame keeps the name Text 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 #label and #subtext layers belong to the shared FormGroup Header and Subtext Message instances and are their owner’s to rename. (C1 · Rename) C1 · Layer Structure & Naming
  • hasValue settled 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, so hasValue earns 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. hasValue goes 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 #D61B2C and is the reference. Text Area’s subtext is not a local layer: it is the shared Subtext Message instance, 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
Styles
Text Area
LabelWrite your message…Message content0/100
Properties
State
hasValue
hasLabel
hasSubtext
Properties
State Default
hasValue False
hasLabel True
hasSubtext True
Colors
Field background #FFFFFF
Border #D7E0EF
Value #90A8D0
Label #0A2757
Subtext #6780A9
Typography
Label —
Value —
Subtext —
Layout
Height 34px
Width 358px
Radius 6px
Padding H 12px / 8px
Padding V 10px / 8px
Gap 0px
Alignment —
Colors by State

Read off node 4781:35856. ROLE is the State value; the second column is the ELEMENT. Token paths are indicative — variable bindings are not readable through the Talk To Figma plugin.

Role Element Token Value
Default Field background surface/default #FFFFFF
— Border border/default #D7E0EF
— Value (empty) text/placeholder #90A8D0
— Value (filled) text/primary #0A2757
— Label text/primary #0A2757
— Subtext text/secondary #6780A9
Focused Field background surface/default #FFFFFF
— Border border/focused #005CE5
— Value (empty) text/placeholder #90A8D0
— Value (filled) text/primary #0A2757
— Label text/primary #0A2757
— Subtext text/secondary #6780A9
Error Field background surface/default #FFFFFF
— Border border/error #D61B2C
— Value (empty) text/placeholder #90A8D0
— Value (filled) text/primary #0A2757
— Label text/primary #0A2757
— Subtext text/secondary #6780A9
Disabled Field background surface/disabled #EEF2F9
— Border – –
— Value (empty) text/placeholder #90A8D0
— Value (filled) text/placeholder #90A8D0
— Label text/primary #0A2757
— Subtext text/secondary #6780A9
Installation Planned API

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:form-elements:1.0.0")

Import

import EastBlueDS
import com.eastblue.ds.form.*

Package not yet published. These are the planned distribution paths.

Property Mapping
Figma PropertySwiftUICompose
State = Default | Focused | Error | Disabled .ebState(.default) · @FocusState · .ebError(true) · .disabled(true) state = EBFieldState.Default · interactionSource · isError = true · enabled = false
hasValue = False | True — derived from whether text is empty — derived from whether value is empty
hasLabel (boolean) .ebLabel(_ text: String?) label: String?
hasSubtext (boolean) .ebSubtext(_ text: String?) subtext: String?
— no Figma property text: Binding<String> value: String + onValueChange: (String) -> Unit
— no Figma property axis: .vertical singleLine = false
Usage Snippets Planned API
Multi-line text area
EBTextArea("Write your message…", text: $message)
    .ebState(.default)
    .ebLabel("Label")
    .ebSubtext("Message content")
EBTextArea(
    placeholder = "Write your message…",
    value = message,
    onValueChange = { message = it },
    label = "Label",
    subtext = "Message content"
)
Without label or subtext
EBTextArea("Write your message…", text: $message)
EBTextArea(
    placeholder = "Write your message…",
    value = message,
    onValueChange = { message = it },
    label = null,
    subtext = null
)
Error
EBTextArea("Write your message…", text: $message)
    .ebState(.error)
EBTextArea(
    placeholder = "Write your message…",
    value = message,
    onValueChange = { message = it },
    state = EBFieldState.Error
)
Disabled
EBTextArea("Write your message…", text: $message)
    .disabled(true)
EBTextArea(
    placeholder = "Write your message…",
    value = message,
    onValueChange = { message = it },
    enabled = false
)
Accessibility
RequirementiOSAndroid
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
Usage Guidelines

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

Turn `hasSubtext` off when there is nothing useful to say — an empty subtext row still reserves 22px and pushes the layout down.

Don't

Turn `hasLabel` off to save space in a form — a field without a visible label leaves screen-reader users with only the placeholder, which disappears on focus.

Do

Pair with a visible label above the field and a helper-text row below for character counts or format hints.

Don't

Expect the field to be manually resizable — the desktop resize handle was removed in v2.2 because it had no touch equivalent. The field grows with its content within the platform's lineLimit / maxLines.

Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Ready Local layers follow the §3 vocabulary — ValueContainer → Value — and the nested duplicate text-container is gone. The remaining #label and #subtext layers belong to the shared FormGroup Header and Subtext Message instances and are their owner’s to rename.
C2 Variant & Property Naming Ready State = Default | Focused | Error | Disabled × hasValue = False | True, PascalCase per §1 with Title Case values per §5. isFilled → hasValue and Active → Focused both landed. Error on the State axis is the documented §6 form-field exception, not a divergence.
C3 Token Coverage Needs Refinement The component-scoped namespace was replaced by the shared generic token scale. Bindings themselves are not readable through the review tooling, so the token paths on the Style tab are indicative and need a Dev Mode confirmation.
C4 Native Mappability Ready Confirmed a standalone component. Native platforms model it as a text field with axis: .vertical / singleLine = false rather than a separate primitive, which the property mapping now states. The desktop-only resize handle was removed — it had no touch equivalent.
C5 Interaction State Coverage Ready All four interaction states ship, each with its own border treatment — 1px #D7E0EF default, 2px #005CE5 focused, 2px #D61B2C error, no border on disabled. Label, subtext and the character counter are all present as shared instances.
C6 Asset & Icon Quality Not Applicable The component carries no icons. The raster resize glyph that previously appeared once per state was removed with the resize affordance in v2.2.
Variants Inventory (8 total)

State (4) × hasValue (2) — all eight combinations exist. The filled variants are 16px taller because the value wraps to two lines. hasLabel and hasSubtext are component properties rather than variant axes, so they do not multiply the count.

StatehasValueDimensionsNode ID
DefaultFalse358 × 784781:35853
DefaultTrue358 × 944781:35851
FocusedFalse358 × 784781:35850
FocusedTrue358 × 944781:35852
ErrorFalse358 × 784781:35849
ErrorTrue358 × 944781:35855
DisabledFalse358 × 784781:35848
DisabledTrue358 × 944781:35854
2.6 — September 2026Minor
Style + Code tabs rebuilt against node 4781:35856
Style tab rebuilt as a single card — one spec card driven by a panel mirroring the Figma property panel: State and hasValue as selects (both are variant properties), hasLabel and hasSubtext as toggles. Replaces four fixed cards. Documented
Style
Filled state renders two lines — the preview had drawn one. Value is 338 × 32 at line-height 16, so switching hasValue now grows the field and wraps the content, as it does in Figma. Caught by checking against an export rather than the layer tree. Documented
Style
Focused and Error carry a 2px rule against Default’s 1px — read off the export, since strokeWeight is not exposed by the plugin. Documented
Style
Property mapping corrected — isFilled, isExpandable and state = active are gone; isExpandable never existed on the component. Now maps State, hasValue, hasLabel and hasSubtext plus the value binding and the multi-line behaviour. Documented
Code
Variants inventory corrected — the isFilled axis renamed to hasValue, and heights corrected from a single 62px to 78px empty / 94px filled. Documented
Code
2.5 — September 2026Minor
Layer naming and two family rules
Local layer names cleaned up — text-container → ValueContainer, #text-label → Value, so the entered text exposes as a single Value property. Resolved
C1
hasValue settled as a family rule — an explicit boolean where the filled state changes geometry (78 → 94 here), derived in code where it changes only colour, as on Search Field. Added to the §2 approved boolean catalog. Resolved
C2
Error-state subtext colouring settled — the validation message carries the error colour; Text Area’s subtext is the shared Subtext Message instance, so that belongs to the shared component rather than here. Resolved
C5
2.4 — September 2026Patch
Sample content corrected
Sample content matches hasValue in all eight variants. Resolved
C1
Character counter corrected — reads 0/100 empty and 71/100 filled, matching the 71-character sample exactly. Previously the count and the content disagreed, making the counter look decorative rather than bound. Resolved
C1
2.3 — September 2026Minor
Boolean renamed and duplicate removed
Boolean renamed isFilled → hasValue — has is the §2 prefix for a content-presence flag. Resolved
C2
Nested duplicate text-container removed. Resolved
C1
2.2 — September 2026Minor
Desktop affordance and tokens
Desktop resize handle removed — a pointer-only affordance with no touch equivalent. Resolved
C4
Token namespace resolved — generic tokens applied in place of the component-scoped namespace. Resolved
C3
2.1 — September 2026Patch
Scope and exception confirmed
Confirmed a standalone component rather than a composed variant of Input Field. Resolved
C4
State=Error exception confirmed — covered by the §6 form-field exception in the Property Naming Guidelines. Resolved
C2
2.0 — September 2026Major
Rebuilt on node 4781:35856 — 2026 Working File
Boolean values migrated to True/False from the old yes/no pair. Resolved
C2
State=Active renamed Focused — matching the sibling fields. Resolved
C2
Label and supporting-text slots added — both now shared FormGroup Header and Subtext Message instances rather than redrawn locally. Resolved
C4
Character-count affordance added. Resolved
C5
1.0.0 — April 2026Major
Initial Assessment · node 3070:21245
Component assessed — 8 variants documented across state (default/active/error/disabled) × isFilled (yes/no). Multi-line sibling of Input Field within the Form Elements group. Documented
Initial
Consolidation proposed — Fold Text Area into Input Field via a multiline / lineLimit prop to match SwiftUI axis: .vertical and Compose singleLine=false. Open
Family
Boolean property uses Yes/No — isFilled=yes/no instead of true/false. Same anti-pattern Input Field already resolved. Open
C2 Open
Desktop resize-handle glyph — 12×12px raster PNG in the bottom-right has no native mobile equivalent; iOS/Android auto-grow without a user-facing handle. Open
C4, C6 Open
Duplicate token namespace — main/text-area/color/* values mirror main/input-field/color/* exactly; candidate for aliasing or deletion after consolidation. Open
C1 Open
Code Connect mappings — No CLI mappings registered yet. Blocked by consolidation decision and property renames. Open
C7 Open