Prisma
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>