TextArea | CoUI
LogoCoUI

TextArea

여러 줄 텍스트 입력 영역 컴포넌트

TextArea#

여러 줄의 텍스트를 입력받는 텍스트 영역 컴포넌트입니다. Flutter/Web 양쪽에서 동일한 API(TextArea)를 제공합니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 댓글, 메모, 설명 등 여러 줄 텍스트 입력이 필요할 때
  • 사용자 피드백이나 문의 내용을 받을 때
  • 긴 텍스트 편집이 필요할 때

대신 다른 컴포넌트를 사용하세요:

  • TextField: 한 줄 텍스트 입력
  • Form: 여러 필드 묶어 검증
  • Select: 미리 정의된 옵션 선택

기본 사용법 (Basic Usage)#

// 기본 텍스트 영역
TextArea(
  placeholder: 'Enter your message...',
  rows: 4,
  onChanged: (value) => print(value),
)

// 라벨 + 설명 + 에러
TextArea(
  label: 'Message',
  description: 'Type your feedback here',
  errorText: hasError ? 'This field is required' : null,
  placeholder: 'Enter your message...',
  onChanged: handleChange,
)

// 크기 조절 가능 (드래그 핸들) — 초기 높이는 chrome 이므로 Style 슬롯으로
TextArea(
  expandableHeight: true,
  expandableWidth: true,
  placeholder: 'Drag to resize...',
  onChanged: handleChange,
  textAreaStyle: const CoreTextAreaStyle(initialHeight: 150),
)
// 기본 텍스트 영역
TextArea(
  placeholder: 'Enter your message...',
  rows: 4,
  onChanged: (value) => print(value),
)

// 라벨 + 설명 + 에러
TextArea(
  label: 'Message',
  description: 'Type your feedback here',
  errorText: hasError ? 'This field is required' : null,
  placeholder: 'Enter your message...',
  onChanged: handleChange,
)

// rows + maxLength 제어
TextArea(
  rows: 6,
  maxLength: 500,
  placeholder: 'Max 500 chars',
  onChanged: handleChange,
)

Props / Parameters#

TextArea은 Flutter/Web에서 동일한 파라미터 이름을 사용합니다.

속성Flutter 타입Web 타입기본값설명
placeholder String? String? null 빈 상태 안내
initialValue String? String? null 초기 값
onChanged CoreValueChanged<String>? CoreValueChanged<String>? null 값 변경 콜백
onSubmitted CoreValueChanged<String>? CoreValueChanged<String>? null 제출 콜백 (Web은 change 이벤트)
enabled bool bool true 활성화 여부
readOnly bool bool false 읽기 전용
autofocus bool bool false 자동 포커스
selectAllOnFocus bool bool false 포커스 시 전체 선택
size CoreComponentSize CoreComponentSize md 크기 토큰 (위젯 파라미터)
label String? String? null 상단 라벨
description String? String? null 하단 설명 (에러 없을 때)
errorText String? String? null 에러 메시지
required bool bool false 필수 입력 표시
name String? String? null 폼 제출용 필드 이름
maxLength int? int? null 최대 글자 수
rows int? int? null 보이는 행 수
prefix Widget? Component? null 좌측 아이콘/텍스트
suffix Widget? Component? null 우측 아이콘/텍스트
expandableHeight bool bool false 세로 리사이즈 가능
expandableWidth bool bool false 가로 리사이즈 가능
textAreaStyle CoreTextAreaStyle? CoreTextAreaStyle? null chrome / dimensional / cursor / nested slot 단일 진입점

textAlign / textAlignVertical 은 위젯 파라미터가 아니라 CoreTextAreaStyle 필드입니다 (defaultTextAlign = start, defaultTextAlignVertical = top). initialHeight / minHeight / maxHeight / padding / borderRadius / cursor 색·두께·반경도 같은 슬롯 안에 있습니다.

입력 동작 / 폼 (양 플랫폼)#

속성Flutter 타입Web 타입기본값설명
autocorrect bool? bool? null (→ true) 자동 교정
obscureText bool bool false 텍스트 마스킹 (Web 은 -webkit-text-security)
obscuringCharacter String String '•' 마스킹 문자. Web 은 CSS 가 표현하는 세 모양( disc / circle / square) 중 가장 가까운 것으로 그린다
showCursor bool? bool? null 커서 표시 (false → Web caret-transparent)
keyboardType TextInputType? String? null 키보드 타입 (Web 은 inputmode 속성 값)
textInputAction TextInputAction? String? null 키보드 액션 키 (Web 은 enterkeyhint)
textCapitalization TextCapitalization String? none / null 자동 대문자화 (Web 은 autocapitalize)
inputFormatters List<TextInputFormatter>? List<CoreInputFormatter>? null 입력 포매터 (편집 중 적용)
onEditingComplete VoidCallback? CoreVoidCallback? null 편집 완료 콜백 (Web 은 change 이벤트)
onTap VoidCallback? CoreVoidCallback? null 탭 콜백
onTapOutside TapRegionCallback? void Function()? null 외부 포인터-다운 콜백 (Web 은 root id 를 제외한 document pointerdown)
onHeightChanged / onWidthChanged ValueChanged<double>? CoreValueChanged<double>? null 리사이즈 콜백. 드래그 가능한 축( expandableHeight / expandableWidth )에서만 호출된다 — Web 은 ResizeObserver

마스킹된 영역은 양 플랫폼 모두 브라우저/OS 텍스트 서비스에서 빠진다 — Web 은 spellcheck="false" · autocorrect="off" · autocapitalize="none" 를 함께 emit 하고, textCapitalization 을 명시해도 마스킹이 이긴다.

Flutter 전용 확장#

속성타입기본값설명
textDirection TextDirection? null 읽기 방향
cursorOpacityAnimates bool true 커서 깜빡임 애니메이션
maxLengthEnforcement MaxLengthEnforcement? null 길이 제한 적용 방식 (Flutter 런타임 인프라)
enableSuggestions bool true 입력 제안
enableInteractiveSelection bool !readOnly || !obscureText 선택 제스처 허용
enableIMEPersonalizedLearning bool true IME 개인화 학습
stylusHandwritingEnabled bool true 스타일러스 필기 입력
autofillHints Iterable<String>? null 자동완성 힌트
controller TextEditingController? null Flutter 전용 런타임 인프라
focusNode FocusNode? null Flutter 전용 런타임 인프라
scrollController ScrollController? null Flutter 전용 런타임 인프라
scrollPhysics ScrollPhysics? null Flutter 전용 런타임 인프라
selectionControls TextSelectionControls? null Flutter 전용 런타임 인프라
selectionHeightStyle / selectionWidthStyle BoxHeightStyle / BoxWidthStyle tight Flutter 전용 런타임 인프라
dragStartBehavior DragStartBehavior start Flutter 전용 런타임 인프라
clipBehavior Clip hardEdge Flutter 전용 런타임 인프라
restorationId String? null Flutter 전용 런타임 인프라
contextMenuBuilder EditableTextContextMenuBuilder? null Flutter 전용 런타임 인프라
spellCheckConfiguration SpellCheckConfiguration? null Flutter 전용 런타임 인프라
undoController UndoHistoryController? null Flutter 전용 런타임 인프라
keyboardAppearance Brightness? null Flutter 전용 런타임 인프라

Web 은 이 capability 들을 브라우저 native <textarea> · DOM focus/scroll · 네이티브 컨텍스트 메뉴로 제공합니다.

Web 전용 확장#

속성타입기본값설명
cols int? null HTML cols 속성 (행 너비 추정치)

scrollPadding 은 위젯 파라미터입니다 (기본값 CoreTextAreaContract.defaultScrollPadding) — 스크롤-인투-뷰 inset 은 아무것도 그리지 않으므로 chrome 이 아니라 behaviour 이고, 그래서 CoreTextAreaStyle 이 아니라 위젯 평면에 있습니다.

스타일 시스템 — textAreaStyle#

TextArea 의 모든 chrome / dimensional / cursor / nested-slot 오버라이드는 CoreTextAreaStyle 단일 슬롯으로 흐릅니다. multi-line 전용 동작 (rows / expandableHeight / expandableWidth) 과 크기 토큰 (size) 은 위젯 파라미터입니다.

TextArea(
  size: CoreComponentSize.md,                              // ← 크기 토큰 위젯 파라미터
  rows: 4,                                                 // ← multi-line 동작
  expandableHeight: true,                                  // ← multi-line 동작
  onChanged: handleBio,
  textAreaStyle: CoreTextAreaStyle(                        // ← chrome / 슬롯 단일 진입점
    borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
    padding: CoreEdgeInsets.symmetric(
      horizontal: CoreSpace.space16,
      vertical: CoreSpace.space12,
    ),
    minHeight: 96,
    maxHeight: 240,
    focusBorderColor: CoreColor.token(CoreColors.primary),
    valueStyle: CoreTextStyle.token(CoreTextStyles.bodyMedium),
  ),
)

minHeight / maxHeight 는 TextArea 의 auto-grow 제약에 특히 의미가 있습니다.

Resolve chain#

variantStyles 는 위젯의 에러 상태(errorText != null) 를 키로 갖는 Map<bool, CoreTextAreaStyle> 입니다 — 별도 variant enum 은 없습니다.

design system default (defaultNormalVariant / defaultsByVariant[hasError])
  → CoreTextAreaTheme.style
  → CoreTextAreaTheme.variantStyles[hasError]
  → 부모 컴포넌트 슬롯 오버라이드
  → widget.textAreaStyle

CoreTextAreaStyle 필드#

필드타입설명
backgroundColor CoreColor? Background fill colour override.
borderColor CoreColor? Border stroke colour override (idle state).
focusBorderColor CoreColor? Border stroke colour when focused.
errorBorderColor CoreColor? Border stroke colour when in error state.
borderWidth double? Border stroke width override (logical px, pre-scaling).
borderRadius CoreBorderRadius? Border radius override (pre-scaling).
initialHeight double? Initial textarea height (logical px). null defers to [defaultInitialHeight].
initialWidth double? Initial textarea width (logical px). null defers to [defaultInitialWidth] — i.e. fill the available width.
minHeight double? Minimum height constraint when resizable (logical px). null defers to [defaultMinHeight].
minWidth double? Minimum width constraint when resizable (logical px). null defers to [defaultMinWidth].
maxHeight double? Maximum height constraint (logical px). null defers to [defaultMaxHeight] — i.e. unbounded.
maxWidth double? Maximum width constraint (logical px). null defers to [defaultMaxWidth] — i.e. unbounded.
padding CoreEdgeInsets? Padding override (logical px).
labelGapStyle CoreGapStyle? 1-off spacer slot between the label (sibling 1) and the field (sibling 2). Forwarded raw to Gap(gapStyle:) so Gap 's own resolver folds in scaling and design defaults.
descriptionGapStyle CoreGapStyle? 1-off spacer slot between the field (sibling 1) and the description / error helper text (sibling 2). Forwarded raw to Gap(gapStyle:) so Gap 's own resolver folds in scaling and design defaults.
rowSpacing double? Spacing between the prefix / input / suffix row children (mirrors Flutter Row.spacing).
scrollPadding CoreEdgeInsets? Scroll padding around the input when scrolled into view.
dragHandlePadding CoreEdgeInsets? Padding inside the resize drag-handle hit area.
dragHandleOffset double? Offset of the resize drag handle from the bottom-right corner.
dragHandleSize double? Size of the square resize drag-handle hit area.
iconStyle CoreIconStyle? Nested [CoreIconStyle] slot shared by the leading / trailing icon affixes (prefix / suffix). Raw forwarded to IconIcon 's own resolver folds in scaling. null defers to CoreTextAreaStyle.defaultsBySize[size]!.iconStyle! .
transitionDuration Duration? Colour-transition duration override for border / focus states.
textAlign CoreTextAlign? Horizontal text alignment override.
textAlignVertical CoreVerticalAlign? Vertical text alignment override.
cursorColorCoreColor?Cursor colour override.
cursorWidth double? Cursor width override (logical px, pre-scaling).
helperTextColor CoreColor? Helper text colour override. null → [defaultHelperTextColor].
errorTextColor CoreColor? Error text colour override. null → [defaultErrorTextColor].
requiredMarkColor CoreColor? Required-mark (*) colour override. null → [defaultRequiredMarkColor].
dragHandleColor CoreColor? Resize drag-handle glyph colour override. null → [defaultDragHandleColor].
selectionColor CoreColor? Text-selection tint colour override. null → [defaultSelectionColor].
backgroundCursorColor CoreColor? Background-cursor (composition/IME) colour override. null → [defaultBackgroundCursorColor].
cursorRadius CoreBorderRadius? Cursor corner radius override.
focusOutlineStyle CoreFocusOutlineStyle? Focus outline style override. Nested [CoreFocusOutlineStyle] covers ring colour / radius / width / offset / offset-background. null (or any unset field on the slot) defers to [defaultFocusOutlineStyle]. A borderColor stated here outranks [focusBorderColor] and the variant's focus-ring tone; a borderRadius stated here outranks the area's own [borderRadius].
baseTextStyle CoreTextStyle? Base text style applied to the editable text content.
labelStyle CoreTextStyle? Label text style override.
placeholderStyle CoreTextStyle? Placeholder text style override. No default* , deliberately. The placeholder's baseline is already decided in two other places and this slot only deviates from it: the metrics come from the per-size baseTextStyle ( baseRole.merge(merged.placeholderStyle) on Flutter) and the colour comes from the variant table ( CoreTextAreaVariantStyle.placeholderColor ). Web reaches the table through a token test — placeholderStyle?.color?.token != null ? … : '${vs.placeholderColor}' — because ::placeholder only accepts a class. A default carrying a colour token makes that test true for every text area, and the variant table's placeholder colour becomes unreachable; a default carrying no token would satisfy the guard while saying nothing.
valueStyle CoreTextStyle? Selected/typed value text style override. No default* , deliberately. The editable role is emitted as a className by emitTypography , and this slot is the inline overlay on top of it — Web spreads it null-aware ( ...?valueStyle?.toInlineCss ), so an unset slot emits no inline CSS at all. web-specifics.md is explicit that the inline half carries explicitly overridden properties only; a default here would overlay every text area with inline declarations that outrank the role class by specificity, which is the one thing the role class exists to avoid.
descriptionStyle CoreTextStyle? Description / helper text style override.
errorStyle CoreTextStyle? Error message text style override — the helper text under the box, not the error chrome (border / focus ring), which lives in [defaultErrorVariant]. No default* of its own: each resolver merges this override onto a base role it already resolved. Those two bases are not the same role today, which is a divergence this rename makes visible rather than one it introduces — a shared defaultErrorStyle would have to pick one of them and change the other platform's render.

빠른 오버라이드 (Chain)#

이미 만든 TextArea 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 == CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class TextAreaChainExample extends StatefulWidget {
  const TextAreaChainExample({super.key});

  @override
  State<TextAreaChainExample> createState() => _TextAreaChainExampleState();
}

class _TextAreaChainExampleState extends State<TextAreaChainExample> {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        TextArea(placeholder: 'Enter your message...', rows: 4).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderWidth)까지 한 번에.
        TextArea(placeholder: 'Enter your message...', rows: 4).withStyle(
          const CoreTextAreaStyle(
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
    );
  }
}
class TextAreaChainExample extends StatefulComponent {
  const TextAreaChainExample({super.key});

  @override
  State<TextAreaChainExample> createState() => _TextAreaChainExampleState();
}

class _TextAreaChainExampleState extends State<TextAreaChainExample> {
  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        TextArea(placeholder: 'Enter your message...', rows: 4).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderWidth)까지 한 번에.
        TextArea(placeholder: 'Enter your message...', rows: 4).withStyle(
          const CoreTextAreaStyle(
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

테마 커스터마이징 (Theme)#

CoreTextAreaThemestyle + variantStyles 두 슬롯만 가집니다. variantStyles 의 키는 에러 상태 bool 입니다 (trueerrorText != null).

CoreComponentTheme(
  textArea: CoreTextAreaTheme(
    style: CoreTextAreaStyle(
      borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
      padding: CoreEdgeInsets.symmetric(
        horizontal: CoreSpace.space16,
        vertical: CoreSpace.space12,
      ),
    ),
    variantStyles: {
      true: CoreTextAreaStyle(
        focusBorderColor: CoreColor.token(CoreColors.error),
      ),
    },
  ),
)

autocorrect / enableSuggestions 같은 입력 정책 기본값은 Theme 이 아니라 CoreTextAreaContract.defaultXxx 단일 출처에서 옵니다.

Resolve 우선순위: widget.textAreaStyle > CoreTextAreaTheme.variantStyles[hasError]

CoreTextAreaTheme.style > 디자인 시스템 기본값.

동작 스펙 (Behavior)#

인터랙션#

  • 타이핑: 입력 시 onChanged 호출
  • 포커스 이탈/Enter: onSubmitted 호출 (Web은 change 이벤트)
  • 크기 조절:
    • Flutter: expandableHeight/expandableWidth로 드래그 핸들 표시
    • Web: 네이티브 <textarea> 리사이즈 핸들(resize: vertical) 제공
    • 양쪽 모두 드래그 가능한 축에서만 onHeightChanged / onWidthChanged 호출 (Web 은 ResizeObserver, 마운트 첫 보고는 크기 변경이 아니므로 건너뜀)

유효성#

  • maxLength: 초과 입력 차단
  • errorText 존재 시 description 숨김, 에러 chrome(variantStyles[true])으로 자동 전환
  • required: true → 라벨 옆 * 표시

시각적 토큰 통일#

  • 정상 상태(defaultNormalVariant): surface 배경 + outlineVariant 보더 + primary 포커스 링
  • 에러 상태(defaultErrorVariant): surface 배경 + error 보더 + error 포커스 링
  • Flutter/Web 모두 CoreTextAreaVariantStyle을 참조하여 토큰 일관성 유지

사용 가이드라인 (Usage Guidelines)#

✅ Do#

세로 리사이즈를 허용할 때는 textAreaStyleminHeight / maxHeight 로 auto-grow 상하한을 지정

TextArea(
  expandableHeight: true,
  onChanged: handleChange,
  textAreaStyle: const CoreTextAreaStyle(
    initialHeight: 96,
    minHeight: 96,
    maxHeight: 320,
  ),
)

minHeight/maxHeight 는 TextArea 의 auto-grow 제약에 특히 의미가 있습니다 — 지정하지 않으면 드래그로 텍스트 영역이 원치 않는 크기까지 줄어들거나 늘어날 수 있습니다.


❌ Don't#

description 에 항상 봐야 하는 안내를 넣고 errorText 와 동시에 표시된다고 가정하지 않기

// ❌ errorText 가 생기는 순간 description 이 사라져 안내를 잃음
TextArea(
  description: '최소 10자 이상 입력하세요',
  errorText: hasError ? '필수 입력입니다' : null,
)

errorText 가 존재하면 description 은 자동으로 숨겨지고 에러 chrome(variantStyles[true])으로 전환됩니다 — description 에 상시 안내를 넣으면 에러가 뜨는 순간 그 안내를 사용자가 볼 수 없게 됩니다.

접근성 (Accessibility)#

동작
TabTextArea로 포커스
Shift+Tab이전 요소로 포커스
Enter새 줄 삽입
  • Web: <textarea> 네이티브 시맨틱을 그대로 사용합니다.
  • Flutter: 편집 표면의 시맨틱 노드가 활성 상태(enabled)와 SemanticsAction.focus 핸들러를 진술합니다. 웹 엔진은 접근성이 켜지면 포인터 이벤트가 아니라 이 focus 액션으로 편집 세션을 열므로, 그 경로가 이 배선으로 성립합니다.
    • 이 컴포넌트의 노드는 focus 외에 tap 액션도 함께 내놓습니다. 그것은 이 배선이 아니라 영역을 감싸는 탭 제스처에서 오며, 그 핸들러가 포커스를 요청합니다 — TextField 쪽 노드에는 tap 이 없습니다.
    • label:·errorText: 는 시각 표시일 뿐 이 노드에 닿지 않습니다(노드의 label 은 빈 문자열).