PE06▸⌨️ Input · 進階輸入▸OTP
OTP Input
OTP 驗證碼
難度 ★★★★★標籤 otp · paste · 焦點3 min read
模式簡述
OTP Input:6 格分離的驗證碼輸入。Apple、Google 都這樣做。要支援自動跳格、整段貼上、autofill。
驗證碼已寄到 +886 ****1234
Claude Code Prompt
參考:PE06 otp 組件:OTP.tsx OTP Input:6 格分離的驗證碼輸入。Apple、Google 都這樣做。要支援自動跳格、整段貼上、autofill。 限制: - 沒處理 paste - autofill 沒寫 - 錯誤後沒清空
完整程式碼
"use client";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
type State = "empty" | "typing" | "complete" | "error";
export function OTP({ state = "empty" }: { state?: State }) {
const init =
state === "complete" || state === "error"
? ["4", "2", "0", "6", "9", "1"]
: state === "typing"
? ["4", "2", "0", "", "", ""]
: ["", "", "", "", "", ""];
const [vals, setVals] = useState<string[]>(init);
useEffect(() => setVals(init), [state]);
const focusIdx = vals.findIndex((v) => v === "");
return (
<div className="w-full max-w-md">
<p className="mb-3 font-mono text-[11px] text-muted-foreground">驗證碼已寄到 +886 ****1234</p>
<div className="flex justify-center gap-2">
{vals.map((v, i) => (
<div
key={i}
className={cn(
"grid h-12 w-10 place-items-center rounded-md border bg-background/50 font-mono text-lg transition-colors",
v ? "border-border text-foreground" : "border-border/60 text-muted-foreground",
i === focusIdx && state === "typing" && "border-lab-accent ring-2 ring-lab-accent/30",
state === "complete" && "border-emerald-500/60",
state === "error" && "border-red-500/70 text-red-500",
)}
>
{v || (i === focusIdx && state === "typing" ? "_" : "")}
</div>
))}
</div>
{state === "error" && (
<p className="mt-3 text-center font-mono text-[11px] text-red-500">驗證碼錯誤,請重試</p>
)}
{state === "complete" && (
<p className="mt-3 text-center font-mono text-[11px] text-emerald-500">驗證成功 ✓</p>
)}
</div>
);
}何時用
- ✅ 手機驗證碼登入
- ✅ 兩步驟驗證
- ✅ 硬體 token 驗證
何時別用
- ❌ 驗證碼超過 8 位(直接用一般輸入)
- ❌ 驗證碼包含字母大小寫(分格反而難)
- ❌ 桌面端為主時(手機優化形式)
我踩過的坑
-
沒處理 paste:使用者複製 6 位數要貼一格貼一次。要攔截 paste 自動分配。
-
autofill 沒寫:iOS 簡訊 OTP 應該自動填,name=‘one-time-code’ + autocomplete=‘one-time-code’。
-
錯誤後沒清空:使用者重打要先刪掉。reset + focus 第一格。
相關模式
- PA05
- PE03