Field
Mẫu chuẩnLabel và Input gộp thành một hàng biểu mẫu, nối bằng id, aria-describedby và aria-invalid — dây nối trợ năng mà mỗi call site sẽ quên tự làm.
Playground
Đổi brand, theme, ghi chú (không có / hint / đang kiểm tra / còn trống / đã dùng / error) và trạng thái disabled để xem cùng một Field thích ứng.
Không có ghi chú.
import { Field } from "@nexobox/ui";
<Field
label="Tên workspace"
placeholder="vd. Xưởng Mộc Tâm An"
/>Bài toán
Một ô nhập trần không dùng được trong biểu mẫu: người dùng cần biết nó hỏi gì, người dùng bàn phím cần nhãn ăn khớp với ô để bấm vào nhãn là tiêu điểm nhảy vào ô, và người dùng trình đọc màn hình cần dòng lỗi được đọc như một phần của ô chứ không như một đoạn văn trôi nổi phía dưới. Input không tự làm được vì nó không biết id của nó sẽ là gì, và Label không tự làm được vì nó không biết id nào để trỏ htmlFor vào. Việc sinh một id chung và nối aria-describedby không thuộc về primitive nào trong hai primitive đó — nó thuộc về thứ chứa cả hai, và đó chính là composite này.
Khi nào sử dụng
Nên dùng
Mọi hàng biểu mẫu một dòng — tên, email, tên workspace, mã số. Khi ô nhập cần một gợi ý thường trực (hint), một dòng kiểm tra sống (status) hoặc một thông báo lỗi ở mức field (error). Khi caller không muốn tự quản lý id — đây là ca mặc định.
Khi nào không dùng
Tránh dùng
| Không dùng cho | Dùng thay |
|---|---|
| Ô nhập không nhãn trong một control lớn hơn — ô tìm kiếm, ô lọc cột bảng | Input trực tiếp, với aria-label |
| Mật khẩu | PasswordField — thêm nút hiện/ẩn và luật autoComplete |
| Mã OTP | OtpInput — thêm lọc chữ số, inputMode và one-time-code |
| Vùng văn bản nhiều dòng | Textarea cộng Label. Field bọc Input, không bọc Textarea |
| Banner lỗi ở mức biểu mẫu, không ở mức field | FormError |
Dựng từ
| Primitive tầng 1 | Vai trong composite |
|---|---|
| Label | Nhãn, nhận htmlFor bằng id đã giải quyết. |
| Input | Ô nhập. FieldProps mở rộng InputProps nên mọi prop của Input đi xuyên qua. |
| Field thêm gì mà không primitive nào có |
|---|
Một id chung: do caller truyền, nếu không thì useId() |
aria-describedby trỏ ${id}-note — chỉ khi thật sự có error, status hoặc hint |
aria-invalid bật khi error có giá trị, hoặc khi status đang hiện và statusInvalid |
Luật loại trừ: error che status, status che hint |
status render role="status", hoặc role="alert" khi statusInvalid. Màu theo statusTone |
Anatomy
| # | Phần | Ghi chú |
|---|---|---|
| 1 | Label | htmlFor bằng id đã giải quyết. |
| 2 | Input | id, aria-describedby, aria-invalid khi có error hoặc statusInvalid. |
| 3 | Ghi chú (p) | id=${id}-note, chỉ render khi có error, status hoặc hint. Một dòng. |
Ba phần theo thứ tự dọc. Không có biến thể nằm ngang — một nhãn đặt cạnh ô là quyết định layout của biểu mẫu, không phải của field.
States
Không error, không hint. Không có aria-describedby.
Dùng tên bạn muốn khách hàng nhìn thấy.
aria-invalid không bật.
Đang kiểm tra…
role=status. aria-invalid không bật.
Địa chỉ này chưa ai dùng.
role=status, tone success.
Địa chỉ này đã có người dùng.
role=alert, aria-invalid="true".
Tên workspace là bắt buộc.
aria-invalid="true". status và hint bị che.
Kế thừa nguyên từ Input — Field không thêm hay ghi đè.
Hover, focus, read-only: kế thừa nguyên từ Input, Field không ghi đè. Dòng đang kiểm tra là status, host truyền câu chữ, Field giữ role và màu.
Bàn phím & trợ năng
Seam — host sở hữu gì
Field không xác thực, không debounce, không gọi gì, và không biết biểu mẫu của nó nộp đi đâu. Theo ADR-UI-003 clause 2 nó không fetch, không đọc router, không giữ session và không import gì từ next.
| Host sở hữu | Cấp qua |
|---|---|
| Giá trị và mọi thay đổi | value / onChange, hoặc để Input chạy uncontrolled |
| Chuỗi lỗi, và khi nào nó xuất hiện | error |
| Câu chữ dòng kiểm tra sống | status. Field không debounce và không gọi kiểm tra |
| Màu dòng đó, và ô có invalid không | statusTone, statusInvalid |
| Chuỗi gợi ý thường trực | hint |
| Ký tự đứng đầu trong ô | prefix |
| Việc nộp biểu mẫu, xác thực phía server | <form> của host cộng FormError |
id cố định, khi host cần trỏ tới ô từ nơi khác | id |
Trạng thái duy nhất Field tự giữ là useId() — một định danh, không phải dữ liệu.
API & triển khai
| Prop | Mặc định | Quy ước |
|---|---|---|
| label | — | Bắt buộc. Không có biến thể nhãn ẩn: dùng Input với aria-label nếu cần. |
| hint | undefined | Bị status hoặc error che. |
| error | undefined | Có giá trị ⇒ aria-invalid="true". Che status và hint. |
| status | undefined | Dòng kiểm tra sống. Host sở hữu câu chữ. Chuỗi rỗng không render. |
| statusTone | muted | muted | success | danger. Chỉ khi status đang hiện. |
| statusInvalid | false | Khi status đang hiện: role="alert" và aria-invalid="true". |
| statusMark | undefined | pending = spinner, ready = check đặc. Đổi bằng opacity trong 180ms. |
| prefix | undefined | aria-hidden, căn trái trong ô. |
| id | useId() | Truyền khi host cần một id biết trước. |
| className | undefined | Đi vào Input, không vào div bọc ngoài. |
| …InputProps | — | type, placeholder, required, disabled, autoComplete, value, onChange, … đi xuyên qua. |
Design tokens
Field đọc --nf-component-auth-field-gap cho khoảng cách dọc, --nf-foundation-typography-caption-font-size cho cỡ chữ ghi chú, --nf-semantic-text-muted cho gợi ý và status muted, --nf-semantic-status-success-foreground cho status success, --nf-semantic-status-danger-foreground cho lỗi và status danger. Lỗ hổng đặt tên, ghi ra thay vì che: --nf-component-auth-field-gap nằm trong nhóm component.auth.* dù Field không còn là composite của auth từ ADR-UI-005 clause 5. Alias đúng là component.field.gap; đổi tên là 10 theme cộng một lần regenerate, theo dõi ở dòng ledger W10.19. Tên trong bảng trên là tên thật sự có trong CSS hôm nay.
Do / Don't
Nên dùng
Để Field sinh id. Truyền error là chuỗi người đọc hiểu được. Dùng hint cho luật thường trực. Dùng status cho dòng kiểm tra sống. Nhớ className đi vào Input. Để error che status, status che hint.
Tránh dùng
Tự nối htmlFor và aria-describedby ở call site. Tự dựng <p role="status"> dưới ô. Truyền error={true} rồi trông đợi một chuỗi mặc định — không có chuỗi mặc định. Dùng hint để báo lỗi. Truyền className mong đổi khoảng cách của cả khối. Trông đợi thấy hai dòng ghi chú cùng lúc.