Live Preview
Default (6-digit, 3+3)
—
Partially Filled
7
2
9
—
Error State
4
8
2
—
5
1
7
Mã OTP không đúng. Vui lòng thử lại.
Disabled
—
OTP Input Component Generation Skill
Skill này hướng dẫn bạn (AI Agent) tạo component OTP Input — nhóm ô input cho mã xác thực (OTP/PIN), auto-advance focus, paste support.
1. Mục tiêu (Objective)
Tạo OTP Input component hỗ trợ 4-6 digits, auto-advance, paste, countdown timer, tích hợp 100% Prisma Design Tokens. Component phải accessible, mobile-optimized, và dark mode compatible.
2. AI Context & Intent (Ngữ cảnh cho AI)
Khi nào dùng OTP Input?
- Xác thực SMS/Email: Nhập mã OTP 6 số sau đăng ký/forgot password
- PIN entry: Nhập mã PIN 4-6 số
- 2FA: Two-factor authentication codes
Phân biệt với component khác
| Tình huống | Component đúng | Lý do |
|---|---|---|
| Mã xác thực 4-6 số | OTP Input | Separated cells, auto-advance |
| Password dài | Input (type=password) | Single field, variable length |
| Số điện thoại | Input (type=tel) | Continuous input, formatting |
| Mã coupon/voucher | Input (type=text) | Alphanumeric, variable length |
Decision Tree cho AI
text
User cần nhập mã?
├─ Mã số cố định 4-6 ký tự → OTP Input
│ ├─ Chỉ số → inputmode="numeric"
│ └─ Chữ + số → inputmode="text"
├─ Mã dài, variable length → Input (text)
└─ Password → Input (password)
3. Ngữ nghĩa & Phân loại (Semantics)
3.1. Architecture (shadcn pattern)
| Part | Mô tả |
|---|---|
| OTPInput | Root container, manages state |
| OTPGroup | Visual grouping (e.g., 3+3 with separator) |
| OTPSlot | Individual digit cell |
| OTPSeparator | Visual separator between groups (dash/dot) |
3.2. States
| State | Visual | Token |
|---|---|---|
| Idle | Default border | --input-border |
| Focused (active cell) | Thicker border + cursor blink | --primary, border-width: 2px |
| Filled | Shows digit, normal border | --input-border |
| Error | All cells red border + error msg | --destructive |
| Success | All cells green border (brief) | --success |
| Disabled | Muted, no interaction | opacity: var(--disabled) |
4. Token Mapping
📦 Atomic Mapping: Xem
ATOMIC-MAPPING.md→ mục otp-input — UI Layer: Item, Density Tier: comp.
| Property | Token | Ghi chú |
|---|---|---|
| Cell bg | --input |
|
| Cell fg | --foreground |
Digit text |
| Cell border | --input-border |
Default |
| Cell border (focus) | --primary |
Active cell |
| Cell border (error) | --destructive |
All cells on error |
| Cell radius | --comp-radius |
Density tier aware |
| Cell width | --spacing-12 (48px) |
|
| Cell height | --spacing-14 (56px) |
|
| Cell font-size | --font-size-xl (20px) |
Large, centered |
| Cell font-weight | --font-weight-semibold |
|
| Group gap | --spacing-2 |
Between cells in same group |
| Separator width | --spacing-4 |
Gap around separator |
| Focus ring | Standard focus ring tokens | |
| Cursor color | --primary |
Blinking line in active cell |
| Transition | --duration-fast-2 + --easing-standard |
Border color transition |
| Error text | --destructive, --font-size-xs |
Below component |
5. Behavior
| Feature | Spec |
|---|---|
| Auto-advance | On digit input → focus next empty cell |
| Backspace | Delete current → focus previous cell |
| Paste | Intercept paste → distribute chars across cells |
| Completion | When all cells filled → trigger onComplete callback |
| Keyboard | Only digits (0-9) accepted, ignore letters |
| Mobile | inputmode="numeric" → numeric keypad |
| AutoFill | autocomplete="one-time-code" → iOS/Android auto-detect SMS OTP |
| Timer | Optional countdown (e.g., 60s) with resend button |
6. Props & API
typescript
interface OTPInputProps {
/** Number of digits */
length?: 4 | 5 | 6; // default: 6
/** Group pattern (e.g., [3,3] for 3+3 with separator) */
groupPattern?: number[]; // default: [3, 3]
/** Called when all digits entered */
onComplete?: (code: string) => void;
/** Called on each change */
onChange?: (code: string) => void;
/** Error state */
error?: boolean;
/** Error message */
errorMessage?: string;
/** Disabled state */
disabled?: boolean;
/** Auto-focus first cell on mount */
autoFocus?: boolean; // default: true
}
7. Accessibility (a11y)
- Label: Wrap in
<fieldset>with<legend>(e.g., "Mã xác thực") - Each cell:
aria-label="Digit N of M"(e.g., "Digit 1 of 6") - Error:
aria-invalid="true"on all cells +aria-describedby→ error message - Screen reader: Announce "Enter verification code, N digits"
- Keyboard: Tab focuses component, arrow keys navigate between cells
- AutoFill:
autocomplete="one-time-code"on hidden unified input
8. CSS Skeleton
css
/* ─── Container ─── */
.otp-input {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
/* ─── Group ─── */
.otp-input__group {
display: flex;
gap: var(--spacing-2);
}
/* ─── Separator ─── */
.otp-input__separator {
width: var(--spacing-4);
display: flex;
align-items: center;
justify-content: center;
color: var(--border);
font-size: var(--font-size-lg);
}
/* ─── Slot (individual cell) ─── */
.otp-input__slot {
width: var(--spacing-12); /* 48px */
height: var(--spacing-14); /* 56px */
border: 1px solid var(--input-border);
border-radius: var(--comp-radius);
background: var(--input);
color: var(--foreground);
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
text-align: center;
caret-color: var(--primary);
outline: none;
transition: border-color var(--duration-fast-2) var(--easing-standard);
font-family: inherit;
}
.otp-input__slot:focus {
border-color: var(--primary);
border-width: 2px;
box-shadow: 0 0 0 3px var(--primary-muted);
}
.otp-input__slot--filled {
border-color: var(--input-border);
}
/* Error state */
.otp-input--error .otp-input__slot {
border-color: var(--destructive);
}
.otp-input--error .otp-input__slot:focus {
border-color: var(--destructive);
box-shadow: 0 0 0 3px var(--destructive-muted);
}
/* Disabled */
.otp-input--disabled .otp-input__slot {
opacity: var(--disabled);
pointer-events: none;
background: var(--muted);
}
/* Error message */
.otp-input__error {
font-size: var(--font-size-xs);
color: var(--destructive);
margin-top: var(--spacing-2);
}
/* Cursor blink animation */
.otp-input__slot--active::after {
content: '';
position: absolute;
width: 2px;
height: 60%;
background: var(--primary);
animation: otp-cursor 1s step-end infinite;
}
@keyframes otp-cursor {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.otp-input__slot { transition: none; }
.otp-input__slot--active::after { animation: none; opacity: 1; }
}
9. Example Usage
jsx
{/* Default 6-digit OTP */}
<OTPInput onComplete={(code) => verifyOTP(code)} />
{/* 4-digit PIN */}
<OTPInput length={4} groupPattern={[4]} onComplete={verifyPIN} />
{/* With error */}
<OTPInput error errorMessage="Mã OTP không đúng. Vui lòng thử lại." />
{/* With countdown timer */}
<div>
<OTPInput onComplete={verify} />
<p class="caption muted">Gửi lại mã sau <strong>45s</strong></p>
</div>