TextField | CoUI
LogoCoUI

TextField

텍스트 입력 필드 컴포넌트

TextField#

사용자로부터 텍스트를 입력받는 컴포넌트입니다. 다양한 타입과 상태를 지원합니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 사용자로부터 짧은 텍스트를 입력받을 때 (이름, 이메일, 비밀번호 등)
  • 검색 필드가 필요할 때
  • 숫자, 전화번호 등 특정 형식의 입력이 필요할 때

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

  • TextArea: 여러 줄의 긴 텍스트를 입력받을 때
  • Select: 미리 정해진 옵션 중 선택할 때
  • Autocomplete: 입력하면서 추천 목록에서 선택할 때
  • DatePicker: 날짜를 입력받을 때

기본 사용법 (Basic Usage)#

// 기본 입력
TextField(
  onChanged: handleNameChange,
  placeholder: Text('이름을 입력하세요'),
)

// 비밀번호 입력
TextField(
  obscureText: true,
  placeholder: Text('비밀번호'),
)

// 라벨 및 에러
TextField(
  label: '이메일',
  onChanged: handleEmailChange,
  errorText: '올바른 이메일을 입력하세요',
)
// 기본 입력
TextField(
  onChanged: handleNameChange,
  placeholder: Text('이름을 입력하세요'),
)

// 비밀번호 입력
TextField(
  obscureText: true,
  placeholder: Text('비밀번호'),
)

// 라벨 + 에러
TextField(
  label: '이메일',
  onChanged: handleEmailChange,
  errorText: '올바른 이메일을 입력하세요',
)

Props / Parameters#

콘텐츠 / 상태#

속성타입기본값설명
placeholder Widget? / Component? null 플레이스홀더
initialValue String? null 초기 값 (비제어)
valueString?null제어 값
onChanged ValueChanged<String>? null 값 변경 콜백
onSubmitted ValueChanged<String>? null 제출 콜백
enabledbooltrue활성화 여부
readOnlyboolfalse읽기 전용
autofocusboolfalse자동 포커스
selectAllOnFocus bool false 포커스 시 전체 선택
size CoreComponentSize md 크기 토큰 (위젯 파라미터)
textFieldStyle CoreTextFieldStyle? null chrome / dimensional / cursor / nested slot 단일 진입점

라벨 / 검증#

속성타입기본값설명
labelString?null라벨 텍스트
description String? null 설명 텍스트
errorText String? null 에러 메시지 (설정 시 자동 error variant)
requiredboolfalse필수 입력 표시
name String? null 폼 제출용 필드 이름
hintText String? null 네이티브 힌트 텍스트
title String? null HTML title (검증 실패 안내)
patternString?null검증 정규식
minLength / maxLength int? null 최소·최대 글자 수
min / max num? null 숫자 입력 범위

텍스트 동작#

속성타입기본값설명
type CoreInputType CoreInputType.defaultType HTML-semantic 입력 타입
obscureText bool false 비밀번호 마스킹
maxLines int? 1 (Flutter) / null (Web) 최대 줄 수
minLinesint?null최소 줄 수
expands bool false 부모 높이만큼 확장
autocorrect bool? null (→ true) 자동 교정
keyboardType TextInputType? / String? null 키보드 타입 (Web → inputmode)
textInputAction TextInputAction? / String? null 키보드 액션 키 (Web → enterkeyhint)
textCapitalization TextCapitalization / String? none / null 자동 대문자화
textDirection TextDirection? / String? null 읽기 방향
autofillHints Iterable<String> / Iterable<String>? const [] / null 자동완성 힌트 (Web → autocomplete)
inputFormatters List<TextInputFormatter>? / List<CoreInputFormatter>? null 입력 포매터
submitFormatters동일 타입const []제출 시점 포매터
showCursorbool?null커서 표시

슬롯 / 어포던스 / 콜백#

속성타입기본값설명
prefix / suffix Widget? / Component? null 입력 영역 좌/우 슬롯. 아이콘도 글자(단위·카운트·타이머)도 받는다 — 글자는 필드의 사이즈별 base 타이포를 affixColor 로 입는다
features List<CoreInputFeature> const [] clear / password-toggle 어포던스
clearButtonSemanticLabel String? null clear 버튼 접근성 이름
onTap VoidCallback? null 탭 콜백
onEditingComplete VoidCallback? null 편집 완료 콜백
onTapOutside / onTapUpOutside TapRegionCallback? / void Function()? null 외부 탭 콜백
focusNode FocusNode? / CoreFocusNode? null 포커스 노드
statesController WidgetStatesController? / CoreStatesController? null 상태 컨트롤러

Flutter 전용 확장#

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

속성타입기본값설명
controller TextEditingController? null 텍스트 컨트롤러
obscuringCharacter String '•' 마스킹 문자
cursorOpacityAnimates bool true 커서 깜빡임 애니메이션
enableSuggestions bool true 입력 제안
enableInteractiveSelection bool !readOnly || !obscureText 선택 제스처 허용
enableIMEPersonalizedLearning bool true IME 개인화 학습
stylusHandwritingEnabled bool EditableText.defaultStylusHandwritingEnabled 스타일러스 필기 입력
maxLengthEnforcement MaxLengthEnforcement? null 길이 제한 적용 방식
keyboardAppearance Brightness? null 키보드 밝기
scrollController ScrollController? null 스크롤 컨트롤러
scrollPhysics ScrollPhysics? null 스크롤 물리
selectionControls TextSelectionControls? null 선택 핸들 UI
selectionHeightStyle / selectionWidthStyle BoxHeightStyle / BoxWidthStyle tight 선택 영역 박스
dragStartBehavior DragStartBehavior start 드래그 시작 기준
clipBehavior Clip hardEdge 클리핑
restorationId String? null 상태 복원 ID
contentInsertionConfiguration ContentInsertionConfiguration? null 리치 콘텐츠 삽입
contextMenuBuilder EditableTextContextMenuBuilder? null 컨텍스트 메뉴
magnifierConfiguration TextMagnifierConfiguration? null 확대경
spellCheckConfiguration SpellCheckConfiguration? null 스펠 체크
undoController UndoHistoryController? null undo 히스토리
strutStyle StrutStyle? null strut 메트릭
skipInputFeatureFocusTraversal bool true 어포던스 버튼 포커스 순회 제외
groupId Object EditableText tap-region 그룹 ID

빠른 오버라이드 (Chain)#

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

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

  @override
  State<TextFieldChainExample> createState() => _TextFieldChainExampleState();
}

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

  @override
  State<TextFieldChainExample> createState() => _TextFieldChainExampleState();
}

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

스타일 시스템 — textFieldStyle#

TextField 의 chrome / dimensional / cursor / 슬롯 텍스트 미세 조정은 단일 textFieldStyle 필드(CoreTextFieldStyle) 하나로 흐릅니다.

TextField(
  size: CoreComponentSize.md,                              // ← 크기 토큰 위젯 파라미터
  onChanged: handleEmailChange,
  textFieldStyle: CoreTextFieldStyle(                      // ← chrome / 슬롯 단일 진입점
    borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
    padding: CoreEdgeInsets.symmetric(
      horizontal: CoreSpace.space16,
      vertical: CoreSpace.space12,
    ),
    focusBorderColor: CoreColor.token(CoreColors.primary),
    cursorColor: CoreColor.token(CoreColors.primary),
    valueStyle: CoreTextStyle.token(CoreTextStyles.bodyMedium),
    placeholderStyle: CoreTextStyle.token(
      CoreTextStyles.bodyMedium,
      color: CoreColor.token(CoreColors.onSurfaceVariant),
    ),
    leadingIconStyle: CoreIconStyle(size: CoreSize.size16),
  ),
  prefix: Icon(LucideIcons.mail),
  label: '이메일',
)

CoreTextFieldStyle 필드#

필드타입설명
backgroundColor CoreColor? Background fill colour override.
borderColor CoreColor? Border stroke colour override (idle state).
focusBorderColor CoreColor? Border stroke colour when the field is focused.
errorBorderColor CoreColor? Border stroke colour when the field is in error state.
borderWidth double? Border stroke width override (logical px, pre-scaling).
borderRadius CoreBorderRadius? Border radius override (pre-scaling).
height double? Fixed height override (logical px).
minHeight double? Minimum height constraint (logical px). No default* , deliberately: the field's resting height is the per-size height in [defaultsBySize], and these two are caps a caller adds ON TOP of it (a multiline / auto-growing field). Web emits min-height only inside if (merged.minHeight != null) , so a default would put a floor on every text field at every size, and the size table's height would stop being what decides the box.
maxHeight double? Maximum height constraint (logical px). No default* — same null test as [minHeight] (Web's if (merged.maxHeight != null) ); a value here caps every field.
padding CoreEdgeInsets? Padding override (logical px).
labelPadding CoreEdgeInsets? Padding applied to the label element. null defers to [defaultLabelPadding].
descriptionPadding CoreEdgeInsets? Padding applied to the description / error helper text element. null defers to [defaultDescriptionPadding].
rowSpacing double? Native Row.spacing between the prefix / input / suffix row children (logical px). null defers to [defaultRowSpacing].
scrollPadding CoreEdgeInsets? Scroll padding around the input when scrolled into view. null defers to [defaultScrollPadding].
iconStyle CoreIconStyle? Nested [CoreIconStyle] slot shared as the per-size base for the leading / trailing icon affixes. Raw forwarded to the affix Icon resolvers — Icon 's own resolver folds in scaling. null defers to CoreTextFieldStyle.defaultsBySize[size]!.iconStyle! .
fileLabelFontWeight int? Font weight applied to the file-upload <input type="file"> label segment. null defers to [defaultFileLabelFontWeight].
transitionDuration Duration? Chrome transition duration override. null defers to [defaultTransitionDuration].
textAlign CoreTextAlign? Horizontal text alignment override. null defers to [defaultTextAlign].
textAlignVertical CoreVerticalAlign? Vertical alignment of the text within the input. null defers to [defaultTextAlignVertical].
crossAxisAlignment CoreVerticalAlign? Vertical alignment override for the prefix / input / suffix row. null defers to [defaultCrossAxisAlignment].
cursorColorCoreColor?Cursor colour override.
cursorWidth double? Cursor width override (logical px).
cursorRadius CoreBorderRadius? Cursor corner radius override.
labelDisabledColor CoreColor? Disabled-state label text colour override. null → [defaultLabelDisabledColor].
descriptionColor CoreColor? Description / helper text colour override. null → [defaultDescriptionColor].
affixColor CoreColor? Prefix / suffix affix glyph colour override. null → [defaultAffixColor].
errorColor CoreColor? Error helper-text colour override. null → [defaultErrorColor].
selectionColor CoreColor? Text-selection tint colour override. null → [defaultSelectionColor].
backgroundCursorColor CoreColor? Background-cursor (composition/IME) colour override. null → [defaultBackgroundCursorColor].
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 field's own [borderRadius].
baseTextStyle CoreTextStyle? Base text style applied to every text slot (label / placeholder / value / description / error) before each slot's own style override merges on top. null defers to CoreTextFieldStyle.defaultsBySize[size]!.baseTextStyle! (size-aware typography token — bodySmall for xs/sm/md, bodyMedium for lg/xl).
labelStyle CoreTextStyle? Label text style override.
placeholderStyle CoreTextStyle? Placeholder text style override.
valueStyle CoreTextStyle? Input value text style override.
descriptionStyle CoreTextStyle? Description / helper text style override.
errorStyle CoreTextStyle? Error text style override.
leadingIconStyle CoreIconStyle? Leading icon style override. No default* , and one would be actively wrong: the base for both affix icons already lives in Core as defaultsBySize[size].iconStyle , and these two are per-affix overrides layered on it. Both resolvers pick the icon size by precedence — text merged.leadingIconStyle?.size ?? merged.trailingIconStyle?.size ?? merged.iconStyle?.size ?? defaultsBySize[size].iconStyle.size — so a static const sitting at the head of that chain would win over the shared [iconStyle] slot AND over the size table, pinning one icon size across every size tier. Being unset is what makes the two lower rungs reachable.
trailingIconStyle CoreIconStyle? Trailing icon style override. No default* — second rung of the precedence chain documented on [leadingIconStyle]; a value here would shadow the shared [iconStyle] slot and the size table.
featureButtonStyle CoreButtonStyle? Nested [CoreButtonStyle] slot for the input-feature affordance buttons (clear / password toggle) — raw-forwarded to Button(variant:.plain, buttonStyle: …) on both platforms. Recursively merged onto [defaultFeatureButtonStyle] (child-component-composition).

Resolve chain#

variant 는 위젯 파라미터가 아닙니다 — errorText 유무로 CoreTextFieldVariant 가 결정되고(defaultVariant / error), 그 키로 variantStyles 가 조회됩니다.

design system default for variant (defaultsByVariant[variant] / defaultsBySize[size])
  → CoreTextFieldTheme.style                       // 프로젝트 공통
  → CoreTextFieldTheme.variantStyles[variant]      // 에러 상태에서 파생된 variant 별
  → 부모 컴포넌트 슬롯 오버라이드
  → widget.textFieldStyle                          // 인스턴스별

nested slot styles 는 재귀 머지 — valueStyle.fontWeight 만 override 해도 valueStyle.fontSize / valueStyle.color 는 이전 레이어 값 유지.

변형 (Variants)#

variant는 errorText 파라미터로 자동 결정됩니다:

  • errorText가 null이면 → 기본 스타일
  • errorText가 설정되면 → 에러 스타일 (빨간 보더)
// 기본 상태
TextField(
  placeholder: Text('이름을 입력하세요'),
)

// 에러 상태 (errorText 설정 시 자동)
TextField(
  errorText: '필수 입력 항목입니다',
  placeholder: Text('이름을 입력하세요'),
)

// 비활성화
TextField(
  enabled: false,
  placeholder: Text('수정 불가'),
)

// 읽기 전용
TextField(
  readOnly: true,
  initialValue: '읽기 전용 값',
)

동작 스펙 (Behavior)#

포커스 관리#

  • 클릭 또는 Tab 키로 포커스 획득 시 테두리 색상 변경
  • autofocus: true로 페이지 로드 시 자동 포커스

상태 전환#

  • defaultfocused (포커스 획득) → filled (값 입력) → default (포커스 해제)
  • error: errorText 설정 시 테두리와 에러 메시지 표시
  • disabled: 모든 인터랙션 비활성화, 흐리게 표시
  • readOnly: 포커스는 가능하나 편집 불가

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

✅ Do#

항상 라벨과 함께 사용하세요.

TextField(
  label: '이메일 주소',
  placeholder: Text('user@example.com'),
)

라벨은 입력 필드의 용도를 명확히 하고, 접근성을 향상시킵니다.


❌ Don't#

placeholder만으로 라벨을 대체하지 마세요.

TextField(
  placeholder: Text('이메일 주소'),
)

입력을 시작하면 placeholder가 사라져 필드 용도를 알 수 없게 됩니다.

✅ Do#

에러 메시지는 구체적으로 작성하세요.

TextField(
  errorText: '이메일 형식이 올바르지 않습니다 (예: user@example.com)',
)

사용자가 무엇을 수정해야 하는지 바로 알 수 있습니다.


❌ Don't#

모호한 에러 메시지를 사용하지 마세요.

TextField(
  errorText: '입력이 올바르지 않습니다',
)

사용자가 무엇을 고쳐야 하는지 알 수 없습니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Tab다음 입력 필드로 이동
Shift+Tab이전 입력 필드로 이동
Enter폼 제출 (단일 라인) 또는 줄바꿈 (다중 라인)
Escape포커스 해제

스크린 리더#

  • Flutter: 편집 표면의 시맨틱 노드가 활성 상태(enabled)와 SemanticsAction.focus 핸들러를 진술합니다. 웹 엔진은 접근성이 켜지면 포인터 이벤트가 아니라 이 focus 액션으로 편집 세션을 열므로, 그 경로가 이 배선으로 성립합니다. 도달 범위는 여기까지입니다.
    • 노드가 내놓는 액션은 focus 하나이고 tap 은 없습니다. 따라서 모바일 보조기술의 활성화 제스처가 보내는 SemanticsAction.tap커버되지 않습니다.
    • label:·errorText: 는 시각 표시일 뿐 이 노드에 닿지 않습니다 — 노드의 label 은 빈 문자열입니다.
  • Web: <input> 요소의 네이티브 접근성. label 속성으로 접근성 라벨 자동 연결

터치 타겟#

  • 최소 터치 타겟 높이는 24 (CoreTouchTarget.minimum) 이며, 실제 필드는 그보다 크게 그립니다.
  • 충분한 내부 패딩으로 터치 영역 확보
  • Form: 여러 입력 필드를 묶어 유효성 검증 수행
  • Select: 미리 정해진 옵션 중 선택
  • TextArea: 여러 줄 텍스트 입력