Nhập OTP
Nhập mã dùng một lần cho xác thực hai yếu tố (2FA), xác minh đăng nhập và màn hình nhập mã PIN — các ô hỗ trợ dán mã với viền theo phong cách tân brutalist và hiệu ứng bóng đậm.
import {
InputOTP,
InputOTPGroup,Input OTP là ô nhập mã dùng một lần được chia thành từng ô: các ô hiển thị được vẽ lên trên một input native duy nhất, nên việc dán mã và tự động điền mã trên mobile hoạt động ngay từ đầu. Cả hai biến thể backend đều bọc cùng một thư viện input-otp của @guilherme_rodz — bên dưới không có primitive nào của Radix hay Base — với các ô được tạo kiểu theo công thức neobrutalist: viền dày, bóng đổ đậm và kiểu chữ đậm.
Nên dùng khi:
- Xác thực hai yếu tố — mã TOTP hoặc mã SMS 6 chữ số sau khi đăng nhập bằng mật khẩu.
- Xác minh email và số điện thoại — xác nhận quyền sở hữu khi đăng ký, thanh toán hoặc khôi phục tài khoản.
- Nhập mã PIN — mã 4 chữ số cho khóa màn hình hoặc xác nhận thanh toán (xem ví dụ Bốn chữ số).
Giới thiệu#
Input OTP được xây dựng dựa trên input-otp của @guilherme_rodz.
Cài đặt#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/input-otp.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/input-otp.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/input-otp.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/input-otp.json
Cài đặt các phụ thuộc sau:
pnpm add input-otpnpm install input-otpyarn add input-otpbun add input-otp
Sao chép và dán đoạn mã sau vào dự án của bạn.
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
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 has-aria-invalid:border-destructive",
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-2 border-r-2 bg-input text-sm shadow-sm transition-all outline-none first:rounded-l first:border-l-2 last:rounded-r aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:outline-2 data-[active=true]:outline-primary data-[active=true]:aria-invalid:border-destructive",
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 }
Cập nhật các đường dẫn import cho phù hợp với thiết lập dự án của bạn.
Cách dùng#
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Kết hợp#
Dùng cách kết hợp sau để xây dựng một InputOTP:
InputOTP
├── InputOTPGroup
│ ├── InputOTPSlot
│ ├── InputOTPSlot
│ └── InputOTPSlot
├── InputOTPSeparator
├── InputOTPGroup
│ ├── InputOTPSlot
│ ├── InputOTPSlot
│ └── InputOTPSlot
├── InputOTPSeparator
└── InputOTPGroup
├── InputOTPSlot
└── InputOTPSlotPattern#
Dùng prop pattern để định nghĩa một mẫu tùy chỉnh cho ô nhập OTP.
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"
;<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
...
</InputOTP>"use client"
import { REGEXP_ONLY_DIGITS } from "input-otp"Ví dụ#
Dấu phân tách#
Dùng thành phần <InputOTPSeparator /> để thêm một dấu phân tách giữa các nhóm ô nhập.
import {
InputOTP,
InputOTPGroup,Vô hiệu hóa#
Dùng prop disabled để vô hiệu hóa ô nhập.
import { Field, FieldLabel } from "@/components/ui/field"
import {
InputOTP,Được kiểm soát#
Dùng các prop value và onChange để kiểm soát giá trị của ô nhập.
"use client"
import * as React from "react"Không hợp lệ#
Dùng aria-invalid trên các slot để hiển thị trạng thái lỗi.
"use client"
import * as React from "react"Bốn chữ số#
Một mẫu phổ biến cho mã PIN. Nó dùng prop pattern={REGEXP_ONLY_DIGITS}.
"use client"
import { REGEXP_ONLY_DIGITS } from "input-otp"Chữ và số#
Dùng REGEXP_ONLY_DIGITS_AND_CHARS để chấp nhận cả chữ cái và số.
"use client"
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"Form#
import { RefreshCwIcon } from "lucide-react"
import { Button } from "@/components/ui/button"RTL#
Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.
"use client"
import * as React from "react"Khả năng truy cập#
Không có mẫu WAI-ARIA APG nào cho ô nhập OTP, và cũng không cần đến: input-otp render một thẻ <input> thật với autocomplete="one-time-code" nằm sau các ô hiển thị, nên công nghệ hỗ trợ chỉ đọc lên một ô văn bản duy nhất và bàn phím trên mobile có thể gợi ý mã vừa nhận được. Các ô hiển thị chỉ mang tính trình bày — hãy gắn nhãn cho chính ô nhập (bằng FormLabel hoặc aria-label), và truyền aria-invalid xuống các ô để tạo kiểu cho trạng thái lỗi.
Tương tác bàn phím theo đúng hành vi của ô văn bản native và giống nhau ở cả biến thể Radix và Base:
| Phím | Hành động |
|---|---|
Tab / Shift + Tab | Chuyển focus vào / ra khỏi ô nhập (chỉ một điểm dừng tab) |
ArrowLeft / ArrowRight | Di chuyển con trỏ giữa các ô |
Home / End | Nhảy đến ô đầu / ô cuối |
Backspace / Delete | Xóa ký tự trước / ký tự sau |
Ctrl/Cmd + V | Dán — mã sẽ điền vào các ô nếu khớp với pattern |
Tham chiếu API#
Xem tài liệu input-otp để biết thêm thông tin.