Skip to content

Input OTP

Enter and verify a one-time code, with a countdown before resending.

import { InputOTP, InputOTPResend } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use it for short codes people copy from somewhere else: an authenticator app, an email, a text message, a recovery sheet. It's one real input, laid over 4 to 8 slots that only draw it, so SMS autofill and password managers work, paste fills the whole code, and a screen reader meets one labeled field instead of six. For a secret people choose themselves, use a Password Input. For a quantity, use a Number Input.

import { Field, FieldLabel, InputOTP, useOTPVerification } from '@youj/react'

export function VerifyCode() {
  const otp = useOTPVerification(async (code) => {
    const res = await fetch('/api/verify', { method: 'POST', body: code })
    return res.ok
  })
  return (
    <Field invalid={otp.status === 'error'}>
      <FieldLabel>Authentication code</FieldLabel>
      <InputOTP groups={[3, 3]} {...otp.field} />
    </Field>
  )
}

useOTPVerification(verify) runs verify once the last slot fills and drives status through verifying to success or error. Resolve true, or nothing, for success. Resolve false, or throw, for an error: the row shakes, and the code clears once the shake has played so the next attempt starts empty. Spread its field onto InputOTP, and call reset() to start over.

Reference

Propslength, value, defaultValue… (28)

Props

InputOTP props
PropType
lengthnumber

Number of characters, 4–8.

default 6

6
valuestring–
defaultValuestring

default ''

''
onValueChange(value: string) => void–
onComplete(value: string) => void

Called once each time the last slot is filled.

–
autoSubmitboolean

Submit the owning form when the code is complete.

default false

false
modeOTPMode
'numeric''alphanumeric'

Digits only (default), or letters and digits (upper-cased), e.g. recovery codes.

default 'numeric'

'numeric'
groupsnumber[]

Slot grouping, e.g. [3, 3]. Groups are split by a dash. Default: one group.

–
statusOTPStatus
'idle''verifying''success''error'

verifying: read-only with a spinner. success: green slots and a drawn check. error: a shake.

default 'idle'

'idle'
statusLabels{ verifying?: string; success?: string; error?: string }

Announced for each status.

–
classNamestring

CSS class applied to the element, or a function that returns a class based on the component's state.

–
refRef<HTMLInputElement>

Allows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref).

–
Other props pass through: extends InputPrimitive.Props.

Other Base UI Input props pass through, such as name, aria-label and onFocus.

InputOTPResend

InputOTPResend props
PropType
onResendRequired() => unknown

Sends a new code. Resolve for success; reject to show the error.

–
secondsnumber

Seconds to wait between sends.

default 30

30
autoStartboolean

Start waiting straight away (a code was just sent).

default true

true
waitingLabel(time: string) => string

The label while waiting.

default `Resend in 0:24`

`Resend in 0:24`
childrenstring–
variantButtonVariant
'default''danger''ghost''outline''link''secondary'

default | secondary | outline | ghost | danger | link.

default 'ghost'

'ghost'
size'default' | 'sm' | 'lg' | 'xs' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'

default 'sm'

'sm'
soundboolean

With sound on (YoujProvider’s sound), the button plays tap as it's pressed. false keeps this one silent.

default true

true
pendingLabelstring

Shown beside the spinner while the action runs, and announced. Announced as "Working…" when omitted.

–
successLabelstring | ((result: TResult) => string)

Shown beside the check and announced. A function gets what onAction resolved with: successLabel={(user) => Saved ${user.name}}. Under status there is no outcome to read, so pass a string.

default "Done"

"Done"
errorLabelstring | ((error: unknown) => string)

Shown beside the cross and announced assertively. A function gets what onAction rejected or threw with, whatever that is: errorLabel={(error) => error instanceof Error ? error.message : 'Failed'}. Under status, pass a string (errorLabel={mutation.error?.message}).

default "Failed"

"Failed"
resetAfternumber

How long success / error hold before the label returns, in ms. Default: 3 × the deliberate token.

–
onActionStateChange(state: AsyncState) => void

Fires on every state change: idle → pending → success | error → idle.

–
holdboolean | number

Hold to confirm: the action runs only once a press is held (a pointer, or Space or Enter held down) for this long. true takes twice the deliberate motion token; a number is milliseconds. The button fills from its inline start while held and springs back if let go early; a full hold runs the action and the button carries on through pending to success or error. It is described as "Hold to confirm".

–
MotionCaret frame, character in, caret blink… · reduced motion

Motion

Motion tokens
MomentTokens
Caret frameone focus frame glides from slot to slot as you type or arrow (Sliding Indicator)
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Character ineach character rises 4px into its slot
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Caret blinkin an empty active slot
duration.deliberateease.linear

560ms · cubic-bezier(0, 0, 1, 1) · × 2 per blink

560mscubic-bezier(0, 0, 1, 1)× 2 per blink
Successslot borders turn green from the inline start, one tight stagger step apart
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
Success checkdraws on beside the slots
duration.slowease.enter

340ms · cubic-bezier(0.16, 1, 0.3, 1)

340mscubic-bezier(0.16, 1, 0.3, 1)
Error shake4, 3 and 2px, decaying
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Clear after errorwith useOTPVerification, once the shake has played
duration.slow

340ms

340ms

Reduced motion

The frame jumps between slots, characters appear in place, the caret holds still, success tints every slot at once and errors don't shake.

Reduced motion

The focus frame jumps to the active slot, characters appear with no rise, the caret doesn't blink, success tints every slot at once and the check appears drawn, and an error marks the field without a shake. With useOTPVerification, the code clears straight after an error.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • It's a single <input> with autocomplete="one-time-code", inputmode="numeric" (or text in alphanumeric mode) and a pattern. The slots are hidden from assistive technology, so the field has one label and one value.
  • Arrow keys move between slots (mirrored in right-to-left layouts), Home and ArrowUp go to the first, End and ArrowDown to the last, and typing over a filled slot replaces it.
  • While verifying, the input is read-only and aria-busy. An error sets aria-invalid, so a surrounding Field shows its message.
  • Verifying and success are announced politely, and errors assertively, through a live region beside the field.
  • On touch screens the hidden text is 16px, so iOS doesn't zoom into the field on focus.