Playground로 바로 이동
Components

Field

Label과 필수 표시, 도움말과 오류 문구를 Form control 하나와 묶는 폼 한 칸입니다. Control이 들어가는 Input slot은 Swap 영역이며 접근성 연결은 Field가 소유합니다.

명세 문서 보기

🚩 Playground

Header와 Footer의 요소를 켜고 끄면서 Input slot에 들어가는 Control을 바꿔 봅니다. Slot을 Group으로 바꾸면 Field가 Label을 그룹의 이름으로 두므로, 같은 Label이 Control 하나일 때와 다르게 연결됩니다.

Helper text

Content

Input slot

State

Required
Disabled

Elements

Right description
Trailing element
Helper text
Error message
Character count

Anatomy

Field를 구성하는 실제 요소와 문서에서 사용하는 명칭입니다. Input slot은 Control이 갈아 끼워지는 Swap 영역이라 실제 컴포넌트를 그리지 않고 교체 표시로 둡니다.

  1. 1.Requirement(Optional)
  2. 2.Label
  3. 3.Right description(Optional)
  4. 4.Trailing element(Optional)
  5. 5.Input slot
  6. 6.Helper text · Error message(Optional)
  7. 7.Character count(Optional)

Structure map

Field
├─ Header
│  ├─ Requirement (Optional)
│  ├─ Label
│  ├─ Right description (Optional)
│  └─ Trailing element (Optional)
├─ Input slot
└─ Footer (Optional)
   ├─ Helper text · Error message (Optional)
   └─ Character count (Optional)

Properties · Input slot

Input slot에 실제로 들어가는 Control입니다. Control 하나일 때는 Label이 그것을 직접 가리키고, Radio Group·Checkbox Group·Chip Group처럼 여러 값을 고르는 묶음일 때는 Label이 그룹의 이름이 됩니다.

Text Field (Input)

Helper text

Textarea

Helper text

Number Input

Helper text

Select

Helper text

Radio Group
Label

Helper text

Checkbox Group (Checkbox Field)
Label

Helper text

Chip Group (Chip)
Label

Helper text

Date (Input Button)

Helper text

Time (Input Button)

Helper text

Guidelines

무엇을 Field가 소유하고 무엇을 Control에 남기는지 정리합니다.

Label과 피드백의 소유자를 Field로 두기

  • Label과 필수 표시, 도움말과 오류 문구는 Field가 소유합니다. Input이나 Select 같은 Control은 값을 다루는 일만 합니다.
  • Control이 Label을 함께 들고 있으면 같은 Label이 컴포넌트마다 다른 속성 이름으로 갈립니다. 폼 한 칸의 구조를 한 곳에서 정하기 위해 Field로 모읍니다.
  • 접근성 연결도 Field가 소유합니다. Label과 Control, 도움말과 오류 문구를 잇는 일을 소비자가 직접 하지 않습니다.

필수는 기호로, 나머지는 Right description으로 알리기

  • 필수 항목은 Label 앞의 * 하나로만 알립니다. 기호는 보조기술에서 숨기고 Control이 실제 required 상태를 갖습니다.
  • 선택사항임을 강조해야 할 때와 여러 개를 고르는 자리에서 개수를 알릴 때만 텍스트를 씁니다. Right description에 「선택」이나 「최대 3개」를 둡니다.
  • 그 문구를 Label 안에 괄호로 적지 않습니다. Label만 빠르게 읽을 수 있어야 하고, 어느 자리에 무엇이 들어가는지가 속성 이름으로 드러나야 합니다.

오류가 있으면 도움말을 대신하기

  • 도움말과 오류 문구가 함께 있으면 오류만 보입니다. 둘이 함께 쌓이면 항목마다 Footer 높이가 달라져 폼의 수직 리듬이 깨집니다.
  • 오류 문구는 무엇을 해야 하는지 알리는 행동 지시형으로 씁니다. 「휴대폰 번호를 10-11자리로 입력해 주세요」처럼 씁니다.
  • 오류 상태에서는 Character count도 오류와 같은 색으로 바뀝니다. 글자 수 제한을 넘겨 생긴 오류를 같은 자리에서 확인할 수 있어야 합니다.

상태값을 따라가기

  • Header와 Footer의 글자는 Input slot에 있는 Control의 상태를 따라갑니다. Control만 상태를 표현하고 Label을 기본 상태로 남기지 않습니다.
  • Disabled에서는 Label과 필수 표시, Right description과 Footer 문구가 모두 흐려집니다. Label만 흐려지고 나머지가 그대로면 절반만 꺼진 것으로 보입니다.
  • Error에서는 Label이 오류 문구와 같은 색을 씁니다. 여러 항목이 놓인 폼에서 어느 항목에서 오류가 났는지 Label에서 바로 읽혀야 합니다.
  • 두 상태가 겹치면 Disabled가 앞섭니다. 손댈 수 없는 항목에 고쳐야 한다는 표시를 남기면 사용자가 그 자리를 붙들게 됩니다.

Control은 Large 이상을 쓰기

  • Field와 결합해 쓰는 화면은 대체로 모바일 폼입니다. 손가락으로 누르는 자리이므로 한 줄 Control은 Regular 36px이 아니라 Large 40px 이상을 씁니다.
  • 크기는 Field가 아니라 각 Control의 controlSize가 소유하므로 Field가 강제하지 않습니다. 데스크톱 관리자 화면처럼 밀도가 중요한 자리에서는 Regular를 쓸 수 있습니다.
  • Radio와 Checkbox, Chip은 이 규칙의 대상이 아닙니다. 세 컴포넌트는 각자 자기 조작 영역과 최소 높이를 소유합니다.

Input slot에는 Control을 그대로 두기

  • Control 하나를 Input slot의 직접 자식으로 둡니다. Field가 그 자식에 id와 상태를 주입해 Label과 잇습니다.
  • Control을 div로 한 번 감싸면 주입 대상을 잃어 Label 연결이 끊깁니다. 개발 환경에서는 그 자리를 경고로 알립니다.
  • Checkbox Group처럼 여러 항목을 나열할 때는 group을 사용합니다. Field가 Label을 그룹의 이름으로 두고 각 항목에는 주입하지 않습니다.

Property map

실제 Props와 기본값입니다. characterCount는 Field가 값을 소유하지 않으므로 현재 글자 수를 소비자가 넘깁니다.

PropertyValuesDefault
labelReactNode필수
requiredfalse | truefalse
groupfalse | truefalse
disabledfalse | truefalse
readOnlyfalse | truefalse
rightDescriptionReactNodeundefined
trailingElementReactNodeundefined
helperTextReactNodeundefined
errorMessageReactNodeundefined
characterCount{ current: number; max: number }undefined
childrenReactNode필수
classNamestringundefined

명세 문서

docs/system/components/field.md

Field

packages/ui/src/field.tsx

Label과 필수 표시, 도움말과 오류 문구를 Form control 하나와 묶는 폼 한 칸이다. Control이 들어가는 자리는 갈아 끼울 수 있는 Input slot이며, Label과 Control을 잇는 접근성 연결은 Field가 소유한다.

역할과 경계

  • Field는 Header, Input slot, Footer 세 층으로 구성된다. Header가 Label과 필수 표시를, Input slot이 실제 Control을, Footer가 도움말·오류 문구와 Character count를 담당한다.
  • Label, Helper text와 Error message는 Field가 소유한다. Input, Textarea, Select 같은 Control은 값을 다루는 일만 하며 자기 Label을 갖지 않는다.
  • Field는 값을 소유하지 않는다. 값과 상태 변경은 Input slot의 Control이 갖고, Field는 그 Control에 접근성 속성과 상태만 주입한다.
  • CheckboxField·RadioField·SwitchField는 Field가 아니라 Control과 인라인 Label을 결합한 선택 행 하나다. 여러 행을 하나의 폼 항목으로 묶을 때 그 바깥을 Field가 감싼다. 두 층의 이름 구분은 vocabulary.md가 소유한다.
  • 폼의 네 구성 층인 Control·Field·Fieldset·Formvocabulary.md의 「Form 구성 층」이 소유한다. Field 위의 FieldsetForm은 현재 제공하지 않으며, 제출과 유효성 검증은 Field의 책임이 아니다.

Anatomy

  1. Label: 필수 요소다.
  2. Requirement: 선택 요소다. 필수 항목일 때 Label 뒤에 *를 표시한다. 앞에 두면 줄마다 *가 먼저 서서 칸 이름이 한 칸씩 밀려 시작하고, 필수가 아닌 칸과 이름의 시작점이 어긋난다.
  3. Right description: 선택 요소다. Requirement 뒤에 낮은 위계로 붙는다.
  4. Trailing element: 선택 요소다. Header 오른쪽 끝으로 밀려 보조 액션을 담는다.
  5. Input slot: 필수 요소다. Control이 갈아 끼워지는 교체 가능 영역이다.
  6. Helper text · Error message: 선택 요소다. Footer 왼쪽에 온다.
  7. Character count: 선택 요소다. Footer 오른쪽에 온다.

Header와 Input slot 사이는 6px, Input slot과 Footer 사이는 4px이다. Footer는 그 항목의 입력 결과를 말하므로 Control에 붙여 한 묶음으로 읽히게 하고, Label이 있는 Header 쪽만 2px 더 벌린다. Footer는 항목당 한 줄을 유지하며 왼쪽 문구와 오른쪽 글자 수가 한 행 안에서 좌우로 갈린다.

Properties

Header

  • label: 필수 ReactNode다. 14px Medium oe-foreground-primary를 사용한다.
  • required: Boolean이며 기본값은 false다. true면 Label 앞에 *oe-danger-foreground-secondary로 표시하고 Input slot의 Control에 required를 주입한다.
  • rightDescription: 선택 ReactNode다. 12px oe-foreground-tertiary를 사용하고 좁아지면 말줄임한다. Label과의 간격은 8px이다. Label과 한 덩어리로 붙어 읽히면 Label이 어디서 끝나는지 알기 어렵다.
  • trailingElement: 선택 ReactNode다. Header 오른쪽 끝에 배치하며 어떤 컴포넌트가 오는지 Field가 정하지 않는다.

Field에는 prefixsuffix를 두지 않는다. 두 이름은 값의 의미를 보완하는 짧은 표기 자리이며 input.md가 그 구분을 소유한다. Header에 오는 것은 조작을 여는 요소이므로 trailingElement를 사용한다.

Input slot

  • children: 필수 ReactNode다. Control 하나를 직접 자식으로 둔다.
  • group: Boolean이며 기본값은 false다. Input slot이 여러 값을 고르는 묶음일 때 사용한다.

Input slot은 group과 자식 개수에 따라 세 가지로 갈린다.

구성 Field의 동작
Control 하나 id를 주입하고 Label이 htmlFor로 그것을 가리킨다
group이고 자식이 하나 그 자식이 이미 자체 group을 렌더링하므로 group을 겹쳐 만들지 않고 aria-labelledby만 주입한다. Select Box Group이 이에 해당한다
group이고 자식이 여러 개 가리킬 대상이 없으므로 Field가 그 행들을 내부 묶음으로 감싸고 그 묶음이 role="group"aria-labelledby를 소유한다. CheckboxField 나열이 이에 해당한다

Input slot에 들어가는 Control은 Input, Textarea, Select, Radio Group, Checkbox Group, Chip Group과 Date·Time을 여는 Input Button이다. Radio Group과 Select Box Group은 자체 group을 렌더링하므로 group과 함께 쓰고, Checkbox Group은 CheckboxField를 여러 개 나열하므로 Field가 group을 직접 소유한다. Chip Group은 별도 컴포넌트가 아니라 chip.md의 「Chip을 Selection으로 사용하기」에 해당하는 조합이며, 가로 묶음과 그룹 role은 화면 코드가 소유한다.

Control을 div나 별도 컴포넌트로 한 번 감싸면 주입 대상을 잃어 Label 연결이 조용히 끊긴다. Field는 개발 환경에서 그 자리를 경고로 알린다. 주입한 id가 아무 곳에도 닿지 않은 경우와, 닿았지만 그것이 Control이 아닌 경우를 나누어 알린다.

Field가 group을 소유할 때 행들을 내부 묶음으로 감싸는 이유는 두 가지다. Field의 세로 간격이 행 사이로 새면 각 행이 자기 높이로 만드는 수직 리듬이 어긋난다. 그리고 role="group"이 Header와 Footer까지 품으면 그룹의 범위가 실제 선택 항목보다 넓어진다.

행 사이에는 별도 간격을 두지 않는다. Radio Group과 Checkbox Group의 수직 리듬은 각 행의 36px 최소 조작 높이가 만든다. Chip처럼 가로로 늘어놓는 조합은 화면 코드가 자기 묶음의 간격을 소유한다.

Footer

  • helperText: 선택 ReactNode다. 12px oe-foreground-tertiary를 사용한다.
  • errorMessage: 선택 ReactNode다. 12px oe-danger-foreground-secondary를 사용하며 값이 있으면 Helper text를 대체한다.
  • characterCount: 선택 { current: number; max: number }다. current/max 형태로 표시하고 tabular-nums를 적용한다.

Field는 값을 소유하지 않으므로 글자 수를 스스로 세지 않는다. 현재 글자 수는 소비자가 characterCount.current로 넘긴다.

State

  • disabled: Boolean이며 기본값은 false다. Input slot의 Control에 그대로 주입하고, Header와 Footer의 글자도 모두 oe-foreground-disabled로 바꾼다. Control만 흐리게 만들고 Label을 활성 상태처럼 남기지 않는다. 필수 표시도 함께 흐려진다. 손댈 수 없는 항목에 채워야 한다는 표시를 남기면 사용자가 그 자리를 붙들게 된다.
  • readOnly: Boolean이며 기본값은 false다. Input slot의 Control에 그대로 주입한다.
  • Invalid는 별도 Property가 아니다. errorMessage가 있으면 Field가 data-invalid를 갖고 Control에 aria-invalid를 주입하며 Label도 oe-danger-foreground-secondary로 바꾼다. 오류가 어느 항목에서 났는지 Label에서 바로 읽히게 한다. 오류 문구 없이 오류 상태만 표시하는 구성은 제공하지 않는다.
  • Label의 색은 Disabled가 Error보다 앞선다. 고칠 수 없는 항목에 고쳐야 한다는 표시를 남기지 않기 때문이다.

Behavior

Field는 Label과 Control, Footer 문구를 잇는 연결을 소유한다.

  • Label은 Control 하나를 감쌀 때 <label htmlFor>로 연결하고, Group일 때는 <span>으로 렌더링해 aria-labelledby로 연결한다. Group에는 가리킬 단일 Control이 없어 htmlFor가 성립하지 않는다.
  • Footer에 보이는 문구 하나가 aria-describedby로 Control 또는 Group에 연결된다. 오류가 도움말을 대체하므로 연결되는 문구도 항상 하나다.
  • Requirement의 *aria-hidden이다. 필수 여부는 Control의 required가 전달하므로 접근 가능한 이름에 기호가 섞이지 않는다.
  • Character count는 aria-describedby에 연결하지 않는다. 값이 바뀔 때마다 이름과 설명이 갱신되면 입력 중에 낭독이 끊긴다.

Select, MultiSelect, Combobox, MultiCombobox와 SelectBoxGroup은 ariaLabel을 선택 Property로 갖는다. Field 안에서 쓸 때는 Field가 이름을 소유하므로 ariaLabel을 넘기지 않고, Field 없이 단독으로 쓸 때만 넘긴다.

Guidelines

Label과 피드백의 소유자를 Field로 두기

Label과 필수 표시, 도움말과 오류 문구는 Field가 소유한다. Control이 Label을 함께 들고 있으면 같은 Label이 컴포넌트마다 다른 속성 이름으로 갈리고, 폼 한 칸의 구조를 한 곳에서 볼 수 없게 된다.

필수는 기호로, 나머지는 Right description으로 알리기

필수 항목은 Label 앞의 * 하나로만 알린다. 텍스트는 두 자리에만 쓴다. 선택사항임을 강조해야 할 때 선택을 쓰고, 여러 개를 고르는 자리에서 개수를 알릴 때 최대 3개를 쓴다. 두 문구는 Label 안에 괄호로 넣지 않고 rightDescription에 둔다. Label만 빠르게 읽을 수 있어야 하고, 어느 자리에 무엇이 들어가는지가 속성 이름으로 드러나야 한다.

화면의 필수 항목 비율에 따라 표기를 뒤집는 규칙은 사용하지 않는다. 항목 하나만 보고는 그 화면이 어느 방식을 쓰는지 알 수 없다.

오류가 있으면 도움말을 대신하기

도움말과 오류 문구가 함께 있으면 오류만 보인다. 둘이 함께 쌓이면 항목마다 Footer 높이가 달라져 폼의 수직 리듬이 깨지고, 먼저 읽어야 하는 문구가 흐려진다. 오류 문구는 무엇을 해야 하는지 알리는 행동 지시형으로 쓴다.

상태값을 따라가기

Header와 Footer의 글자는 Input slot에 있는 Control의 상태를 따라간다. Control만 상태를 표현하고 Label을 기본 상태로 남기지 않는다.

Disabled에서는 Label과 필수 표시, Right description과 Footer 문구가 모두 흐려진다. Label만 흐려지고 나머지가 그대로면 절반만 꺼진 것으로 보인다. Error에서는 Label이 오류 문구와 같은 색을 쓴다. 여러 항목이 놓인 폼에서 어느 항목에서 오류가 났는지 Label에서 바로 읽혀야 한다.

두 상태가 겹치면 Disabled가 앞선다. 손댈 수 없는 항목에 고쳐야 한다는 표시를 남기면 사용자가 그 자리를 붙들게 된다.

Control은 Large 이상을 쓰기

Field와 결합해 쓰는 화면은 대체로 모바일 폼이다. 손가락으로 누르는 자리이므로 한 줄 Control은 Regular 36px이 아니라 Large 40px 이상을 쓴다. 크기는 Field가 아니라 각 Control의 controlSize가 소유하므로 Field가 강제하지 않는다. 데스크톱 관리자 화면처럼 밀도가 중요한 자리에서는 Regular를 쓸 수 있다.

Radio, Checkbox와 Chip은 이 규칙의 대상이 아니다. 세 컴포넌트는 각자 자기 조작 영역과 최소 높이를 소유하며, 그 값은 radio.md·checkbox.md·chip.md에 있다.

Input slot에는 Control을 그대로 두기

Control 하나를 Input slot의 직접 자식으로 둔다. 여러 항목을 나열할 때는 group을 사용한다. Layout이 필요해 Control을 감싸야 하면 Field의 className으로 처리하고 Input slot의 자식 구조는 바꾸지 않는다.

Specification

  • Label: 14px Medium, line-height 20px. 기본은 oe-foreground-primary이고 Error에서는 oe-danger-foreground-secondary, Disabled에서는 oe-foreground-disabled
  • Requirement: 14px, line-height 20px. 기본은 oe-danger-foreground-secondary이고 Disabled에서는 oe-foreground-disabled
  • Right description: 12px, line-height 20px, oe-foreground-tertiary. Disabled에서는 oe-foreground-disabled
  • Requirement ↔ Label 간격: 2px. 필수 마크는 Label의 일부처럼 붙어 읽혀야 한다
  • Label ↔ Right description 간격: 8px. Label과 다른 위계이므로 떼어 놓는다
  • Helper text: 12px, line-height 20px, oe-foreground-tertiary. Disabled에서는 oe-foreground-disabled
  • Error message: 12px, line-height 20px, oe-danger-foreground-secondary
  • Character count: 12px, line-height 20px, tabular-nums. 기본은 oe-foreground-tertiary이고 오류 상태에서는 oe-danger-foreground-secondary
  • Header ↔ Input slot 간격: 6px
  • Input slot ↔ Footer 간격: 4px

현재 정의하지 않은 항목

  • Label weight 두 단계다. 굵은 Label이 필요한 실제 사례를 확인하지 못했으므로 현재는 Medium 하나만 제공한다.
  • Footer 자리 예약이다. 오류 문구가 나타날 때 아래 내용이 그만큼 밀린다. 도움말이 없는 항목까지 빈 줄을 들고 있으면 폼 전체가 늘어나므로 예약하지 않으며, 인라인 검증 화면에서 밀림이 문제가 되면 선택 Property로 다시 검토한다.
  • Label을 Control 왼쪽에 두는 가로 배치다. 현재는 Label이 항상 Control 위에 온다.
  • 여러 Field를 묶는 Fieldset과 폼 전체의 제출·검증을 담당하는 Form이다.
  • characterCount의 값을 Field가 직접 세는 방식이다. 현재는 소비자가 넘기며, Control의 이벤트를 Field가 가로채는 방식은 개발자 검토 항목으로 남긴다.