Skip to Content
Hashti Design System
Components

Input OTP

Segmented numeric code input tailored for multi-factor authentication and SMS verification codes.

Input OTP

The InputOTP component provides a clean, segmented code input specifically tailored for multi-factor authentication (MFA), two-step verification, SMS passcodes, and transaction authorization PINs.

Rather than forcing users into an awkward monolithic text box, InputOTP distributes characters across distinct tactile visual slots with automated focus shifting, backspace retreat, paste interception, and immediate onComplete triggers.

Input OTPSegmented verification code input with auto-advance.
Entered: ------

Import

import { InputOTP } from "@hashti/react";

Examples

6-Digit Authentication Code

The default configuration renders 6 distinct digit slots suitable for standard authenticator app (TOTP) codes:

import { useState } from "react";
import { InputOTP } from "@hashti/react";

export function SixDigitOTPDemo() {
  const [code, setCode] = useState("");

  return (
    <div className="flex flex-col gap-3 max-w-xs">
      <label className="text-sm font-medium">Authentication Code</label>
      <InputOTP
        length={6}
        value={code}
        onChange={setCode}
        onComplete={(fullCode) => {
          console.log("Submitting verification code:", fullCode);
        }}
      />
      <p className="text-xs text-gray-500">
        Enter the 6-digit code generated by your authenticator app.
      </p>
    </div>
  );
}

4-Digit Security PIN

Adjust the length property to render 4 slots for short transaction or access PINs:

import { useState } from "react";
import { InputOTP } from "@hashti/react";

export function FourDigitPINDemo() {
  const [pin, setPin] = useState("");

  return (
    <div className="flex flex-col gap-3 max-w-xs">
      <label className="text-sm font-medium">Vault Access PIN</label>
      <InputOTP length={4} value={pin} onChange={setPin} aria-label="4-digit security PIN" />
    </div>
  );
}

Automated Paste Handling

InputOTP intercepts clipboard paste events across any focused slot. When a user pastes a verification code (e.g., copied from an SMS or email notification), the component strips non-alphanumeric characters, distributes the digits across all slots in sequence, advances focus to the end, and fires onComplete:

import { useState } from "react";
import { InputOTP, Button } from "@hashti/react";

export function PasteDemo() {
  const [otp, setOtp] = useState("");
  const [isVerifying, setIsVerifying] = useState(false);

  const handleComplete = (completedOtp: string) => {
    setIsVerifying(true);
    setTimeout(() => {
      alert(`Verified code: ${completedOtp}`);
      setIsVerifying(false);
    }, 800);
  };

  return (
    <div className="flex flex-col gap-4 max-w-sm">
      <div>
        <label className="text-sm font-medium">SMS Verification</label>
        <p className="text-xs text-gray-500 mb-3">
          Paste any 6-digit code directly into the first box.
        </p>
        <InputOTP length={6} value={otp} onChange={setOtp} onComplete={handleComplete} />
      </div>

      <Button
        variant="primary"
        isPending={isVerifying}
        isDisabled={otp.length < 6}
        onPress={() => handleComplete(otp)}
      >
        Verify Code
      </Button>
    </div>
  );
}

Validation Error State

Set isInvalid to display error borders when an incorrect code has been submitted:

import { useState } from "react";
import { InputOTP, Button } from "@hashti/react";

export function InvalidOTPDemo() {
  const [code, setCode] = useState("123456");
  const [hasError, setHasError] = useState(true);

  return (
    <div className="flex flex-col gap-3 max-w-xs">
      <label className="text-sm font-medium">Two-Factor Code</label>
      <InputOTP
        length={6}
        value={code}
        onChange={(val) => {
          setCode(val);
          setHasError(false);
        }}
        isInvalid={hasError}
      />
      {hasError && (
        <span className="text-xs text-red-600 dark:text-red-400 font-medium">
          The code you entered is invalid or has expired. Please try again.
        </span>
      )}
      <Button variant="secondary" size="sm" onPress={() => setCode("")}>
        Clear & Resend
      </Button>
    </div>
  );
}

Disabled State

import { InputOTP } from "@hashti/react";

export function DisabledOTPDemo() {
  return (
    <InputOTP length={6} defaultValue="982341" isDisabled aria-label="Locked verification code" />
  );
}

Props Reference

PropTypeDefaultDescription
lengthnumber6Total number of individual code slots to display.
valuestring—The controlled string value across all slots.
defaultValuestring""The initial uncontrolled value.
onChange(value: string) => void—Callback fired whenever any digit is entered, deleted, or pasted.
onComplete(value: string) => void—Callback fired immediately when all slots have been filled.
isDisabledbooleanfalseDisables interaction with all slot inputs.
isInvalidbooleanfalseApplies error styling across the slot borders.
aria-labelstring"One-time code"Accessible name for the input group container.
classNamestring—Additional CSS classes for the container.

Accessibility

  • Group Container Semantics: The wrapper carries role="group" and an accessible label (aria-label="One-time code"), grouping all individual digits into a single cognitive unit for screen readers.
  • Per-Slot Announcements: Each underlying input is given an explicit positional label (e.g. aria-label="Digit 1 of 6"), enabling screen reader users to understand their current position within the sequence.
  • Smart Keyboard Shifting:
    • Entering a character automatically advances focus to the next slot.
    • Pressing Backspace clears the current slot; if already empty, it clears the preceding slot and moves focus back.
    • ← and → navigate between adjacent slots without altering text.
  • Mobile Autofill & Virtual Keyboards: Slots use inputMode="numeric" to summon the numeric keypad on touch devices, and autoComplete="one-time-code" to integrate with iOS and Android automated SMS code autofill features.

Keyboard Interactions

KeyAction
Digits (0 - 9)Fills focused slot and auto-advances to next slot.
BackspaceClears current slot and moves focus back to previous slot.
→ / ←Navigates horizontally between slot inputs.

WAI-ARIA & Data Attributes

AttributeTargetDescription
[data-focused]OTP SlotSet on active slot receiving input.
[data-filled]OTP SlotSet when slot has a digit.