Input OTP
จาก shadcnช่องกรอกรหัสยืนยันแบบทีละหลัก พร้อมตัวคั่นกลุ่มตัวเลข เช่น รหัส 6 หลักจาก SMS ก่อนเข้าถึงคำขอกู้
รหัสหมดอายุใน 5 นาที ส่งถึงมือถือที่ลงทะเบียนไว้
4
2
1
ติดตั้ง
Terminal
pnpm dlx shadcn@latest add @whereui/input-otpต้องผูก registry และติดตั้ง @whereui/theme นำหน้าเสมอเมื่อติดตั้งครั้งแรก
การใช้งาน
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"<InputOTP maxLength={6}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> </InputOTPGroup></InputOTP>ข้อควรรู้
- maxLength ต้องเท่ากับจำนวน InputOTPSlot ทั้งหมด และ index เรียงต่อเนื่องจาก 0
- วาง InputOTPSeparator ระหว่างกลุ่มเพื่อแยกหลัก เช่น 3-3 สำหรับรหัส 6 หลัก
- รองรับวางรหัสทั้งกลุ่มจากคลิปบอร์ดได้ทันที
ซอร์สโค้ด
components/ui/input-otp.tsx
"use client"import * as React from "react"import { cn } from "cn"import { OTPInput, OTPInputContext } from "input-otp"import { MinusIcon } from "lucide-react"function InputOTP({ className, containerClassName, ...props}: React.ComponentProps<typeof OTPInput> & { containerClassName?: string}) { return ( <OTPInput data-slot="input-otp" containerClassName={cn( "cn-input-otp flex items-center has-disabled:opacity-50", containerClassName )} spellCheck={false} className={cn("disabled:cursor-not-allowed", className)} {...props} /> )}function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="input-otp-group" className={cn( "flex items-center rounded-lg has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40", className )} {...props} /> )}function InputOTPSlot({ index, className, ...props}: React.ComponentProps<"div"> & { index: number}) { const inputOTPContext = React.useContext(OTPInputContext) const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {} return ( <div data-slot="input-otp-slot" data-active={isActive} className={cn( "relative flex size-8 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-3 data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40", className )} {...props} > {char} {hasFakeCaret && ( <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> </div> )} </div> )}function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { return ( <div data-slot="input-otp-separator" className="flex items-center [&_svg:not([class*='size-'])]:size-4" role="separator" {...props} > <MinusIcon /> </div> )}export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }