Input Component Generation Skill
Skill này hướng dẫn bạn (AI Agent) tạo component Input / TextField — trường nhập liệu đơn dòng, nền tảng cho mọi form trong hệ thống.
1. Mục tiêu (Objective)
Tạo component Input hoàn chỉnh, hỗ trợ nhiều type (text, password, email, search, number), có validation states, và tích hợp 100% Design Tokens. Component phải tái sử dụng được, responsive, hỗ trợ Dark Mode tự động.
2. AI Context & Intent (Ngữ cảnh cho AI)
Khi nào dùng Input?
- Nhập dữ liệu đơn dòng: tên, email, password, số điện thoại, tìm kiếm
- Form fields: mọi trường input trong form đăng ký, đăng nhập, thanh toán
Phân biệt với component khác
| Tình huống | Component đúng | Lý do |
|---|---|---|
| Nhập text ngắn (1 dòng) | Input | Đơn dòng, submit khi Enter |
| Nhập text dài (nhiều dòng) | Textarea | Multi-line, auto-resize |
| Chọn từ danh sách có sẵn | Select | Predefined options, không tự nhập |
| Tìm kiếm với autocomplete | Input (type=search) + Dropdown | Input + suggestion list |
Decision Tree cho AI
User cần nhập dữ liệu?
├─ Dữ liệu dạng text ngắn → Input
│ ├─ Có cần ẩn nội dung? → type="password"
│ ├─ Có cần validate email? → type="email"
│ ├─ Có cần tìm kiếm? → type="search" + icon
│ └─ Có cần chỉ số? → type="number"
├─ Dữ liệu dạng text dài → Textarea
└─ Chọn từ list → Select / Radio / Checkbox
3. Ngữ nghĩa & Phân loại (Semantics)
3.1. Default / Outlined Input
- Mục đích: Input chuẩn cho mọi form. Border rõ ràng giúp user nhận diện vùng nhập.
- Đặc điểm: Nền trắng/trong suốt, border
input-border, focus ring khi active. - Tương đồng:
Outlined TextField(Material 3),Default TextField(iOS).
3.2. Filled Input
- Mục đích: Dùng khi cần phân biệt Input với nền page (vd: search bar trên header tối).
- Đặc điểm: Background
base.muted, không có border rõ ràng ở trạng thái idle. - Tương đồng:
Filled TextField(Material 3).
3.3. States
| State | Mô tả | Token chính |
|---|---|---|
| Idle | Trạng thái mặc định | base.input, base.input-border |
| Focused | User đang nhập | base.primary (border), focus.ring-color |
| Error | Validation fail | base.destructive (border + message) |
| Success | Validation pass | base.success (border + icon) |
| Disabled | Không tương tác | opacity: base.disabled, cursor: not-allowed |
| Readonly | Chỉ đọc, có thể copy | Border nhạt, no focus ring |
3.5. Slot Map (Figma ↔ Code)
📎 Source:
slot-manifest.json→input· Layer: item
| Figma Slot | data-slot |
CSS Class | Required | Accepts |
|---|---|---|---|---|
| Root | input |
.input-group |
✅ | — |
| Label | input-label |
.label |
❌ | text |
| Field | input-field |
.input |
✅ | text-input |
| Leading | input-leading |
— | ❌ | icon, prefix-text, currency-symbol |
| Trailing | input-trailing |
— | ❌ | icon, clear-button, suffix-text, eye-toggle |
| Helper | input-helper |
— | ❌ | helper-text, error-text |
4. Token Mapping
📦 Token values: Xem
ATOMIC-MAPPING.md— single source of truth cho tất cả actual token values.
| Token | Type | Value | Mô tả |
|---|---|---|---|
height.sm |
number | {min-width.8} |
Small input height — compact forms, table inline edit |
height.md |
number | {min-width.10} |
Default input height — standard forms |
height.lg |
number | {min-width.12} |
Large input height — search bars, hero forms, mobile |
padding-x |
number | {spacing.3} |
Horizontal padding inside input fields |
padding-y.sm |
number | {spacing.1} |
Vertical padding for small inputs |
padding-y.md |
number | {spacing.2} |
Vertical padding for default inputs |
padding-y.lg |
number | {spacing.3} |
Vertical padding for large inputs |
font-size.sm |
number | {font.size.xs} |
Font size for small inputs |
font-size.md |
number | {font.size.sm} |
Font size for default inputs |
font-size.lg |
number | {font.size.base} |
Font size for large inputs |
radius |
number | {comp.radius} |
Corner radius — inherits from density tier comp.radius |
border-width |
number | {border-width.1} |
Default border width for input fields |
border-width-focus |
number | {border-width.2} |
Thicker border when input is focused |
placeholder-color |
color | {base.muted-foreground} |
Placeholder text color |
color.background |
color | {base.input} |
Default variant background |
color.background-filled |
color | {base.muted} |
Filled variant background |
color.border-default |
color | {base.input-border} |
Idle border |
color.border-focus |
color | {base.primary} |
Focus state border |
color.border-error |
color | {base.destructive} |
Error state border |
color.border-success |
color | {base.success} |
Success state border |
color.text |
color | {base.foreground} |
Input text color |
color.label |
color | {base.foreground} |
Label text color |
color.helper |
color | {base.muted-foreground} |
Helper text color |
color.error-text |
color | {base.destructive-muted-foreground} |
Error message color |
color.focus-ring |
color | {focus.ring-color} |
Focus ring color |
color.disabled-bg |
color | {base.muted} |
Disabled state background |
color.disabled-fg |
color | {base.muted-foreground} |
Disabled state foreground |
| icon-size | number | {min-width.5} | Size of leading/trailing icons in input fields |
| icon-gap | number | {spacing.2} | Gap between icon and input text |
| transition | string | {duration.fast-2} {easing.standard} | Border/shadow transition on focus |
| label.font-size | number | {font.size.sm} | Form label font size |
| label.font-weight | number | {font.weight.medium} | Form label font weight |
| label.margin-bottom | number | {spacing.1} | Gap between label and input field |
| helper.font-size | number | {font.size.xs} | Helper/error text font size below input |
| helper.margin-top | number | {spacing.1} | Gap between input field and helper text |
5. Props & API
interface InputProps {
/** Visual variant */
variant?: 'default' | 'filled';
/** HTML input type */
type?: 'text' | 'password' | 'email' | 'search' | 'number' | 'tel' | 'url';
/** Size preset */
size?: 'sm' | 'md' | 'lg';
/** Label hiển thị phía trên */
label?: string;
/** Placeholder hint */
placeholder?: string;
/** Helper text bên dưới */
helperText?: string;
/** Error message — hiển thị thay helperText khi có lỗi */
errorMessage?: string;
/** Success state */
isSuccess?: boolean;
/** Disabled state */
disabled?: boolean;
/** Readonly state */
readOnly?: boolean;
/** Icon bên trái (search, user, mail...) */
leftIcon?: ReactNode;
/** Icon/action bên phải (clear, toggle password, copy...) */
rightIcon?: ReactNode;
/** Full width */
fullWidth?: boolean;
}
6. Accessibility (a11y)
- Label: Mỗi Input BẮT BUỘC phải có
<label>liên kết quahtmlFor/id, hoặcaria-labelnếu label ẩn. - Error: Khi error, thêm
aria-invalid="true"vàaria-describedbytrỏ đến error message. - Helper text: Liên kết qua
aria-describedby. - Password toggle: Nút toggle show/hide password phải có
aria-label="Show password"/"Hide password". - Search: Input type=search nên wrap trong
<form role="search">. - Focus:
:focus-visibleoutline rõ ràng, không dùngoutline: nonemà không thay thế. - Disabled: Thêm
aria-disabled="true", không chỉ dùng attributedisabled.
7. Best Practices & Rules
- Semantic HTML: Dùng
<input>gốc, KHÔNG dùng<div contenteditable>. - Không Hardcode: Mọi màu sắc, spacing, radius, typography đều từ Token.
- Icon:
leftIcon/rightIconCHỈ dùng Lucide icon từassets/icons/(vd:search,eye,mail,user). CẤM dùng text emoji. Xemicon.mdmục 10. - Dark Mode: Tự động qua CSS Variables từ Mode tokens.
- Responsive: Sử dụng
fullWidthprop, padding từ Style Mode tokens. - Validation: Error state chỉ hiển thị sau khi user đã interact (onBlur), không hiển thị khi form mới load.
- Password: Luôn có toggle show/hide, không tự động hiện password.
8. Domain-Specific Patterns
8.1. Password Toggle (show/hide)
Reference: MD3 Outlined TextField + trailing icon action, shadcn Input OTP toggle pattern
| Aspect | Spec |
|---|---|
| Trigger | Trailing icon button (Lucide eye / eye-off) |
| Behavior | Click toggles type between password ↔ text |
| Icon tokens | Color: --muted-foreground, hover: --foreground |
| Size | --icon-md (20px), touch target: 44×44px (via padding) |
| a11y | aria-label="Show password" / "Hide password", role="button" |
| Default | type="password" (hidden) — user must opt-in to reveal |
<!-- Password with toggle -->
<div class="input-group">
<label for="pw">Mật khẩu</label>
<div class="input-wrapper">
<input id="pw" type="password" class="input" />
<button type="button" class="input__trailing-action" aria-label="Show password">
<svg class="icon icon--md"><!-- eye.svg --></svg>
</button>
</div>
</div>
8.2. Currency Input (VND formatting)
Reference: Fluent NumberField + suffix pattern, Ant Design InputNumber
| Aspect | Spec |
|---|---|
| Format | Live formatting ###,### via Intl.NumberFormat('vi-VN') |
| Suffix | Static text "VND" — --muted-foreground, caption text style |
| Input mode | inputmode="numeric" (mobile numeric keypad) |
| Validation | > 0, integers only, max 999,999,999 |
| Behavior | Format on input, store raw number, display formatted |
| Tokens | All existing input tokens + suffix: --muted-foreground, --font-size-xs |
<!-- Currency input -->
<div class="input-group">
<label for="price">Giá bán</label>
<div class="input-wrapper input-wrapper--suffix">
<input id="price" type="text" inputmode="numeric" class="input" value="150,000" />
<span class="input__suffix">VND</span>
</div>
</div>
// Currency formatter utility
function formatCurrency(value) {
const num = parseInt(value.replace(/\D/g, ''), 10);
if (isNaN(num)) return '';
return new Intl.NumberFormat('vi-VN').format(num);
}
8.3. Phone Auto-format (Vietnamese)
Reference: Apple HIG
telcontent type + OneUI interaction area
| Aspect | Spec |
|---|---|
| Pattern | 0XXX XXX XXXX — auto-insert spaces at positions 4, 7 |
| Validation | 10 digits, starts with 0 |
| Attributes | type="tel", inputmode="numeric", autocomplete="tel" |
| Leading icon | Lucide phone (optional) |
| Behavior | Format on input, strip spaces for data |
| Tokens | All existing input tokens |
<!-- Phone input -->
<div class="input-group">
<label for="phone">Số điện thoại</label>
<div class="input-wrapper">
<svg class="input__leading-icon icon icon--md"><!-- phone.svg --></svg>
<input id="phone" type="tel" inputmode="numeric" autocomplete="tel"
class="input" placeholder="0912 345 678" maxlength="12" />
</div>
</div>
// Phone formatter utility
function formatPhone(value) {
const digits = value.replace(/\D/g, '').slice(0, 10);
if (digits.length <= 4) return digits;
if (digits.length <= 7) return `${digits.slice(0, 4)} ${digits.slice(4)}`;
return `${digits.slice(0, 4)} ${digits.slice(4, 7)} ${digits.slice(7)}`;
}
9. Example Usage
{/* Basic */}
<Input label="Email" type="email" placeholder="you@example.com" />
{/* Password with toggle */}
<Input label="Mật khẩu" type="password" showToggle />
{/* Currency */}
<Input label="Giá bán" format="currency" suffix="VND" inputMode="numeric" />
{/* Phone */}
<Input label="Số điện thoại" format="phone" type="tel" placeholder="0912 345 678" />
{/* Search variant */}
<Input variant="filled" type="search" placeholder="Tìm kiếm..." leftIcon={<SearchIcon />} size="lg" />
{/* With validation error */}
<Input label="Password" type="password" errorMessage="Mật khẩu phải có ít nhất 8 ký tự" />
{/* Disabled */}
<Input label="Username" value="ngocln" disabled />