Skip to content
SuperCoach

Components · Inputs

Input OTP

Beta

Enters a one-time code during sign-in.

We sent a 6-digit code to your phone ending in 34.

When to use

  • To enter a short one-time code sent by SMS or email: sign-in, verifying a new phone number, confirming a payout.
  • For a short numeric PIN with a fixed length.

When not to use

  • For passwords or codes of unknown length: use an Input.
  • For long codes people paste rather than type (an API key, a discount code): use an Input.
  • For amounts, phone numbers or dates: those have their own formats and fields.

Anatomy

4
8
2
  1. 1. Label: linked to the hidden input with htmlFor
  2. 2. Group: slots joined into one box, one character each
  3. 3. Separator (optional): splits a 6-digit code into 3 + 3

Behind the slots is one real <input>. It receives typing, paste and the phone's code suggestion, and the slots draw its value.

Variants

LayoutUse for
6 slots in two groups of 3, with a separatorSMS and email codes. Groups make the code easier to read back and check. See the first example.
4 slots in one groupShort PINs. See the states example below.

Set maxLength to the number of slots and pattern={REGEXP_ONLY_DIGITS} (from input-otp) for numeric codes, so letters are rejected as they are typed.

States

2
7
4
1
1
9
3
7

That PIN is wrong. 2 tries left.

StateWhat changesToken
EmptySlot bordersborder-input
Active slotRing on the slot that receives the next character, with a blinking caretborder-ring, ring-ring/50, animate-caret-blink
FilledThe digit in the slottext-sm
DisabledHalf opacity on the whole field. Wrap label and field in group with data-disabled="true" to dim the label tooopacity-(--opacity-disabled)
InvalidRed slot borders, error text below. Set aria-invalid on the InputOTP (for assistive tech) and on each slot (for the style)border-destructive, text-destructive-text

Usage guidelines

Do.Slots match the code's length, and the field stays left to right in Arabic screens too.
Don't.Fewer slots than the code has digits. The last two digits disappear and the code fails.
  • Say where the code was sent and how long it is: "We sent a 6-digit code to your phone ending in 34."
  • Submit automatically when the last slot is filled (onComplete), but keep a visible button for people who paste or use a keyboard.
  • Offer "Resend code" with a countdown. On a wrong code, show the error below the field and keep focus in it.
  • Don't split one code across several inputs. This component is one input, so paste and the phone's code suggestion work.

Content guidelines

  • Label: "Verification code" (رمز التحقق), "Payout PIN". Not "OTP"; many clients don't know the term.
  • Errors say what happened and what's next: "That code has expired. We sent a new one."
  • Codes use Western digits (0–9) in both languages (Q22). Arabic copy around them is natural: "أرسلنا رمزًا من 6 أرقام".

Accessibility

KeyDoes
TabMoves focus to the field (one tab stop for all slots)
0–9Fills the active slot and moves to the next
BackspaceDeletes the last character
Arrow left / rightMoves the caret between slots
Ctrl or Cmd+VPastes the whole code
  • One <input> with autocomplete="one-time-code" and inputmode="numeric", so iOS and Android offer the code from SMS. Label it with htmlFor on the InputOTP id, or aria-label.
  • Announced as "Verification code, edit text", then the help text from aria-describedby. Each character is read as it is typed.
  • The slots are drawn from the input's value. They are not separate fields, so there is one name, one value and one error to announce.
  • Give people enough time: don't expire a code in less than a few minutes, and say how long it lasts (WCAG 2.2.1).

RTL

  • Codes read left to right in Arabic too, so the component renders its slots in a dir="ltr" container. In a right-to-left page the field still sits at the start (right), but the first digit stays on the left.
  • Upstream, the slots reversed in RTL: a code typed as 482915 showed as 519284 from left to right. This version fixes it in the component.
  • The label, help text and errors follow the page direction as usual.

Tokens used

Read from components/ui/input-otp.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.

Semantic color tokens used by this component
TokenUtilitiesSuperCoachCoach
--destructiveborder-destructive ring-destructive/20red-600red-600
--foregroundbg-foregroundneutral-950neutral-950
--inputborder-inputneutral-200neutral-200
--ringborder-ring ring-ring/50brand-600orange-500
  • Layerz-(--z-raised)
  • Opacityopacity-(--opacity-disabled)
  • Radiusrounded-s-md rounded-e-md
  • Shadowshadow-xs
  • Typetext-sm

Props

<InputOTP>

Also takes every prop of <OTPInput>

InputOTP props
PropTypeDefault
containerClassName?string—

<InputOTPGroup>

Also takes every prop of <div>

No props of its own.

<InputOTPSlot>

Also takes every prop of <div>

InputOTPSlot props
PropTypeDefault
indexnumber—

<InputOTPSeparator>

Also takes every prop of <div>

No props of its own.

Replaces in coach-admin

From the read-only audit of coach-admin: 6 style values, 7 uses.

The shared OtpField component, including its hint, error and resend styles. Its help text and error map to the muted-foreground and destructive-text tokens.

Classes and selectors

  • .otp-field__hint2
  • .otp-field__error1
  • .otp-field__resend1
  • .otp-field1

Files

  • src/reusableComponents/OtpField/OtpField.scss7

The inventory lists the first 12 locations of each value, so file counts are a lower bound.

SCSS variables in these styles

  • $otp-field-fontnot a color: spacing, size or layout
  • $invoice-ink-secondary→ --foreground-secondary
  • $danger-text-color→ --destructive-text
  • $spacing-xsnot a color: spacing, size or layout
  • $spacing-smnot a color: spacing, size or layout

Input, Label, Form.