Forms
OtpField
A one-time code entry on the Field contract: one slot per character, with focus that moves as the person types, a whole code pasted at once, and autoComplete="one-time-code" so phones offer the code from a text message. Pass label, length, and error from your server action; onValueComplete or autoSubmit act when every slot is filled.
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/otp-field#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/otp-fieldIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/otp-field#v1.2.13 ourostack/ouro-ui/otp-field-tests#v1.2.13import { OtpField } from "@/components/ouro/otp-field/otp-field";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<OtpField label="Verification code" name="code" length={6} autoSubmit error={errors?.code} />
<OtpField label="Recovery code" validationType="alphanumeric" length={8} groupSize={4} />Use instead when
- Use
TextFieldwithinputMode="numeric"instead for a number with no fixed length, andTextField type="password"for a password.
Props
Type: OtpFieldProps.
autoComplete- Type:
string - Default:
"one-time-code" - Autofill for the first slot. The default lets phones offer a code from a text message.
autoSubmit- Type:
boolean - Submits the form when every slot is filled.
className- Type:
string - Layout only: margins, width, grid placement (
mt-4). Appearance classes warn in development; style fields in your theme. defaultValue- Type:
string - The code the field starts with, when it keeps its own state.
description- Type:
ReactNode - Supporting text under the label ("We sent it to your phone"), read through
aria-describedby. disabled- Type:
boolean - Disables every slot and fades the field.
error- Type:
ReadonlyErrorMessages - What is wrong ("That code has expired"), from your server action or client check. It shows under the slots, which get
aria-invalid. A message is text or a React node (one with a link); an empty string or array means valid. groupSize- Type:
number - Shows the slots in groups of this many, with a dash between groups:
groupSize={3}reads123-456. hideLabel- Type:
boolean - Hides the label visually; screen readers still read it.
label(required)- Type:
string - What the code is ("Verification code"), shown above the slots and read as the first slot's name. Required: every control needs a name. To hide it visually, keep it and add
hideLabel. length- Type:
number - Default:
6 - How many characters the code has: one slot each.
mask- Type:
boolean - Hides the characters as dots, as a password field does.
name- Type:
string - The form field name the whole code submits under.
onValueChange- Type:
((value: string) => void) - Called with the code as the person types, pastes or deletes.
onValueComplete- Type:
((value: string) => void) - Called with the code when every slot is filled.
readOnly- Type:
boolean - Shows the code without letting the person change it.
ref- Type:
Ref<HTMLDivElement> - The slots' group element (
role="group"). required- Type:
boolean - The code must be filled before the form submits.
slotLabel- Type:
((position: number, length: number) => string) - Default: (position, length) =>
Character ${position} of ${length} - The name of each slot after the first (the first takes
label). Translate it with your app. validationType- Type:
"numeric" | "alpha" | "alphanumeric" - Default:
"numeric" - Which characters the slots take. Others are dropped as the person types or pastes.
numericbrings up the number keyboard on phones. value- Type:
string - The code, when you control it. Pair it with
onValueChange.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
errorMessage: Not a prop. Ouro UI useserror:<OtpField error="Wrong code.">.helperText: Not a prop. Ouro UI usesdescription:<OtpField description="...">.invalid: Not a prop.errormarks the field invalid:<OtpField error="What is wrong">.isDisabled: Not a prop. Ouro UI usesdisabled:<OtpField disabled>.isInvalid: Not a prop.errormarks the field invalid:<OtpField error="What is wrong">.isRequired: Not a prop. Ouro UI usesrequired:<OtpField required>.maxLength: Not a prop. Ouro UI useslength:<OtpField length={6}>.numInputs: Not a prop. Ouro UI useslength:<OtpField length={4}>.onChange: Not a prop. Ouro UI usesonValueChange, which gets the code:<OtpField onValueChange={(code) => ...}>.onComplete: Not a prop. Ouro UI usesonValueComplete:<OtpField onValueComplete={(code) => ...}>.pattern: Not a prop. Ouro UI usesvalidationType:<OtpField validationType="alphanumeric">.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: OtpField merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ErrorMessage
One error message: text, or a React element for a message that carries a link or other markup.
type ErrorMessage = string | ReactElement;ReadonlyErrorMessages
A read-only message view accepted by fields and Form; mutable input arrays remain valid.
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];Claim sources
- layout-classes: test:registry/ouro/components/ouro/otp-field/otp-field.test.tsx::OtpField > forwards its ref to the group and lets layout classes win on the root
verify:testExists(registry/ouro/components/ouro/otp-field/otp-field.test.tsx::OtpField > forwards its ref to the group and lets layout classes win on the root)