Input
Field văn bản một dòng trên nền nổi, bốn size, với icon trang trí hoặc văn bản cố định tùy chọn trước hoặc sau giá trị. Chữ là 16px trên màn hình cảm ứng để iOS Safari không phóng to khi focus.
Xem trước
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-input" className="text-sm text-text-primary">Email</label>
<Input id="preview-input" placeholder="you@example.com" />
</div>Cách dùng
Khi nào dùng
- Văn bản một dòng người dùng gõ: một tên, một địa chỉ email, một từ khóa tìm kiếm, một số.
- Giá trị người dùng sao chép được nhưng không sửa được, chẳng hạn API key, với readOnly.
- Field có icon cho biết nội dung nhập vào, chẳng hạn kính lúp cho tìm kiếm, với leadingIcon.
Khi nào không dùng
- Văn bản nhiều dòng. Dùng Textarea.
- Chọn một giá trị từ một danh sách đã biết. Dùng Select.
- Một field disabled cho giá trị người dùng vẫn cần đọc hoặc sao chép. Dùng readOnly, để field vẫn focus được và giá trị sao chép được.
Biến thể
size
Chiều cao từ size-element-*: sm 28px, md 32px (mặc định), lg 36px, xl 44px khi dùng chuột. Trên cảm ứng, md tăng lên 36px, lg lên 44px và xl lên 48px; sm không tăng, nên đừng dùng sm trên màn hình ưu tiên cảm ứng.
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-input-size-sm" className="text-sm text-text-primary">Email</label>
<Input id="preview-input-size-sm" size="sm" placeholder="you@example.com" />
</div>
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-input-size-md" className="text-sm text-text-primary">Email</label>
<Input id="preview-input-size-md" placeholder="you@example.com" />
</div>
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-input-size-lg" className="text-sm text-text-primary">Email</label>
<Input id="preview-input-size-lg" size="lg" placeholder="you@example.com" />
</div>
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-input-size-xl" className="text-sm text-text-primary">Email</label>
<Input id="preview-input-size-xl" size="xl" placeholder="you@example.com" />
</div>Ví dụ
Mặc định
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="email" className="text-sm text-text-primary">Email</label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>Với một icon
Icon mang tính trang trí; label nhìn thấy đặt tên cho field.
import { IconSearch } from "@tabler/icons-react"
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="search-files" className="text-sm text-text-primary">Tìm kiếm tệp</label>
<Input id="search-files" type="search" placeholder="Tên hoặc phần mở rộng" leadingIcon={<IconSearch size={16} />} />
</div>Với văn bản cố định
Văn bản cố định là một phần ý nghĩa của giá trị, nên input được mô tả bởi nó.
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-md">
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="workspace-url" className="text-sm text-text-primary">Workspace URL</label>
<Input id="workspace-url" leadingText="werewolf.dev/" defaultValue="acme" />
</div>
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="weight" className="text-sm text-text-primary">Weight</label>
<Input id="weight" type="number" inputMode="decimal" trailingText="kg" defaultValue="12" />
</div>
</div>Vô hiệu
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="workspace" className="text-sm text-text-primary">Không gian làm việc</label>
<Input id="workspace" disabled defaultValue="Personal" />
</div>Chỉ đọc
import { IconLock } from "@tabler/icons-react"
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="api-key" className="text-sm text-text-primary">Khóa API</label>
<Input id="api-key" readOnly defaultValue="wolf_sk_4f9a..." trailingIcon={<IconLock size={16} />} />
</div>Không hợp lệ
import { Input } from "@/components/ui/input"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="signup-email" className="text-sm text-text-primary">Email</label>
<Input id="signup-email" type="email" aria-invalid="true" aria-describedby="signup-email-error" defaultValue="not-an-email" />
<p id="signup-email-error" className="text-sm text-text-danger">Nhập email như you@example.com.</p>
</div>Nên và không nên
Nên
- Luôn ghép input với một label nhìn thấy (htmlFor / id).
- Khớp size của input với button bên cạnh: md trong form, sm chỉ trong UI đặc, ưu tiên chuột (bộ lọc table, toolbar), lg hoặc xl cho field chính duy nhất của một màn hình (search hero, đăng nhập trên mobile).
- Không hợp lệ: aria-invalid="true", viền danger, cùng văn bản lỗi bằng color-text-danger liên kết với aria-describedby. Không bao giờ chỉ dùng màu.
- Readonly: readOnly gốc, cùng kiểu với mặc định, vẫn focus được với vòng focus. Dùng cho giá trị người dùng sao chép được nhưng không sửa được (API key, URL được tạo).
- Giữ viền danger khi đang focus; vòng focus được vẽ bên ngoài viền.
- Dùng leadingIcon cho icon cho biết loại giá trị được nhập: kính lúp cho tìm kiếm, phong bì cho email.
- Truyền icon kèm size, và size đó khớp size của input: 16 ở sm, md và lg, 20 ở xl.
- Dùng leadingText hoặc trailingText cho phần cố định của giá trị mà người dùng không gõ: https://, đơn vị tiền, đơn vị đo. Giữ ngắn, một từ hoặc ký hiệu.
Không nên
- Đừng dùng placeholder làm label hoặc cho thông tin bắt buộc; nó biến mất khi gõ.
- Đừng dùng size sm trên màn hình ưu tiên cảm ứng. Trên cảm ứng md đã là 36px, lg 44px và xl 48px.
- Đừng dùng disabled cho giá trị người dùng vẫn cần đọc hoặc sao chép; dùng readOnly.
- Đừng thêm đổi viền khi hover. border-control đã đánh dấu field.
- Đừng xuống dưới font-size-input hoặc tự đặt lại màu qua className.
- Đừng để một icon tự mang ý nghĩa (dấu kiểm cho hợp lệ, cảnh báo cho không hợp lệ). Nói điều đó bằng văn bản liên kết với aria-describedby.
- Đừng đặt button hoặc link vào leadingIcon hoặc trailingIcon. Icon mang tính trang trí và bị ẩn khỏi trình đọc màn hình.
Props
| Tên | Kiểu | Giá trị | Mặc định | Mô tả |
|---|---|---|---|---|
| size | enum | sm | md | lg | xl | md | Chiều cao từ size-element-*: sm 28px, md 32px (mặc định), lg 36px, xl 44px khi dùng chuột. Trên cảm ứng, md tăng lên 36px, lg lên 44px và xl lên 48px; sm không tăng, nên đừng dùng sm trên màn hình ưu tiên cảm ứng. |
| disabled | boolean | - | false | Không tương tác. Kiểu disabled trung tính. |
| readOnly | boolean | - | false | Giá trị đọc, chọn và sao chép được nhưng không sửa được. Cùng kiểu với mặc định; giữ vòng focus. |
| type | enum | text | email | password | search | tel | url | number | text | type của input gốc cho các field dạng văn bản một dòng. Chỉ trên web. |
| leadingIcon | node | - | - | Icon Tabler trang trí trước văn bản, chẳng hạn kính lúp trong field tìm kiếm. Truyền size, và: 16 ở sm, md và lg, 20 ở xl. |
| trailingIcon | node | - | - | Icon Tabler trang trí sau văn bản, chẳng hạn ổ khóa trên một giá trị readonly. Cùng quy tắc size với leadingIcon. Không dành cho button: button xóa hoặc hiện mật khẩu được lên kế hoạch thành prop riêng. |
| leadingText | node | - | - | Văn bản cố định gắn vào đầu field, chẳng hạn https://, một tên miền hoặc đơn vị tiền. Vẽ thành một đoạn nền lõm, tách khỏi giá trị bằng một đường kẻ; input được mô tả bởi nó, nên trình đọc màn hình đọc nó cùng field. |
| trailingText | node | - | - | Văn bản cố định gắn vào cuối field, chẳng hạn đơn vị (kg, %) hoặc tên miền (.com). Cùng kiểu và cách đọc với leadingText. |
Slot
| Tên | Bắt buộc | Mô tả |
|---|---|---|
| value | không | Giá trị văn bản controlled hoặc uncontrolled. |
Cấu tạo
| Phần | Mô tả |
|---|---|
| root | Phần tử input. Khi đứng một mình, đó là hộp field. Khi có icon, nó lấp đầy group và không vẽ hộp riêng. |
| placeholder | Văn bản gợi ý hiện khi giá trị trống. Tô bằng text-secondary, nên đạt 4.5:1 trên nền nổi. |
| group | Hộp field khi có icon hoặc văn bản cố định: một wrapper cùng nền, border, radius và chiều cao với input đứng một mình. Hiện trạng thái focus, kiểu disabled và viền danger của input thông qua :has(). className nằm ở đây. |
| groupIcon | Icon phía trước hoặc phía sau bên trong group. 16px ở sm, md và lg, 20px ở xl (DESIGN.md 9.1). Mang tính trang trí. |
| addon | Đoạn leadingText hoặc trailingText: nền lõm, text-secondary, một đường border-border ở phía hướng về giá trị, sát hai mép của group. |
Khả năng truy cập
Role: textbox
| Phím | Hành động |
|---|---|
| Tab | Chuyển focus tới input và hiện vòng focus. |
| Any printable key | Chèn văn bản tại con nháy. |
- Ghép với một label nhìn thấy (htmlFor / id). Chỉ placeholder không phải là label.
- Disabled dùng thuộc tính disabled gốc, nên input rời thứ tự tab.
- Readonly dùng thuộc tính readOnly gốc: vẫn focus được và nằm trong thứ tự tab, giá trị chọn và sao chép được.
- Không hợp lệ dùng aria-invalid="true" cùng văn bản lỗi nhìn thấy bằng color-text-danger liên kết với aria-describedby.
- font-size-input là 14px với chuột và 16px trên cảm ứng để iOS Safari không phóng to trang khi focus.
- leadingIcon và trailingIcon mang tính trang trí (aria-hidden). Label và văn bản mang ý nghĩa, nên trình đọc màn hình nghe cùng một field dù có hay không có chúng.
- Một lần nhấn lên icon hoặc padding của group đưa focus vào input, giống nhấn lên chính field.
Cài đặt
npx werewolf-ui add inputChạy npx werewolf-ui init một lần trong app trước khi thêm component. Init cài token, thêm CSS import và luật cho agent. Add cài component này cùng các dependency và giữ nguyên theme của bạn. Xem Cài đặt để biết yêu cầu và cách cài qua registry.