Field
공유 외형입니다. Field.Root는 컴파운드 스코프입니다. 테두리가 있는 셸이 Root입니다. 값은 Field.TextInput, Field.Textarea, Field.ChipInput에 있습니다. 장식 요소는 순서로만 위치가 정해집니다. Icon / Affix를 입력 앞에 두면 앞쪽에, 뒤에 두면 뒤쪽에 놓입니다. side prop은 없습니다. Label은 컴파운드 파트가 아니라 형제 요소입니다. 필드 안의 모든 파트(입력, Icon, Affix, 칩)는 필드 한 줄 높이이고, 셸의 패딩이 안쪽 여백입니다.
Label
Label은 컴파운드 파트가 아니라 형제 요소입니다. Field.Root에 넘기는 것과 같은 id를 받습니다. 둘을 하나의 .labelled-field로 감싸세요. 간격은 field.label.gap입니다. API는 Label을 보세요.
const id = useId()
<div className="labelled-field">
<Label htmlFor={id}>Email</Label>
<Field.Root id={id}>
<Field.TextInput value={v} onChange={(event) => setV(event.target.value)} />
</Field.Root>
</div>
Label은 Icon, Affix, ChipInput과 함께 동작합니다.
Affix는 컨트롤이 아니라 텍스트입니다. 장식용 Icon은 aria-hidden입니다. 인터랙티브 Icon은 이름을 유지합니다. 지우기는 내장 prop이 아닙니다.
메시지
Description과 ErrorMessage는 셸 아래에 형제 요소로 놓입니다. Field 파트가 아닙니다. 각각 margin-block-start를 가집니다. aria-describedby는 Field.Root에 두세요. Root가 그 값을 입력에 복사합니다. Field.TextInput에는 설정하지 마세요.
const describedBy = invalid ? `${hintId} ${errorId}` : hintId
<div className="labelled-field">
<Label htmlFor={id}>Email</Label>
<Field.Root id={id} invalid={invalid} aria-describedby={describedBy}>
<Field.TextInput value={email} onChange={(event) => setEmail(event.target.value)} />
</Field.Root>
<Description id={hintId}>Use your work address.</Description>
{invalid ? <ErrorMessage id={errorId}>{error}</ErrorMessage> : null}
</div>
Textarea
여러 줄 텍스트입니다. 높이는 CSS로만 정해집니다. rows는 높이와 드래그 최솟값을, maxRows는 상한을 정합니다. autoGrow는 브라우저가 지원하면 field-sizing: content를 쓰고, maxRows를 생략하면 상한 없이 늘어납니다. field-sizing이 없는 브라우저에서는 rows에 머물고 안에서 스크롤됩니다. autoGrow가 켜져 있을 때는 인라인 height를 설정하지 마세요. 늘어나는 동작을 덮어씁니다. 패딩은 셸 안쪽의 textarea에 있습니다.
autoGrow와 maxRows={6}.
rows 없이 maxRows만 주면 min(3, maxRows)에서 시작합니다.
autoGrow를 끈 상태에서 드래그로 크기를 바꾸면 rows와 maxRows에서 멈춥니다.
compact, comfortable, spacious.
네이티브 스크롤바를 위한 다크 appearance.
레이블, 설명, 오류, invalid, disabled.
Counter
Field.Counter는 Field.TextInput이나 Field.Textarea의 실시간 글자 수를 보여 줍니다. 자체 줄을 차지합니다. 서버 HTML에 글자 수가 포함되도록 입력 뒤에 쓰세요. Field.Root마다 Counter는 하나입니다. 입력은 계속 제어 상태이며, defaultValue는 읽지 않습니다.
같은 줄에 두려면 Counter에 flex-basis: auto를 설정하고, 여전히 마지막에 쓰세요. textarea에서는 그 인라인 글자 수가 마지막 줄에 놓입니다. 각 쌍 아래의 빈 필드는 높이 확인용입니다.
좁은 필드입니다. 줄보다 넓은 인라인 글자 수는 입력 아래로 줄바꿈될 수 있습니다.
입력 앞에 쓴 Counter도 맨 아래 줄에 놓입니다. 글자 수가 서버 HTML에 들어가도록 입력 뒤에 쓰는 편이 좋습니다.
maxLength를 넘으면 글자 수가 오류 색상을 씁니다. 셸은 그대로입니다.
maxLength가 없으면 글자 수는 숫자만 표시됩니다.
maxLength가 있으면 Counter는 스크린 리더에 한도를 알리고, 남은 글자가 20자 이하가 되면 몇 자 남았는지도 알립니다. 이 텍스트는 StringsProvider의 characterLimit, charactersLeft, characterLimitReached 문자열에서 오며, 설정하지 않으면 영어입니다.
Chip input
제어되는 칩과 입력입니다. 필드는 칩을 직접 추가하지 않습니다. 부모의 onCommit이 결정합니다. 장식 요소는 칩 흐름 밖, Field.Root에 남습니다. ChipInput 안의 칩은 모든 밀도에서 입력과 같은 필드 한 줄 높이입니다. chipLayout="row"는 ChipInput 안에서 칩을 입력 위의 전체 너비 줄에 놓습니다. Icon과 Affix는 첫 줄에 놓입니다. 칩이 없으면 Field.TextInput과 같은 높이의 한 줄 필드입니다. row에서 maxRows는 칩 줄만 세고, 인라인 흐름처럼 잘라 냅니다. Label htmlFor나 Root의 aria-label로 레이블을 붙이세요.
maxRows가 설정되면 흐름은 첫 페인트부터 토큰에서 계산한 줄 수로 제한되고, 첫 레이아웃 측정 전까지 칩을 숨깁니다. 그래서 SSR이 +N으로 접히기 전에 펼쳐지지 않습니다. 보이는 영역은 앞쪽 칩들입니다. 가장 새 칩은 +N으로 접히고 실제 흐름에서 언마운트됩니다. 이후 칩이 전체 너비로 들어가지 않으면 +N으로 옮겨집니다. 첫 칩과 +N, 입력이 한 줄을 함께 쓸 수 없으면 첫 칩은 말줄임표로 잘립니다. 접근 가능한 이름은 전체 레이블을 유지합니다.
칩 제거 버튼은 chipRemoveIcon을 전달하지 않으면 X 모양을 그립니다. Chip과 같습니다.
이번 단계에서 미룬 것: 내장 지우기, 붙여넣기 분할, 클릭 가능한 +N, 칩 화살표 키 로빙.
선택 사항인 useChipField는 칩과 입력을 보관하고, 그 getChipInputProps()를 Field.ChipInput에 펼쳐 넘깁니다.
commitKeys={['Enter', ',']}. 알 수 없는 키는 버려집니다.
compact와 comfortable에서의 maxRows={1}과 {2}.
disabled와 invalid는 Field.Root에 있습니다.
Row
Field.Row는 전체 너비 줄입니다. 입력 앞에 쓰면 위에, 뒤에 쓰면 아래에 놓입니다. 필드에 아래쪽 줄이 있으면 Field.Counter를 Row 안에 두세요. 작성기에는 autoGrow를 쓰세요. Row에 텍스트를 그대로 넣지 말고 Field.Affix로 감싸세요. 뒤쪽 항목은 그 첫 항목에 style={{ marginInlineStart: 'auto' }}를 주어 끝으로 밀어 냅니다. 칩에는 chipLayout="row"를 설정하세요. ChipInput이 자체 칩 줄을 그립니다. Field.Row 안에서 칩을 직접 만들지 마세요.
compact, comfortable, spacious에서의 같은 작성기.
숫자, 날짜, 시간
Field.TextInput은 type="number", "date", "time", "datetime-local"을 받습니다. 브라우저 자체의 스피너, 날짜 선택기, 로캘 형식을 그대로 쓰는 네이티브 입력으로 남으며, 필드 토큰으로 스타일을 입히고 숫자를 세로로 맞춥니다. value는 입력의 문자열입니다. "2", "2026-10-07", "14:30"이거나, 입력이 덜 끝났을 때는 빈 문자열입니다. 네이티브 min, max, step으로 제한하세요.
스테퍼 버튼과 달력 그리드는 앱에서 조합할 부분입니다. 네이티브 입력으로 부족할 때 이 입력을 중심으로 만드세요.
Select
Field.Select는 네이티브 <select>입니다. appearance: base-select를 지원하는 브라우저에서는 버튼과 선택 창을 field, option 토큰으로 그립니다. 그 밖의 브라우저에서는 같은 값과 키보드 동작으로 브라우저 자체 선택 창이 나타납니다. JavaScript listbox는 없습니다. 자식은 네이티브 <option>과 <optgroup> 요소이며, 다른 필드처럼 Label htmlFor로 이름을 붙입니다.
<Label htmlFor={id}>Size</Label>
<Field.Root id={id}>
<Field.Select value={size} onChange={(event) => setSize(event.target.value)}>
<option value="s">Small</option>
<option value="m">Medium</option>
</Field.Select>
</Field.Root>
커스텀 컨트롤
별점이나 색상 선택처럼 직접 만든 컨트롤은 useField로 Field.Root에 연결됩니다.
언제 쓰나요
Field.Root
언제: 텍스트, textarea, 칩 입력 하나를 위한 공유 필드 외형 — 포커스 링, disabled/invalid, 밀도. 값은 입력 파트에 있습니다.
쓰지 않을 때: 선택 컨트롤(Checkbox / Radio / Switch). 버튼인 오버레이 트리거. 안에 다른 컴파운드 루트를 중첩할 때.
Field.TextInput
언제: Field.Root 안에서 한 줄로 입력하는 값(text / search / email / url / tel / password), 또는 네이티브 숫자, 날짜, 시간.
쓰지 않을 때: 여러 값의 토큰 → Field.ChipInput. 여러 줄 → Field.Textarea. Root 없이 단독으로 쓸 때.
Field.Textarea
언제: 메모, 메시지, 설명 같은 여러 줄 자유 텍스트. rows가 시작 높이를 정하고, autoGrow는 CSS만으로 maxRows까지 늘립니다.
쓰지 않을 때: 한 줄 값에는 Field.TextInput을 쓰세요. 개별 태그나 토큰에는 Field.ChipInput을 쓰세요. 서식 있는 텍스트나 서식 지정은 제공하지 않습니다.
Field.ChipInput
언제: Field.Root 안의 제어되는 칩 + 입력 하나. 부모가 onCommit으로 확정/추가를 담당하거나, useChipField의 getChipInputProps()를 펼쳐 그 상태를 보관합니다.
쓰지 않을 때: 자유 형식의 단일 문자열 → TextInput. 목록에서 고르는 옵션 → Combobox 레시피 + OptionList.
Field.Select
언제: 폼 안에서 짧고 고정된 목록 중 값 하나를 네이티브 선택 창과 키보드로 고를 때.
쓰지 않을 때: 긴 목록을 필터링하거나 검색 → Combobox 레시피. 여러 값 → Checkbox 또는 Field.ChipInput. 값이 아닌 명령 → Menu.
Field.Icon
언제: Root 안의 앞쪽/뒤쪽 아이콘이나 아이콘 컨트롤. 위치 = DOM 순서. 장식용 → interactive={false}(aria-hidden).
쓰지 않을 때: 텍스트 장식 → Affix. 클릭 가능한 지우기/보기는 전용 Action으로(제공하지 않음).
Field.Row
언제: 필드 안에서 파트를 자체 전체 너비 줄로 묶을 때. 예를 들어 Field.ChipInput 위의 칩, 또는 Field.Textarea 아래의 첨부, Field.Counter, 보내기.
쓰지 않을 때: 입력 옆의 Icon이나 Affix 하나는 Row 없이 인라인으로 구성하고, 별개의 필드를 배치하는 데 Row를 쓰지 마세요.
Field.Affix
언제: Root 안의 정적 텍스트 장식(통화, 단위). 순서 = 위치.
쓰지 않을 때: 아이콘 → Icon. 인터랙티브 컨트롤 — Affix는 클릭할 수 없습니다.
Field.Counter
언제: Field.TextInput이나 Field.Textarea의 실시간 글자 수를 보여 줄 때. maxLength가 설정되면 n / max로, 그렇지 않으면 n으로 표시합니다.
쓰지 않을 때: 글자 수를 보이지 않는 엄격한 제한에는 maxLength만 설정하고, 코드나 PIN 같은 짧은 고정 길이 값에는 쓰지 마세요.
Props
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| disabled | boolean | 아니요 | false | 입력을 비활성화하고 셸도 비활성 상태로 표시합니다. |
| invalid | boolean | 아니요 | false | 셸을 유효하지 않음으로 표시하고 입력에 aria-invalid를 설정합니다. |
| id | string | 아니요 | — | 입력 id. 생략하면 자동 생성됩니다. |
| aria-describedby | string | 아니요 | — | Description과 ErrorMessage의 id. Root에 넣습니다. TextInput은 입력 수준의 aria를 덮어씁니다. |
| name | string | 아니요 | — | 네이티브 입력 name. |
| ref | Ref<HTMLDivElement> | 아니요 | — | 셸 요소. Popover의 anchorRef로 씁니다. |
| children | React.ReactNode | 아니요 | — | TextInput 또는 ChipInput과 선택적인 Icon, Affix. 순서에 따라 앞쪽 또는 뒤쪽에 놓입니다. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| value | string | 예 | — | 제어 값. |
| onChange | (event: ChangeEvent<HTMLInputElement>) => void | 예 | — | 네이티브 change 핸들러. |
| onValueChange | (value: string) => void | 아니요 | — | 선택적 값 콜백. |
| placeholder | string | 아니요 | — | 네이티브 placeholder. |
| type | "text" | "search" | "email" | "url" | "tel" | "password" | "number" | "date" | "time" | "datetime-local" | 아니요 | "text" | 네이티브 입력 type. number, date, time은 네이티브 스피너와 선택기를 그대로 쓰며, value는 "2026-10-07"처럼 입력 문자열 그대로입니다. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| value | string | 예 | — | 제어 값. |
| onChange | (event: ChangeEvent<HTMLTextAreaElement>) => void | 예 | — | 네이티브 change 핸들러. |
| onValueChange | (value: string) => void | 아니요 | — | 선택적 값 콜백. |
| placeholder | string | 아니요 | — | 네이티브 placeholder. |
| rows | number | 아니요 | "3" | 행 단위 시작 높이. 네이티브 rows 속성이자 드래그 최솟값이기도 합니다. |
| maxRows | number | 아니요 | — | autoGrow와 드래그 크기 조절의 상한. 생략하면 상한이 없습니다. |
| autoGrow | boolean | 아니요 | "false" | CSS만으로 내용에 맞춰 maxRows까지 늘어납니다. maxRows가 없으면 상한이 없습니다. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| chips | string[] | 예 | — | 제어 칩 레이블. |
| inputValue | string | 예 | — | 제어 입력 텍스트. |
| onInputValueChange | (value: string) => void | 예 | — | 입력 텍스트 변경. |
| onCommit | (value: string) => void | 예 | — | 확정 키를 누르고 공백을 제거한 값이 비어 있지 않을 때 호출됩니다. 칩을 추가하지는 않습니다. |
| onChipRemove | (index: number, value: string) => void | 예 | — | 제거 버튼 클릭 또는 빈 입력에서 Backspace. |
| chipRemoveLabel | (value: string) => string | 아니요 | — | 제거 버튼의 이름. 기본값은 StringsProvider의 removeChip 문자열이며, 영어로는 Remove {value}입니다. |
| chipRemoveIcon | React.ReactNode | 아니요 | — | 칩 제거 버튼의 내용. 예: 직접 사용하는 아이콘 세트의 아이콘. 없으면 CSS가 X 모양을 그립니다. |
| overflowLabel | (n: number) => string | 아니요 | — | +N의 이름. 기본값은 StringsProvider의 moreChips 문자열이며, 영어로는 {n} more입니다. |
| commitKeys | ReadonlyArray<"Enter" | "Tab" | "," | ";" | " "> | 아니요 | ["Enter"] | 확정 키로 쓸 수 있는 키의 고정 목록. |
| maxRows | number | 아니요 | — | 보이는 줄 수를 제한합니다. inline: 칩과 입력을 함께 셉니다. row: 칩 줄만 세며, 입력은 자기 줄을 유지합니다. 가장 최근 칩부터 +N으로 접힙니다. 전체 너비로 들어가지 않는 칩은 +N으로 옮겨집니다. 줄보다 넓은 칩만 잘립니다. |
| chipLayout | "inline" | "row" | 아니요 | "inline" | inline: 칩이 입력 옆으로 흐릅니다. row: 칩이 ChipInput 안에서 입력 위의 전체 너비 줄에 놓이며, Icon과 Affix는 첫 줄에 놓입니다. 칩이 없으면 TextInput 높이의 한 줄 필드로 렌더링됩니다. |
| removeOnBackspace | boolean | 아니요 | true | 빈 입력에서 Backspace를 누르면 마지막 칩을 제거합니다. |
| placeholder | string | 아니요 | — | 네이티브 placeholder. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| value | string | 예 | — | 제어 값. |
| onChange | (event: ChangeEvent<HTMLSelectElement>) => void | 아니요 | — | 네이티브 change 핸들러. onValueChange로 충분하면 생략할 수 있습니다. |
| onValueChange | (value: string) => void | 아니요 | — | 선택적 값 콜백. |
| children | React.ReactNode | 아니요 | — | 네이티브 <option>과 <optgroup> 요소. |
| className | string | 아니요 | — | select에 붙는 선택적 클래스. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| children | React.ReactNode | 아니요 | — | 아이콘 또는 컨트롤. |
| interactive | boolean | 아니요 | false | false면 래퍼가 aria-hidden이 됩니다. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| children | React.ReactNode | 아니요 | — | Affix 내용. 문자열은 텍스트로 렌더링됩니다. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| children | React.ReactNode | 아니요 | — | 줄에 놓을 파트. 칩, 버튼, Field.Counter 등. |
| className | string | 아니요 | — | 행에 붙는 추가 클래스. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| className | string | 아니요 | — | 카운트에 붙는 클래스. 인라인 flex-basis 재정의에 씁니다. |
| style | React.CSSProperties | 아니요 | — | 인라인 스타일. flex-basis: auto면 카운트가 입력 행에 놓입니다. |