Enter the 6-digit code we sent to ada@example.com.
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { OTPField, OTPFieldInput } from "@/components/ui/otp-field";
export default function OTPFieldBasic() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="verification-code">Verification code</FieldLabel>
<OTPField id="verification-code" length={6}>
{Array.from({ length: 6 }, (_, index) => (
<OTPFieldInput key={index} />
))}
</OTPField>
<FieldDescription>Enter the 6-digit code we sent to ada@example.com.</FieldDescription>
</Field>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add otp-field
pnpm dlx shadcn@latest add @jarvis/otp-field
Usage
import { Field, FieldLabel } from "@/components/ui/field";
import { OTPField, OTPFieldInput } from "@/components/ui/otp-field";
export function CodeField() {
return (
<Field>
<FieldLabel htmlFor="code">Code</FieldLabel>
<OTPField id="code" length={4}>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPField>
</Field>
);
}Render one OTPFieldInput per character, matching length. An id on OTPField goes to the
first box, so a label can point at it.
Composition
Examples
Checking the code
onValueComplete runs once every box is filled. Set invalid on the Field and aria-invalid
on the inputs, and show a FieldError when the code is wrong.
Try 424242, or any other code to see an error.
import * as React from "react";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { OTPField, OTPFieldInput } from "@/components/ui/otp-field";
import { toast } from "@/components/ui/toast";
export default function OTPFieldVerify() {
const [code, setCode] = React.useState("");
const [invalid, setInvalid] = React.useState(false);
return (
<Field invalid={invalid} className="w-fit">
<FieldLabel htmlFor="sign-in-code">Sign-in code</FieldLabel>
<OTPField
id="sign-in-code"
length={6}
value={code}
onValueChange={(value) => {
setCode(value);
setInvalid(false);
}}
onValueComplete={(value) => {
if (value === "424242") {
setCode("");
toast.add({ title: "Code verified", type: "success" });
} else {
setInvalid(true);
}
}}
>
{Array.from({ length: 6 }, (_, index) => (
<OTPFieldInput key={index} aria-invalid={invalid || undefined} />
))}
</OTPField>
{invalid ? (
<FieldError>That code didn't work. Check it and try again.</FieldError>
) : (
<FieldDescription>Try 424242, or any other code to see an error.</FieldDescription>
)}
</Field>
);
}Groups
Split a long code into OTPFieldGroups with an OTPFieldSeparator between them.
import { Field, FieldLabel } from "@/components/ui/field";
import {
OTPField,
OTPFieldGroup,
OTPFieldInput,
OTPFieldSeparator,
} from "@/components/ui/otp-field";
export default function OTPFieldGrouped() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="authenticator-code">Authenticator code</FieldLabel>
<OTPField id="authenticator-code" length={6}>
<OTPFieldGroup>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPFieldGroup>
<OTPFieldSeparator />
<OTPFieldGroup>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPFieldGroup>
</OTPField>
</Field>
);
}Large
import { Field, FieldLabel } from "@/components/ui/field";
import {
OTPField,
OTPFieldGroup,
OTPFieldInput,
OTPFieldSeparator,
} from "@/components/ui/otp-field";
export default function OTPFieldLarge() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="device-code">Device code</FieldLabel>
<OTPField id="device-code" length={6} size="lg">
<OTPFieldGroup>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPFieldGroup>
<OTPFieldSeparator />
<OTPFieldGroup>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPFieldGroup>
</OTPField>
</Field>
);
}Letters and numbers
The field accepts only digits by default. Set validationType="alphanumeric" to allow letters, and
normalizeValue to transform input, such as to uppercase.
Letters are converted to uppercase.
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
OTPField,
OTPFieldGroup,
OTPFieldInput,
OTPFieldSeparator,
} from "@/components/ui/otp-field";
export default function OTPFieldAlphanumeric() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="recovery-code">Recovery code</FieldLabel>
<OTPField
id="recovery-code"
length={8}
validationType="alphanumeric"
normalizeValue={(value) => value.toUpperCase()}
>
<OTPFieldGroup>
{Array.from({ length: 4 }, (_, index) => (
<OTPFieldInput key={index} />
))}
</OTPFieldGroup>
<OTPFieldSeparator />
<OTPFieldGroup>
{Array.from({ length: 4 }, (_, index) => (
<OTPFieldInput key={index} />
))}
</OTPFieldGroup>
</OTPField>
<FieldDescription>Letters are converted to uppercase.</FieldDescription>
</Field>
);
}Masked
mask hides the characters, like a password field.
import { Field, FieldLabel } from "@/components/ui/field";
import { OTPField, OTPFieldInput } from "@/components/ui/otp-field";
export default function OTPFieldMasked() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="pin">PIN</FieldLabel>
<OTPField id="pin" length={4} mask>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPField>
</Field>
);
}Disabled
import { Field, FieldLabel } from "@/components/ui/field";
import { OTPField, OTPFieldInput } from "@/components/ui/otp-field";
export default function OTPFieldDisabled() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="previous-pin">Previous PIN</FieldLabel>
<OTPField id="previous-pin" length={4} defaultValue="4821" mask disabled>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPField>
</Field>
);
}Props
| Part | Extends |
|---|---|
| OTPField | Base UI's OTPField.Root |
| OTPFieldGroup | <div> |
| OTPFieldInput | Base UI's OTPField.Input |
| OTPFieldSeparator | Base UI's OTPField.Separator |
The tables leave out className, style and render. Parts built on Base UI take them as its styling and composition guides describe.
OTPField
| Prop | Type | Default |
|---|---|---|
idThe id of the first input element.
Subsequent inputs derive their ids from it ( | string | — |
autoCompleteThe input autocomplete attribute. Applied to the first slot and hidden validation input. | string | 'one-time-code' |
formA string specifying the | string | — |
lengthThe number of OTP input slots. Required so the root can clamp values, detect completion, and generate consistent validation markup before all slots hydrate. | number | — |
autoSubmitWhether to submit the owning form when the OTP becomes complete. | boolean | false |
maskWhether the slot inputs should mask entered characters.
Pass | boolean | false |
inputModeThe virtual keyboard hint applied to the slot inputs and hidden validation input. Built-in validation modes provide sensible defaults, but you can override them when needed. | "decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | — |
validationTypeThe type of input validation to apply to the OTP value. | "alpha" | "alphanumeric" | "none" | "numeric" | 'numeric' |
normalizeValueFunction that normalizes the OTP value after whitespace and The returned value is filtered by | ((value: string) => string) | — |
requiredWhether the user must enter a value before submitting a form. | boolean | false |
disabledWhether the component should ignore user interaction. | boolean | false |
readOnlyWhether the user should be unable to change the field value. | boolean | false |
nameIdentifies the field when a form is submitted. | string | — |
valueThe OTP value. | string | — |
defaultValueThe uncontrolled OTP value when the component is initially rendered. | string | — |
onValueChangeCallback fired when the OTP value changes. The
| ((value: string, eventDetails: OTPField.Root.ChangeEventDetails) => void) | — |
onValueInvalidCallback fired when entered text contains characters that are rejected by validation or normalization before the OTP value updates. The | ((value: string, eventDetails: OTPField.Root.InvalidEventDetails) => void) | — |
onValueCompleteCallback function that is fired when the OTP value becomes complete, or when a complete value is pasted while the OTP is already complete. When the value changes, it runs later than If | ((value: string, eventDetails: OTPField.Root.CompleteEventDetails) => void) | — |
size | "default" | "lg" | "default" |
OTPFieldSeparator
| Prop | Type | Default |
|---|---|---|
orientationThe orientation of the separator. | "horizontal" | "vertical" | 'horizontal' |