OTP field
A one-time code in one frame, a cell per character. While you check it, a segment runs along the bottom edge; a wrong code turns the frame red and puts the caret back at the start.
Loading the live example…
"use client"
import { useEffect, useRef, useState } from "react"
import { OtpField, type OtpSignal } from "@/components/keyline/ui/otp-field"
const CODE = "271828" // what your server would check
export function SignInCode() {
const [code, setCode] = useState("")
const [signal, setSignal] = useState<OtpSignal>("idle")
const timer = useRef<ReturnType<typeof setTimeout>>(undefined)
useEffect(() => () => clearTimeout(timer.current), [])
return (
<div className="grid max-w-[360px] gap-3">
<OtpField
label="Code from your phone"
length={6}
groups={[3, 3]}
value={code}
signal={signal}
error="That code doesn't match. Codes last 10 minutes."
description={`Sent to the number ending 4417. For this demo, it is ${CODE}.`}
onValueChange={(v) => {
// after a wrong code, the first new character starts a fresh code, so completing it checks again
if (signal === "failed") {
setSignal("idle")
setCode(v.slice(0, 1))
} else setCode(v)
}}
onValueComplete={(v) => {
setSignal("pending")
timer.current = setTimeout(() => setSignal(v === CODE ? "done" : "failed"), 900)
}}
/>
<p role="status" className="text-[14px] text-kl-ink-2">
{signal === "done" ? "Signed in to Dunder Mifflin Infinity." : "Type or paste the six digits."}
</p>
</div>
)
}Installation
npx shadcn@latest add @keyline-ui/otp-fieldRun it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.
Usage
import { OtpField } from "@/components/keyline/ui/otp-field"<OtpField
label="Code from your phone"
length={6}
groups={[3, 3]}
signal={signal}
error="That code doesn't match."
onValueComplete={verify}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | Default — | Names the first cell; the rest are “Character n of N”. Required. |
length | number | Default — | How many characters. Required. |
groups | number[] | Default — | Splits the cells, such as [3, 3]. Must add up to length. |
onValueComplete | (code: string) => void | Default — | Runs once every cell is filled, typed or pasted. Check the code here. |
signal | "idle" | "pending" | "done" | "failed" | Default "idle" | pending makes the cells read-only while you check. failed turns the frame red and returns the caret to the first cell; pair it with error. |
validationType | "numeric" | "alpha" | "alphanumeric" | "none" | Default "numeric" | Which characters a cell takes. |
mask | boolean | Default false | Hides the characters, for a PIN. |
autoSubmit | boolean | Default false | Submits the form once the code is complete. |
error | ReactNode | Default — | The message when a check fails. With invalid it always shows. |
description | ReactNode | Default — | A line under the field, read out with it. |
layout | "control" | "cell" | Default "control" | cell drops the frame and hides the label, to sit inside another frame. |
Pasting a whole code fills every cell; Backspace in an empty cell goes back one. Built on Base UI OTP Field.