TextArea | CoUI
본문으로 건너뛰기
LogoCoUI

TextArea

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

목차

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

기본 사용법 (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),
)

// 대화 입력 — 보내기 버튼은 글자 아래, 박스 안의 footer 줄 끝에
TextArea(
  rows: 1,
  placeholder: 'Message...',
  onChanged: handleChange,
  footer: Row(
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
      Button(size: .sm, onPressed: handleSend, child: const Text('Send')),
    ],
  ),
)
// 기본 텍스트 영역
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,
)

// 대화 입력 — 보내기 버튼은 글자 아래, 박스 안의 footer 줄 끝에
TextArea(
  rows: 1,
  placeholder: 'Message...',
  onChanged: handleChange,
  footer: div(
    [Button(size: .sm, onPressed: handleSend, child: const Text('Send'))],
    classes: 'flex flex-row justify-end',
  ),
)

footer 는 글자 아래, 박스 안에 놓이는 한 줄입니다 — 보내기·첨부처럼 입력한 글에 대해 하는 동작을 둡니다. 무엇을 어떻게 놓을지는 부르는 쪽이 정합니다: footer 는 박스의 내용 너비를 받고 시작 쪽에 놓이므로, 동작을 끝 쪽에 두려면 Row / flex 로 줄을 채웁니다.

Live Preview#

사용 시기 (When to Use)#

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

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

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

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

Props / Parameters#

W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 슬롯 타입입니다. 공통 데이터·상태 파라미터는 한 표로 읽고 실제 입력 인프라 타입만 구분합니다.

속성타입기본값설명
placeholder String? null 빈 상태 안내
initialValue String? null 초기 값
onChanged CoreValueChanged<String>? null 값 변경 콜백
onSubmitted CoreValueChanged<String>? null 제출 콜백 (Web은 change 이벤트)
enabledbooltrue활성화 여부
readOnlyboolfalse읽기 전용
autofocusboolfalse자동 포커스
selectAllOnFocus bool false 포커스 시 전체 선택
size CoreComponentSize md 크기 토큰. xs는 표 아래 안내 참고
labelString?null상단 라벨
labelMaxLines int? null 라벨의 최대 줄 수; 생략하면 제한 없음
labelOverflow CoreTextOverflow? null 라벨 넘침 처리
description String? null 에러가 없을 때의 하단 설명
errorText String? null 에러 메시지
requiredboolfalse필수 입력 표시
nameString?null폼 제출용 이름
maxLengthint?null최대 글자 수
showCounter bool false 설명 줄 오른쪽 끝에 글자 수( n / maxLength , maxLength 가 없으면 n )를 표시. 설명과는 12 떨어집니다( counterGapStyle ). 글자는 그래핌 단위로 셉니다
variant CoreTextAreaVariant defaultVariant defaultVariant / filled (테두리 대신 채움). errorText 가 있고 활성이면 error 가 이깁니다
rowsint?null보이는 행 수
prefix / suffix W? null 입력 영역 좌/우 콘텐츠
footer W? null 글자 아래, 박스 안의 한 줄 — 보내기·첨부 같은 동작. 글자와의 간격은 footerGapStyle (기본 12). 편집 표면이 아니라서 탭하면 그 내용만 반응하고 커서·포커스를 가져가지 않습니다
expandableHeight bool false 세로 리사이즈 가능
expandableWidth bool false 가로 리사이즈 가능
textAreaStyle CoreTextAreaStyle? null chrome / cursor / 중첩 슬롯 단일 진입점

xs 는 이르면 coui_core 0.153 부터 sm 으로 그려집니다 — 대신 sm 을 쓰세요. 그 전까지 xs 는 지금처럼 그려지고, assert 가 켜진 실행(디버그 빌드, Jaspr 정적 빌드의 사전 렌더)에서 로그 경고가 한 번 나옵니다 — 경고일 뿐 화면이나 빌드를 멈추지 않습니다. xs 행이 지워진 뒤에는 sm 으로 그려지고, assert 가 켜진 실행은 xs 에서 멈춥니다.

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 전용 런타임 인프라
magnifierConfiguration TextMagnifierConfiguration? null (→ 플랫폼 확대경) 터치로 선택을 끌 때 보이는 확대경. Web 은 브라우저가 그린다
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 이 아니라 위젯 평면에 있습니다.

스타일 시스템 (Style System)#

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 는 위젯의 에러 상태 를 키로 갖는 Map<bool, CoreTextAreaStyle> 입니다. 우선순위는 disabled > error 입니다 — true 는 errorText != null 이고 영역이 활성일 때만 조회되고, 비활성 영역은 에러가 있어도 박스 chrome 에 false 행을 씁니다. 에러 메시지는 비활성이어도 표시되며, 그 스타일 (errorStyle / errorTextColor)은 계속 true 행에서 읽습니다.

design system default (defaultsByVariant[CoreTextAreaVariant.of(hasError:, enabled:)])
  → CoreTextAreaTheme.style
  → CoreTextAreaTheme.variantStyles[hasError && enabled]
  → 부모 컴포넌트 슬롯 오버라이드
  → widget.textAreaStyle

CoreTextAreaStyle 필드#

필드타입설명
backgroundColor CoreColor? Background fill colour override.
borderColor CoreColor? Border stroke colour override (idle state).
focusBorderColor CoreColor? Border stroke colour when focused.
hoverBorderColor CoreColor? Border stroke colour while the pointer is over the field. null → the variant's own hoverBorderColor ( on-surface on the ordinary area, error on the error one), else the resting borderColor for a variant that states none.
errorBorderColor CoreColor? Border stroke colour when in error state.
borderWidth double? Border stroke width override (logical px, pre-scaling) — the resting boundary, which stays in the layout. null → [defaultBorderWidth].
activeBorderWidth double? Width of the active boundary (logical px, pre-scaling) — a focused area, or one wearing the error variant while enabled ( disabled > error ). null → [defaultActiveBorderWidth]. The resting boundary ([borderWidth]) stays where it is and only the part of this width beyond it is added, over the box and outside the layout: a wider boundary never moves the text or the area's own height. A width at or below [borderWidth] adds nothing, so a caller that states only a thicker [borderWidth] is not thinned on focus. Same name, same reading and same default as CoreTextFieldStyle.activeBorderWidth .
borderRadius CoreBorderRadius? Border radius override (pre-scaling). null → the size row's corner ( defaultsBySize[size].borderRadius , the TextField's for that size).
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.
footerGapStyle CoreGapStyle? 1-off spacer slot between the text row (sibling 1) and the footer (sibling 2), inside the box. Forwarded raw to Gap(gapStyle:) so Gap 's own resolver folds in scaling and design defaults. Read only when the area has a footer.
counterGapStyle CoreGapStyle? 1-off spacer slot between the helper line (sibling 1) and the character count (sibling 2). Forwarded raw to Gap(gapStyle:) . Read only when the area shows a counter.
rowSpacing double? Spacing between the prefix / input / suffix row children (mirrors Flutter Row.spacing).
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 Icon — Icon '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).
labelDisabledColor CoreColor? Disabled-state label colour override, forwarded to the composed FormLabel as its disabledColor . null → [defaultLabelDisabledColor].
helperTextColor CoreColor? Helper text colour override. null → [defaultHelperTextColor].
affixColor CoreColor? Prefix / suffix affix colour override — the glyphs and any affix text, in every state. null → [defaultAffixColor].
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 — forwarded into the composed FormLabel . Deliberately has no default* : TextArea does not own the label's role, CoreFormLabelStyle.defaultsBySize does (the 13px rung on xs / sm / md , the 14px rung on lg and up). A constant here can only name ONE of those rungs, so it would restate one role for every size — which is what the Web resolver read while the Flutter twin derived a body role from its per-size base, rendering the same label at three different roles across Flutter, Web and the canvas. The absence is the design: the role stays with the child, and this slot carries only what a caller states per instance.
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.
counterStyle CoreTextStyle? Character-count text style override (#5747) — the n / max line at the helper row's end while showCounter is on. Merges onto [defaultCounterStyle].
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 — both resolvers merge this override onto [defaultDescriptionStyle], the same size-invariant bodyXs base [descriptionStyle] merges onto (2026-09-11 재검토: Flutter used to merge onto the per-size baseTextStyle instead, a cross-platform divergence rather than a canvas-confirmed per-size ladder — fixed to match Web).

빠른 오버라이드 (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)#

CoreTextAreaTheme 은 style + variantStyles 두 슬롯만 가집니다. variantStyles 의 키는 에러 상태 bool 입니다 (true → errorText != null 이고 활성일 때 — 비활성이면 박스 chrome 은 false 행, 에러 메시지 스타일만 true 행).

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 && enabled]

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

동작 스펙 (Behavior)#

테두리는 쉴 때 outline-variant, 마우스를 올리면 outline, 포커스되면 primary 1.5 입니다. 에러 테두리는 쉴 때와 hover 에서 1 이고 포커스될 때만 1.5 로 두꺼워집니다. 바깥 포커스 링은 키보드로 포커스했을 때만 그립니다 — 마우스·터치로 누른 포커스는 primary 테두리가 이미 말합니다. 비활성 영역는 값과 placeholder 를 모두 disabled-content 로 그립니다.

filled 변형은 테두리 대신 채움으로 경계를 말합니다 — 카드·시트처럼 이미 틀이 있는 면 위에 둘 때 씁니다. 바탕 surface-container, hover surface-container-high, 포커스 primary 1.5 테두리, 비활성 surface-container-low 입니다.

rows 도 높이도 주지 않으면 크기 단계의 값 글줄 4줄 + 위아래 여백으로 열립니다 (md 108). 여백은 네 변이 같습니다(md 12).

인터랙션#

  • 타이핑: 입력 시 onChanged 호출
  • 편집 완료: Flutter의 편집기 action 경로, Web의 값이 바뀐 뒤 change 이벤트에서 onSubmitted 호출. Enter 줄바꿈과 제출은 같은 이벤트가 아닙니다.
  • 크기 조절:
    • Flutter: expandableHeight/expandableWidth로 드래그 핸들 표시
    • Web: 네이티브 <textarea> 리사이즈 핸들(resize: vertical) 제공
    • 양쪽 모두 드래그 가능한 축에서만 onHeightChanged / onWidthChanged 호출 (Web 은 ResizeObserver, 마운트 첫 보고는 크기 변경이 아니므로 건너뜀)
  • footer: 편집 표면은 글자 칸뿐입니다.
    • footer 를 누르면 그 안의 내용만 반응하고, 커서를 놓거나 영역에 포커스를 주지 않습니다. 포인터 모양도 footer 내용의 것입니다.
    • 박스의 포커스 경계는 글자 칸의 포커스만 따릅니다 — Tab 으로 footer 의 버튼에 가면 박스는 쉬는 모양으로 돌아갑니다.
    • 높이: rows 를 주면 그 줄 수와 footer 를 담을 만큼 박스가 자라고, 주지 않으면 고정 높이 박스에서 footer 가 남긴 몫을 글자 칸이 씁니다. Web 은 rows 를 줘도 minHeight(기본 100)를 바닥으로 깔아, 내용이 그보다 낮은 박스(rows: 1 + footer 포함)는 Flutter 보다 높게 그립니다 — 아래 플랫폼 차이 표.
    • 크기 조절 핸들은 박스가 아니라 글자 칸의 모서리(footer 위)에 놓여 footer 끝의 동작과 겹치지 않고, 끌면 footer 를 포함한 박스 전체 크기가 바뀝니다 — 보고되는 높이도 footer 를 포함합니다.
    • 비활성 영역은 footer 안의 내용을 끄지 않습니다. footer 의 버튼은 부르는 쪽이 함께 끕니다(onPressed: null).

유효성#

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

시각적 토큰 통일#

  • 정상 상태(defaultNormalVariant): surface 배경 + outline 보더(hover 는 onSurface) + primary 포커스 링
  • 에러 상태(defaultErrorVariant): surface 배경 + error 보더 + error 포커스 링
  • 포커스와 에러에서는 테두리가 activeBorderWidth(기본 1.5)로 굵어집니다. 쉬는 테두리(borderWidth, 기본 1)는 레이아웃 안에 그대로 두고 늘어난 폭만 안쪽에 덧그리므로(Flutter 는 foregroundDecoration, Web 은 차이만큼의 inset 그림자) 글자와 rows 로 정해지는 박스 높이는 움직이지 않습니다. borderWidth 만 더 굵게 쓰면(예: stroke2) 포커스에서 얇아지지 않고 그 폭을 유지합니다. TextField 와 이름·기본값·읽는 방식이 같습니다.
  • 글자 색은 TextField 와 같은 세 단계입니다. 값은 onSurface, 라벨은 onSurfaceVariant(FormLabel 의 기본 — 영역이 값 색을 라벨 위에 덮지 않습니다), 도움말(helperTextColor)과 앞·뒤 아이콘(affixColor)은 onSurfaceSubtle 입니다. 비활성 라벨(labelDisabledColor)도 onSurfaceSubtle 로, 활성 라벨과 구분되면서 읽을 수 있는 글자 단계에 머뭅니다. 도움말과 붙임은 슬롯이 따로라 한쪽을 바꿔도 다른 쪽은 움직이지 않고, 붙임은 비활성에서도 색이 같습니다.
  • Flutter/Web 모두 CoreTextAreaVariantStyle을 참조하여 토큰 일관성 유지

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

여러 줄 입력과 크기 조절에는 TextArea를 사용합니다. 한 줄 입력과 clear/password 어포던스는 TextField, 편집 없이 선택·복사만 필요하면 SelectableText를 선택합니다.

✅ Do#

세로 리사이즈를 허용할 때는 textAreaStyle 의 minHeight / 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)#

역할#

  • Flutter — 별도 role 플래그를 세우지 않고 EditableText 가 만드는 텍스트 필드 노드를 그대로 쓰며, 그 안쪽에 Semantics(enabled:, onFocus:) 가 병합됩니다. expandableHeight / expandableWidth 로 켜지는 드래그 핸들에는 Semantics 가 없어 보조기술에 아무 역할도 이름도 노출되지 않습니다.
  • Web — 편집 표면이 네이티브 <textarea> 라 여러 줄 textbox 역할을 그대로 받습니다. 이 컴포넌트는 aria-* 를 하나도 내보내지 않고, 보조기술에 닿는 것은 disabled · readonly · required · rows · cols · placeholder · maxlength 같은 네이티브 속성뿐입니다. 화면 우하단의 크기 조절 표시는 pointer-events: none 인 장식용 <div> 이고 역할·이름이 없습니다 — 실제 드래그는 브라우저가 CSS resize 로 제공합니다. 루트 <div> · <label> · 설명/오류 <p> 에는 역할이 없습니다.

키보드#

키FlutterWeb
Tab / Shift+Tab필드로 / 필드에서 포커스 이동같음
Enter 줄바꿈 삽입. 단 rows: 1 이면 한 줄 필드가 되어 줄바꿈 대신 onSubmitted 로 갑니다 줄바꿈 삽입
텍스트 편집 키(화살표 · Home/End · Shift 선택 · 복사/붙여넣기) EditableText 가 주는 기본 편집 단축키 브라우저 <textarea> 네이티브

onSubmitted 는 Enter 가 아니라 다른 시점에 호출됩니다 — Flutter 는 textInputAction 이 지정한 키보드 동작에서, Web 은 편집이 확정되는 change (실질적으로 포커스를 잃는 순간)에서 호출합니다. Escape 는 어느 플랫폼도 처리하지 않습니다. 크기 조절에 대응하는 키는 없습니다(→ 알려진 제약).

스크린 리더#

  • Web: <textarea> 네이티브 시맨틱을 그대로 사용합니다. 접근 가능한 이름을 주는 속성이 하나도 emit 되지 않아 이름이 비어 있습니다 — label: 은 필드에 연결되지 않습니다(→ 알려진 제약).
  • Flutter: 편집 표면의 시맨틱 노드가 활성 상태(enabled)와 SemanticsAction.focus 핸들러를 진술합니다. 웹 엔진은 접근성이 켜지면 포인터 이벤트가 아니라 이 focus 액션으로 편집 세션을 열므로, 그 경로가 이 배선으로 성립합니다.
    • 이 컴포넌트의 노드는 focus 외에 tap 액션도 함께 내놓습니다. 그것은 이 배선이 아니라 아래 포커스 관리 절의 탭 제스처에서 옵니다 — TextField 쪽 노드에는 tap 이 없습니다.
    • label:·errorText: 는 시각 표시일 뿐 이 노드에 닿지 않습니다(노드의 label 은 빈 문자열).
  • 값 변경도 오류 발생도, 크기가 바뀐 결과도 통보되지 않습니다 — live region 이 양 플랫폼 모두 없습니다.

포커스 관리#

  • 진입 — 오버레이가 아니므로 열림에 따른 포커스 이동이 없습니다. autofocus: true 를 주면 최초 빌드에서 필드가 포커스를 가져갑니다(Flutter 는 EditableText.autofocus, Web 은 <textarea autofocus>). 기본값은 false 입니다. Flutter 는 추가로 필드 영역을 탭하면 _focusNode.requestFocus() 를 호출합니다 — 위에서 말한 tap 액션의 출처입니다.
  • 이탈 — 없음. 포커스를 다른 곳으로 되돌리는 코드가 양 플랫폼 모두 없습니다.
  • 트랩 — 없음. Tab 이 필드를 그대로 통과하며, 드래그 핸들과 크기 조절 그립은 어느 플랫폼에서도 tab stop 이 아닙니다. footer 안의 컨트롤은 글자 칸 다음의 tab stop 입니다.
  • selectAllOnFocus: true(기본 false)를 주면 포커스가 들어오는 순간 값 전체가 선택됩니다 — 양 플랫폼 동일합니다.

알려진 제약#

  • Web 은 label: 을 <textarea> 에 연결하지 않습니다. <label> 요소는 그려지지만 for 가 없고 <textarea> 를 감싸지도 않아 라벨이 필드의 이름이 되지 않고, 라벨을 눌러도 포커스가 옮겨가지 않습니다. description: / errorText: 도 형제 <p> 일 뿐 aria-describedby 로 참조되지 않고 오류 상태에 aria-invalid 도 붙지 않습니다 — 이 컴포넌트는 aria-* 를 하나도 내보내지 않으며, 보조기술에 닿는 것은 네이티브 속성으로 나가는 disabled / readonly / required 뿐입니다. 호출부의 attributes 는 루트 <div> 에 실리므로 <textarea> 에 이름을 직접 붙일 방법도 없습니다.
  • expandableHeight / expandableWidth 로 켠 크기 조절은 포인터 전용입니다. Flutter 는 드래그 핸들을 GestureDetector(onPanUpdate:) 로만 처리하고 Web 은 브라우저의 CSS resize 그립에 맡기므로, 키보드로 크기를 바꿀 경로가 양 플랫폼 모두 없습니다. 크기를 고정해야 하면 expandable* 을 끄고 rows: 로 지정하세요.
  • Flutter 드래그 핸들의 히트 영역은 16×16 입니다. 접근성 페이지가 보장하는 최소 24×24(WCAG 2.2 AA)에 미치지 못하고 TouchTarget 으로 감싸지지도 않습니다. textAreaStyle 의 dragHandleSize 로 키울 수 있지만 그려지는 글리프도 함께 커집니다 (Web 은 브라우저 네이티브 그립을 쓰므로 이 값의 대상이 아닙니다).

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
편집 표면EditableText의 다중 줄 입력native <textarea>
외부 텍스트·포커스 제어TextEditingController / FocusNodeDOM 값과 브라우저 focus
편집 완료편집기 action/submit 콜백change 이벤트
리사이즈위젯 드래그 핸들CSS resize grip과 ResizeObserver 통보
rows 를 준 박스의 높이 내용(줄 수 + footer)이 정함 — minHeight 는 드래그 하한에만 쓰임 내용이 정하되 minHeight (기본 100)가 바닥 — 내용이 그보다 낮은 박스( rows: 1 + footer 포함)는 Flutter 보다 높음
포매터TextInputFormatterCoreInputFormatter
마스킹편집기 obscureText-webkit-text-security의 지원 모양
고급 편집 UIselectionControls·확대경·undoController 등브라우저 native 텍스트 서비스

리사이즈 콜백의 폭·높이는 논리 px/CSS px 단위입니다. 관찰기는 최초 크기 보고를 변경 이벤트로 통보하지 않으며 허용한 리사이즈 축만 전달합니다.