Light

Input OTP

Accessible one-time password component with copy paste functionality.

Spec · from metadata

When to use

  • OTP / verification code entry
  • PIN code input
  • Any fixed-length numeric or alphanumeric code input

When not to use

  • General text input — use Input instead
  • Password fields — use Input with type='password'
  • Variable-length codes — use Input with a pattern

Anti-patterns

Avoid<InputOTP maxLength={6}>
  <InputOTPSlot index={0} />
  <InputOTPSlot index={1} />
</InputOTP>
Prefer<InputOTP maxLength={6}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
  </InputOTPGroup>
</InputOTP>

InputOTPSlot must be wrapped in InputOTPGroup for correct border styling.

Avoid<InputOTP maxLength={4}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
</InputOTP>
Prefer<InputOTP maxLength={3}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
</InputOTP>

maxLength must match the total number of InputOTPSlot components.

Accessibility

  • Min touch target36px
  • Screen readerBuilt on input-otp library which manages ARIA attributes internally. The separator has role='separator'.
  • ContrastActive slot uses border-ring for clear visual focus indication.

Token bindings

TokenCategoryUsage
input-bgcolorSlot background
inputcolorSlot border
ringcolorActive slot focus ring
destructivecolorError state
shadow-xsshadowSlot shadow

Import

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@timelycare/helix-ui"

Props

interface InputOTPProps {
  maxLength: number
  value?: string
  onChange?: (value: string) => void
  disabled?: boolean
  pattern?: string // REGEXP_ONLY_DIGITS
  className?: string
}

interface InputOTPSlotProps {
  index: number
  className?: string
}

Variants

VariantSlotsLayoutWidth
digits-only6[ ][ ][ ][ ][ ][ ]216px
with-separator6[ ][ ] — [ ][ ] — [ ][ ]296px
simple6[ ][ ][ ] — [ ][ ][ ]256px
with-spacing4[ ] [ ] [ ] [ ]168px

Choosing a Variant

  • digits-only — Standard verification codes (6 digits, no breaks)
  • with-separator — Grouped as XX-XX-XX with dashes
  • simple — Two groups XXX-XXX format
  • with-spacing — 4-digit PINs with visual spacing

Label Option

LabelDisplay
trueShows "Label" text above input
falseNo label, slots only

Slot States

StateBackgroundBorderRingContent
Defaultbg-input-bgborder-inputEmpty
Focusbg-input-bgborder-inputring-2 ring-ringCaret
Filledbg-input-bgborder-inputDigit (e.g., "1")
Errorbg-input-bgborder-destructiveEmpty/digit
Error (Focus)bg-input-bgborder-destructivering-2 ring-destructiveCaret

Slot Size: 36×36px


Common Patterns

Digits Only

<InputOTP maxLength={6}>
  <InputOTPGroup>
    {[0,1,2,3,4,5].map(i => <InputOTPSlot key={i} index={i} />)}
  </InputOTPGroup>
</InputOTP>

With Separator (XX-XX-XX)

<InputOTP maxLength={6}>
  <InputOTPGroup>
    <InputOTPSlot index={0} /><InputOTPSlot index={1} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={2} /><InputOTPSlot index={3} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={4} /><InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Simple (XXX-XXX)

<InputOTP maxLength={6}>
  <InputOTPGroup>
    <InputOTPSlot index={0} /><InputOTPSlot index={1} /><InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} /><InputOTPSlot index={4} /><InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

With Label

<div className="space-y-2">
  <Label>Label</Label>
  <InputOTP maxLength={6}>
    <InputOTPGroup>
      {[0,1,2,3,4,5].map(i => <InputOTPSlot key={i} index={i} />)}
    </InputOTPGroup>
  </InputOTP>
</div>

Accessibility

  • Built on input-otp library
  • Keyboard: digits auto-advance to next slot, Backspace to go back
  • Screen reader: slots announced with position context
  • Requires label association for form context
  • Tab moves between slot groups when using separators

Gotchas

ProblemSolution
Slots not groupedWrap related slots in InputOTPGroup
Separator missingAdd InputOTPSeparator between groups
Wrong maxLengthMatch maxLength to total slot count
Non-numeric inputAdd pattern={REGEXP_ONLY_DIGITS}

See Also


Last updated: February 9, 2026