@akhatmirzo/ui

OtpInput

Enter a one-time code from an SMS — separate cells, paste, autofill and a resend timer.

+998 90 *** ** 67 ga yuborildi. Kod: 123456

Import

import { OtpInput, ResendTimer } from '@akhatmirzo/ui';

ResendTimer is also available as OtpInput.ResendTimer, so a separate import is optional.

When to use

  • Phone or email verification, two-factor sign-in — any 4–8 character code sent by SMS or an authenticator app.
  • Verify as soon as the code is complete: onComplete fires when the last character goes in, so you don't need a "Verify" button.
  • Explain errors in words: invalid turns the cells red and shakes them, but put the reason in the Field error prop ("Wrong code").
  • Offer a resend with a timer — ResendTimer asks users to wait for the SMS and keeps your server from being hammered.
  • Not for passwords or PINs you need to hide — the characters are shown in clear text.

Verification flow

In the example above the correct code is 123456. A wrong code switches on invalid: the cells turn red and the group shakes gently. Clear the error as soon as the user starts typing again, and disable the input once the code is accepted.

VerifyForm.tsx
import { Field, OtpInput } from '@akhatmirzo/ui';
import { useState } from 'react';

async function verifyCode(code: string) {
  const res = await fetch('/api/verify', { method: 'POST', body: JSON.stringify({ code }) });
  return res.ok;
}

export function VerifyForm() {
  const [code, setCode] = useState('');
  const [error, setError] = useState(false);

  return (
    <Field label="Verification code" error={error ? 'Wrong code' : undefined}>
      <OtpInput
        value={code}
        onValueChange={(v) => {
          setCode(v);
          setError(false);
        }}
        onComplete={async (v) => setError(!(await verifyCode(v)))}
      />
    </Field>
  );
}

Paste and SMS autofill

Users can paste the whole code (Ctrl+V) into any cell and every cell fills up. The first cell has autocomplete="one-time-code", so iOS and Android suggest the code from the SMS right above the keyboard.

Length, type and grouping

length sets the number of cells (default 6). type="alphanumeric" accepts letters as well (the default is digits only). groupSize splits the cells into groups: 3 → 123 – 456.

Qiymat: —

States

invalid adds a red border and a shake (no shake when prefers-reduced-motion is on). disabled turns the cells off.

ResendTimer

"Kodni qayta yuborish (0:59)" — the button can't be clicked until the timer runs out. If onResend returns a Promise, the button also stays blocked until it settles, then the timer starts over. The countdown starts as soon as the component mounts, assuming the code has just been sent.

Props

OtpInput

Accepts the remaining <div> attributes (className, aria-label, data-* ...) and ref (the root is the group of cells).

PropTypeDefaultDescription
lengthnumber6Number of cells
type'numeric' | 'alphanumeric''numeric'Digits only, or letters and digits
valuestring—The code (controlled)
defaultValuestring—
onValueChange(value: string) => void—
onComplete(code: string) => void—Called when the last character is entered
invalidboolean—Error: red + shake (automatic inside a Field with an error)
disabledboolean—
groupSizenumber—Grouping: 3 → 123 – 456
namestring—Name of the hidden form field
autoFocusboolean—Focus the first cell on mount
labelsOtpInputLabels—{ cell?: (index, length) => string; group?: string }

ResendTimer

Accepts the remaining <button> attributes.

PropTypeDefaultDescription
secondsnumber60Wait time in seconds
onResend() => unknown—Click handler; if it returns a Promise, the button waits for it
labels{ resend?: string }{ resend: 'Kodni qayta yuborish' }
disabledboolean—Blocks the button regardless of the timer

Data attributes

AttributeElementValue
data-invalidOtpInputpresent when invalid
data-disabledOtpInputpresent when disabled
data-completeOtpInputpresent when all cells are filled
data-stateResendTimerwaiting · ready

Keyboard

KeyAction
A characterFills the cell and moves to the next one
BackspaceDeletes the character; in an empty cell, goes back one
DeleteDeletes the character in the current cell
← →Move between cells
Home / EndFirst cell / first empty cell

You can't skip ahead past an empty cell — focus moves to the first empty one.

Accessibility

  • The group has role="group". Inside a Field it's named by the label (aria-labelledby); outside, it's called "Tasdiqlash kodi" ("verification code", change it with labels.group).
  • Each cell is its own <input> with a name: "1-raqam", "2-raqam" ... For another language, use labels.cell: cell: (i, n) => 'Digit ' + (i + 1) + ' of ' + n.
  • aria-invalid, aria-describedby and aria-required from the Field reach every cell, so the error text is read whichever cell has focus.
  • Mobile keyboard: the numeric type sets inputMode="numeric".
  • Motion: the shake is skipped with prefers-reduced-motion: reduce.
  • ResendTimer uses aria-disabled while waiting — it stays focusable but can't be activated; the countdown is part of the button's name.

On this page