Input OTP (Lite)

A one-time password input with slots, without JavaScript.

Lite
import { InputOTPLite } from "@/components/ui/input-otp-lite"

export default function InputOTPLiteDemo() {
  return <InputOTPLite maxLength={6} name="code" aria-label="One-time code" />
}

About

Input OTP (Lite) is a hand-written alternative to shadcn/ui's Input OTP, which is built on the React library input-otp. It keeps upstream's look in every style: one native text input lies over the slot boxes, its characters spaced to one per box, so typing, pasting, one-time-code autofill and form validation come from the browser.

Installation

pnpm dlx shadcnui-hono-jsx@latest add input-otp-lite

Usage

import { InputOTPLite } from "@/components/ui/input-otp-lite"
<InputOTPLite maxLength={6} name="code" />

Unlike upstream, it is one component: maxLength sets the number of slots, and the input takes every attribute of a text input (name, pattern, required, value, disabled, aria-*).

Pattern

Use pattern and inputmode for what the code accepts. Characters line up with tabular digits; set a monospace font for letters.

import { InputOTPLite } from "@/components/ui/input-otp-lite"

export default function InputOTPLitePattern() {
  return (
    <InputOTPLite
      maxLength={6}
      pattern="[A-Za-z0-9]{6}"
      inputmode="text"
      class="font-mono uppercase"
      aria-label="Invite code"
    />
  )
}

Four Digits

import { InputOTPLite } from "@/components/ui/input-otp-lite"

export default function InputOTPLiteFourDigits() {
  return <InputOTPLite maxLength={4} pattern="[0-9]{4}" aria-label="PIN" />
}

Disabled

import { InputOTPLite } from "@/components/ui/input-otp-lite"

export default function InputOTPLiteDisabled() {
  return (
    <InputOTPLite maxLength={6} value="123456" disabled aria-label="Code" />
  )
}

Invalid

Set aria-invalid to show the invalid state.

import { InputOTPLite } from "@/components/ui/input-otp-lite"

export default function InputOTPLiteInvalid() {
  return (
    <InputOTPLite maxLength={6} value="000000" aria-invalid aria-label="Code" />
  )
}

Form

It submits with the form, and the browser validates required, minlength (set to maxLength) and pattern.

Enter the 6-digit code sent to your phone.

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { InputOTPLite } from "@/components/ui/input-otp-lite"

export default function InputOTPLiteForm() {
  return (
    <form class="w-full max-w-xs" method="post">
      <FieldGroup>
        <Field>
          <FieldLabel for="otp">One-Time Password</FieldLabel>
          <InputOTPLite
            id="otp"
            name="otp"
            maxLength={6}
            pattern="[0-9]{6}"
            required
          />
          <FieldDescription>
            Enter the 6-digit code sent to your phone.
          </FieldDescription>
        </Field>
        <Button type="submit" class="w-fit">
          Verify
        </Button>
      </FieldGroup>
    </form>
  )
}

Notes

  • A lite alternative to input-otp (not a port), with no JavaScript: one native text input over the slots, as in daisyUI, so typing, pasting, one-time-code autofill (autocomplete="one-time-code") and form validation (minlength, pattern, required) come from the browser.
  • One component with maxLength slots instead of InputOTPGroup, InputOTPSlot and InputOTPSeparator; the whole group is highlighted while focused (upstream highlights the active slot, with a blinking caret). Characters are aligned with tabular digits: set a monospace font (class="font-mono") for letters.
  • Accepts class instead of className.
  • Upstream documentation: shadcn/ui Input OTP. See also Differences from shadcn/ui.