Input OTP

An accessible one-time code field with alphanumeric, masked, and grouped variants for verification and two-factor flows.

Loading preview

Examples

Practical examples and common states for the same installable component.

4-digit PIN

Short numeric codes for device PINs and quick unlock flows.

examples/input-otp-pin.tsx
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpPin() {
return (
<InputOtp
label="Device PIN"
mode="numeric"
length={4}
masked
hint="Use your PIN."
/>
);
}

8-digit backup code

Alphanumeric recovery codes with custom group separators.

examples/input-otp-backup-code.tsx
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpBackupCode() {
return (
<InputOtp
label="Backup code"
mode="alphanumeric"
length={8}
groups={[4, 4]}
separator="-"
hint="Use backup code."
/>
);
}

Alphanumeric

Backup codes accept letters and numbers.

examples/input-otp-alphanumeric.tsx
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpAlphanumeric() {
return (
<InputOtp
label="Backup code"
mode="alphanumeric"
length={6}
hint="Alphanumeric only."
/>
);
}

Read-only verified

Show a confirmed code without the muted disabled treatment.

examples/input-otp-readonly.tsx
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpReadOnly() {
return (
<InputOtp
label="Verified code"
mode="numeric"
groups={[3, 3]}
separator=" "
value="120805"
readOnly
status="success"
success="Code verified."
/>
);
}

Controlled reset

Remount with key={attempt} to clear slots after external actions.

Requires: button

examples/input-otp-reset.tsx
import { useState } from "react";
import { Button } from "@/components/wensity/button";
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpReset() {
const [attempt, setAttempt] = useState(0);
const [value, setValue] = useState("");
return (
<>
<InputOtp
key={attempt}
label="Authentication code"
value={value}
onValueChange={setValue}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
setValue("");
setAttempt((current) => current + 1);
}}
>
Reset field
</Button>
</>
);
}

Try again / resend

Recovery actions after a failed verification attempt.

Requires: button

examples/input-otp-resend.tsx
import { useState } from "react";
import { Button } from "@/components/wensity/button";
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpResend() {
const [attempt, setAttempt] = useState(0);
const [value, setValue] = useState("");
const [status, setStatus] = useState<"default" | "error" | "success">("default");
const reset = () => {
setValue("");
setStatus("default");
setAttempt((current) => current + 1);
};
return (
<>
<InputOtp
key={attempt}
label="SMS code"
value={value}
status={status}
onValueChange={setValue}
onFilled={(code) => setStatus(code === "120805" ? "success" : "error")}
error={status === "error" ? "Invalid code." : undefined}
/>
{status === "error" ? (
<>
<Button type="button" variant="secondary" onClick={reset}>
Try again
</Button>
<Button type="button" variant="ghost" onClick={reset}>
Resend code
</Button>
</>
) : null}
</>
);
}

Masked

Hide entered characters for PIN-style entry.

examples/input-otp-masked.tsx
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpMasked() {
return (
<InputOtp
label="Security PIN"
mode="numeric"
masked
hint="Hidden entry."
/>
);
}

Separated

Group digits with separators for easier scanning.

examples/input-otp-separated.tsx
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpSeparated() {
return (
<InputOtp
label="Authenticator code"
mode="numeric"
groups={[3, 3]}
separator="·"
hint="Grouped code."
/>
);
}

Form

Compose with FormField for labels and validation layout.

Requires: buttonform-field

examples/input-otp-form.tsx
import { Button } from "@/components/wensity/button";
import {
FormActions,
FormField,
FormFieldControl,
} from "@/components/wensity/form-field";
import { InputOtp } from "@/components/wensity/input-otp";
export function InputOtpForm() {
return (
<form onSubmit={(event) => event.preventDefault()}>
<FormField label="Authentication code" required>
<FormFieldControl asChild>
<InputOtp
mode="numeric"
groups={[3, 3]}
hint="Use authenticator code."
/>
</FormFieldControl>
</FormField>
<FormActions className="mt-4">
<Button type="submit">Verify</Button>
</FormActions>
</form>
);
}

Props

PropTypeDefaultDescription
lengthnumber6Number of digit slots.
mode"numeric" | "alphanumeric""numeric"Which characters are accepted, and the mobile keyboard shown.
valuestring-Controlled value.
defaultValuestring""Initial value when uncontrolled.
onValueChange(value: string) => void-Called on every change to the value.
onComplete(value: string) => void-Fires once every slot is filled by the user. Requires autoSubmit.
onFilled(value: string) => void-Fires when every slot is filled, without submitting. Never fires on mount.
autoSubmitbooleanfalseCalls onComplete as soon as the last slot is filled.
maskedbooleanfalseHides entered characters, for codes treated as secrets.
groupsnumber[]-Slot grouping for separated layouts. Must sum to length — [3, 3] renders 123 . 456.
separatorReact.ReactNode"•"Node rendered between digit groups.
status"default" | "error" | "success"-Drives the success and error visuals and which message is shown.
errorReact.ReactNode-Message shown when status is error.
successReact.ReactNode-Message shown when status is success.
loadingbooleanfalseVerifying state: locks input, sets aria-busy, and pulses the slots.
loadingMessageReact.ReactNode"Verifying…"Screen-reader and helper copy while loading is true.
readOnlybooleanfalseLocks input without the muted disabled styling. Useful after submit.
inputSize"sm" | "md" | "lg""md"Slot size and text size.
fullWidthbooleantrueStretches the slot row to fill its container.