Nền tảng
States
Đã đặc tảSáu trạng thái: rest, hover, pressed, focus-visible, selected, disabled. Chúng được kể bằng màu bề mặt và ring, không bằng hình học — một control không lún, không nảy, không đổi kích thước khi bị chạm vào. Focus có ba lớp, không phải hai, và lớp thứ ba không phải focus.
Nghĩa vụ
Obligation
A33
A33 Trạng thái kể bằng bề mặt
Hover, pressed, selected và disabled biểu đạt bằng màu — nền, viền, chữ. Không control nào đổi chiều cao, chiều rộng, vị trí hay scale để báo trạng thái. Ô nhập chữ là ngoại lệ duy nhất không có pressed: gõ không phải nhấn.
Ở chiều cao mặc định 36px sau DS-017 clause 3, một phép thu 2% dịch 0,7 pixel — dưới ngưỡng đọc được thành phản hồi, nhưng đủ để trình duyệt lấy mẫu lại chữ trong nhãn và làm nó rung. Đó là nhiễu phải trả giá render mà không mua được thông tin nào.
Kiểm. node infra/check-control-states.mjs — chuỗi press-scale không được xuất hiện trong packages/ui/src/**.
A34
A34 Không control nào câm
Mỗi primitive tương tác có hover, pressed và focus-visible; cộng selected nếu nó có trạng thái chọn, và disabled nếu nó vô hiệu hoá được. Trước DS-018, checkbox, radio và switch không có hover — cách duy nhất biết con trỏ đang ở trên một checkbox là con trỏ chuột đổi hình. Sáu ô dưới đây là năm trạng thái trên primitive thật, cạnh nhau — không phải mô tả, không phải ảnh chụp.
rest
Trạng thái nghỉ, không token nào áp.
hover
surface.hover — nền +1 bậc.
pressed
surface.pressed, border.strong. Không lún, không co — chỉ màu đổi.
focus-visible
Ring 2px, cách 2px. Bấm chuột vào nút này không vẽ ring — chỉ Tab mới vẽ.
selected / checked
Di chuột vào từng hộp: nền của chính hộp không đổi — hover và pressed sống trên một state layer phía sau, vì trên hộp 20px đổi nền của chính nó sẽ đọc như đổi checked.
disabled
Nền và chữ mờ, con trỏ không còn bấm được.
A35
A35 Một ring duy nhất
Ring vẽ ở :focus-visible, không ở :focus. Nó dùng --nf-component-focus-color, -width, -offset và không được tuỳ biến theo primitive.:focus-visible đã xử lý đúng sự khác biệt ở tầng trình duyệt: click vào một ô text vẫn được coi là focus-visible vì người dùng cần biết caret ở đâu, click một nút thì không. Ô nhập là chỗ duy nhất hai lớp tách nhau: :focus đổi border-color, còn :focus-visible vẽ ring.
Ring giống hệt nhau trên mọi control. Người dùng bàn phím học chỉ báo này một lần; hình học của ring — độ dày, khoảng cách — thuộc A16, trang này không lặp lại nó.
A36
A36 Highlighted không phải focus
Trong menu, select và combobox, con trỏ bàn phím hiển thị bằng nền qua data-[highlighted]. Những item đó không nhận ring — ring cộng nền highlighted đọc như hai con trỏ cùng lúc trên cùng một danh sách, và nền một mình là đủ.
Never. Đừng "sửa" dropdown-menu.tsx vì nó không có focus-visible. Con số 0 ở đó là đúng, và gate của A34 miễn trừ nó có tên kèm lý do ghi trong chính mã gate — chứ không im lặng bỏ qua.
Bảng
Sáu trạng thái
| Trạng thái | Cơ chế | Token |
|---|---|---|
| rest | — | — |
| hover | nền +1 bậc | --nf-semantic-surface-hover |
| pressed | nền +2 bậc, viền đậm hơn | --nf-semantic-surface-pressed, --nf-semantic-border-strong |
| focus-visible | ring 2px, cách 2px | --nf-component-focus-color / -width / -offset |
| selected / checked | nền và chữ riêng | --nf-semantic-action-selected-background / -foreground |
| disabled | nền và chữ mờ | --nf-semantic-action-disabled-background / -foreground |
Mỗi primitive bind alias -hover và -pressed của chính nó ở tầng component, không bind thẳng token semantic. Giá trị thật của mười token dưới, đọc trực tiếp từ theme đang mở:
| Token | Giá trị |
|---|---|
--nf-semantic-surface-hover | Unknown |
--nf-semantic-surface-pressed | Unknown |
--nf-semantic-border-strong | Unknown |
--nf-component-focus-color | Unknown |
--nf-component-focus-width | Unknown |
--nf-component-focus-offset | Unknown |
--nf-semantic-action-selected-background | Unknown |
--nf-semantic-action-selected-foreground | Unknown |
--nf-semantic-action-disabled-background | Unknown |
--nf-semantic-action-disabled-foreground | Unknown |
Thời lượng
Thời lượng: vào tức thì, ra mượt
--nf-component-transition-duration là 120ms và chi phối chiều ra. --nf-component-transition-press-duration là 0ms và được ép lên chiều vào qua :active. Đó không phải một chi tiết cài đặt tình cờ — nó là thứ tạo cảm giác chắc tay: trạng thái nhấn xuất hiện ngay lập tức dưới ngón tay, rồi tan đi trong 120ms khi thả. Đảo chiều lại sẽ cho cảm giác control phản hồi chậm dù tổng thời gian không đổi.
prefers-reduced-motion: reduce đưa mọi thời lượng về 0ms, nên các trạng thái nhảy tức thì. Đổi màu không phải chuyển động và về nguyên tắc không cần zero hoá, nhưng kết quả vô hại và nhất quán với phần còn lại của kit.
Ranh giới
States không phải là gì
Không phải bảng màu — giá trị của surface.hover và surface.pressed thuộc màu, trang này chỉ nói chúng dùng vào việc gì.
Không phải hình học của ring — độ dày, khoảng cách và lý do ring không dán sát viền nằm ở tiêu điểm và bàn phím.
Không phải presence — dialog vào bằng đường cong nào, menu ra trong bao lâu, thuộc chuyển động. Ranh giới: trạng thái là control đang được tương tác; presence là bề mặt đang xuất hiện hay biến mất.
Không phải validation — aria-invalid, viền lỗi và thông điệp thuộc trang của từng primitive và nội dung/ngôn ngữ.
Chưa quyết
Open questions
| Question | Owner | Resolves when |
|---|---|---|
| surface.hover và surface.subtle có nên tách giá trị không? | nexoboxvn@gmail.com | Một nút ghost đặt trên nền subtle cần hover nhìn thấy được. Hôm nay hai token phân giải về cùng một giá trị ở theme sáng |
| Có cần trạng thái loading ở tầng này không? | nexoboxvn@gmail.com | Một primitive thứ hai ngoài Button cần nó |
| pressScale quay lại cho bề mặt nào? | nexoboxvn@gmail.com | Chủ sở hữu nêu tên một bề mặt. Token ở lại corpus chính vì câu hỏi này còn mở |