FormField
먼저 보고, 그다음 계약을 읽습니다
Label · hint · error · 필수(*) 마크를 묶는 폼 필드 컴포지션 래퍼.
Component intent의도와 경계 읽기
FormField 는 controlled value 를 갖지 않는 순수 컴포지션 래퍼로, 라벨/보조설명/에러/필수표시를 axe-form-field 레이아웃으로 감싼다. 자식이 단일 valid element 면 aria-describedby(hint/error id)와 aria-invalid(error 시)를 자동 주입하되, 소비자가 직접 준 aria-invalid 는 그대로 이기고 직접 준 aria-describedby 는 자동 id 와 병합된다. htmlFor 는 필수이며 자식 control 의 id 와 매칭해 클릭-포커스와 a11y 를 성립시킨다. 함께 export 되는 Label 은 Radix Label 래퍼로 단독으로도 쓴다.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { FormField, Input } from "@axe/ui";<FormField label="이메일" htmlFor="email" hint="회사 메일을 사용하세요" required> <Input id="email" type="email" /></FormField>import { FormField, Input } from "@axe/ui";<FormField label="비밀번호" htmlFor="pw" error="8자 이상이어야 합니다"> <Input id="pw" type="password" /></FormField>import { Label, Checkbox } from "@axe/ui";<div style={{ display: "flex", gap: 8, alignItems: "center" }}> <Checkbox id="remember" /> <Label htmlFor="remember">로그인 유지</Label></div><div class="axe-form-field axe-form-field--error"> <label class="axe-label axe-form-field__label" for="pw"> 비밀번호<span class="axe-form-field__required" aria-hidden="true">*</span> </label> <div class="axe-form-field__control"> <input id="pw" class="axe-input" type="password" aria-invalid="true" aria-describedby="pw-err" /> </div> <p id="pw-err" class="axe-form-field__error" role="alert">8자 이상이어야 합니다</p></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
label | React.ReactNode | 필수 | — | (FormField) 라벨 텍스트/노드 (string 외 노드 가능). |
htmlFor | string | 필수 | — | (FormField) 자식 control 의 id — Label 의 htmlFor 와 매칭 (필수). |
hint | React.ReactNode | — | — | (FormField) 보조 설명 (작은 회색, error 가 있으면 가려짐). |
error | React.ReactNode | — | — | (FormField) 에러 메시지 — 있으면 hint 자리 대체, role="alert", danger 색. |
required | boolean | — | — | (FormField) 우상단 (*) 마크 표시 — 시각 표시만 (자식 control 의 required 와는 별개). |
children | React.ReactNode | 필수 | — | (FormField) 감쌀 control — 단일 valid element 면 aria-describedby/aria-invalid 자동 wiring. |
...rest | React.HTMLAttributes<HTMLDivElement> | — | — | (FormField) className 은 axe-form-field 와 병합, 나머지는 루트 <div> 로 전파. |
htmlFor (Label) | string | — | — | (Label) 연결할 control 의 id — click-to-focus. |
...rest (Label) | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> | — | — | (Label) className 은 axe-label 과 병합, 나머지 Radix Label props 를 <label> 로 전파. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-label | stable | 라벨 루트 (Radix Label, cursor:pointer, user-select:none). |
.axe-form-field | stable | 필드 래퍼 (flex column, gap). |
.axe-form-field--error | stable | 에러 상태 modifier — 내부 Input/Select/Checkbox/Radio/Switch 에 빨간 테두리를 전파. |
.axe-form-field__label | stable | 필드의 Label 파트 (axe-label 상속). |
.axe-form-field__required | stable | 필수(*) 마크 (danger 색, aria-hidden). |
.axe-form-field__control | stable | control 래퍼 (Input/Select/Checkbox 등이 들어옴). |
.axe-form-field__hint | stable | 보조 설명 텍스트 (text-xs, tertiary). |
.axe-form-field__error | stable | 에러 텍스트 (text-xs, danger, role="alert"). |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- 라벨 클릭 시 htmlFor 로 연결된 control 로 포커스 이동
에러 메시지는 role="alert" 로 즉시 안내됨. 단일 element 자식엔 aria-describedby(hint/error) 와 aria-invalid(error 시)가 자동 연결됨. (*) 마크는 aria-hidden 이라 스크린리더가 읽지 않음.
Fragment 자식은 cloneElement 대상이 아니라 aria 자동 wiring 이 안 됨 — 단일 element 로 감싸거나 수동 지정. 실제 필수 검증은 control 의 required 로 (required prop 은 시각 표시). Radix Label 이 텍스트 선택 중 더블클릭 오포커스를 막음.