codefast/ui

Command Palette

Search for a command to run...

Source
Form

Input OTP

One-time password input with slot groups and separator. Built on input-otp.

Examples

Digits only

Pass a pattern (REGEXP_ONLY_DIGITS) to reject non-matching keystrokes.

21 lines
import { Field, FieldLabel } from "@codefast/ui/field";
import { REGEXP_ONLY_DIGITS } from "@codefast/ui/input-otp";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/ui/input-otp";

export function InputOTPPattern() {
  return (
    <Field className="w-fit">
      <FieldLabel htmlFor="digits-only">Digits Only</FieldLabel>
      <InputOTP id="digits-only" maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
    </Field>
  );
}

Disabled

A non-interactive code input.

1
2
3
4
5
6
19 lines
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";

export function InputOTPDisabled() {
  return (
    <InputOTP id="disabled" maxLength={6} disabled value="123456">
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  );
}

Alphanumeric

Use REGEXP_ONLY_DIGITS_AND_CHARS to accept both letters and numbers.

20 lines
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "@codefast/ui/input-otp";
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";

export function InputOTPAlphanumeric() {
  return (
    <InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  );
}

Controlled

Use the value and onChange props to control the input value.

Enter your one-time password.
24 lines
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/ui/input-otp";
import * as React from "react";

export function InputOTPControlled() {
  const [value, setValue] = React.useState("");

  return (
    <div className="space-y-2">
      <InputOTP maxLength={6} value={value} onChange={(value) => setValue(value)}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
      <div className="text-center text-sm">
        {value === "" ? <>Enter your one-time password.</> : <>You entered: {value}</>}
      </div>
    </div>
  );
}

Form

Accessible one-time password component with copy-paste functionality.

Verify your login
Enter the verification code we sent to your email address: m@example.com.

I no longer have access to this email address.

Having trouble signing in? Contact support
58 lines
import { Button } from "@codefast/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@codefast/ui/card";
import { Field, FieldDescription, FieldLabel } from "@codefast/ui/field";
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";
import { RefreshCwIcon } from "lucide-react";

export function InputOTPForm() {
  return (
    <Card className="mx-auto max-w-md">
      <CardHeader>
        <CardTitle>Verify your login</CardTitle>
        <CardDescription>
          Enter the verification code we sent to your email address: <span className="font-medium">m@example.com</span>.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <Field>
          <div className="flex items-center justify-between">
            <FieldLabel htmlFor="otp-verification">Verification code</FieldLabel>
            <Button variant="outline" size="xs">
              <RefreshCwIcon />
              Resend Code
            </Button>
          </div>
          <InputOTP maxLength={6} id="otp-verification" required>
            <InputOTPGroup className="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
              <InputOTPSlot index={0} />
              <InputOTPSlot index={1} />
              <InputOTPSlot index={2} />
            </InputOTPGroup>
            <InputOTPSeparator className="mx-2" />
            <InputOTPGroup className="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
              <InputOTPSlot index={3} />
              <InputOTPSlot index={4} />
              <InputOTPSlot index={5} />
            </InputOTPGroup>
          </InputOTP>
          <FieldDescription>
            <a href="/">I no longer have access to this email address.</a>
          </FieldDescription>
        </Field>
      </CardContent>
      <CardFooter>
        <Field>
          <Button type="submit" className="w-full">
            Verify
          </Button>
          <div className="text-sm text-muted-foreground">
            Having trouble signing in?{" "}
            <a href="/" className="underline underline-offset-4 transition-colors hover:text-primary">
              Contact support
            </a>
          </div>
        </Field>
      </CardFooter>
    </Card>
  );
}

Four Digits

A common pattern for PIN codes. This uses the pattern={REGEXP_ONLY_DIGITS} prop.

15 lines
import { REGEXP_ONLY_DIGITS } from "@codefast/ui/input-otp";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/ui/input-otp";

export function InputOTPFourDigits() {
  return (
    <InputOTP maxLength={4} pattern={REGEXP_ONLY_DIGITS}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
        <InputOTPSlot index={3} />
      </InputOTPGroup>
    </InputOTP>
  );
}

Invalid

Use aria-invalid on the slots to show an error state.

0
0
0
0
0
0
25 lines
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";
import * as React from "react";

export function InputOTPInvalid() {
  const [value, setValue] = React.useState("000000");

  return (
    <InputOTP maxLength={6} value={value} onChange={setValue}>
      <InputOTPGroup>
        <InputOTPSlot index={0} aria-invalid />
        <InputOTPSlot index={1} aria-invalid />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={2} aria-invalid />
        <InputOTPSlot index={3} aria-invalid />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={4} aria-invalid />
        <InputOTPSlot index={5} aria-invalid />
      </InputOTPGroup>
    </InputOTP>
  );
}

RTL

Right-to-left layout support for languages such as Arabic and Hebrew.

Translations are AI-generated for demonstration and may be imperfect.

1
2
3
4
5
6
48 lines
import { Field, FieldLabel } from "@codefast/ui/field";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/ui/input-otp";
import { useState } from "react";

import type { Translations } from "#/features/components-catalog/components/detail/language";
import { useTranslation } from "#/features/components-catalog/components/detail/language-context";

const translations: Translations = {
  en: {
    dir: "ltr",
    values: {
      verificationCode: "Verification code",
    },
  },
  ar: {
    dir: "rtl",
    values: {
      verificationCode: "رمز التحقق",
    },
  },
  he: {
    dir: "rtl",
    values: {
      verificationCode: "קוד אימות",
    },
  },
};

export function InputOTPRtl() {
  const { dir, t } = useTranslation(translations, "ar");
  const [value, setValue] = useState("123456");

  return (
    <Field className="mx-auto max-w-xs">
      <FieldLabel htmlFor="input-otp-rtl">{t.verificationCode}</FieldLabel>
      <InputOTP dir={dir} id="input-otp-rtl" maxLength={6} onChange={setValue} value={value}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
    </Field>
  );
}

Separator

Use the <InputOTPSeparator /> component to add a separator between input groups.

22 lines
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";

export function InputOTPWithSeparator() {
  return (
    <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>
  );
}

Anatomy

How the parts nest — every slot the component exposes, in composition order.

InputOTP
└── InputOTPGroup
└── InputOTPSlot

Features

  • Renders a single hidden <input> under the hood — pasting a full code fills every slot at once, no per-slot paste handling needed.
  • Ships REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS, and REGEXP_ONLY_DIGITS_AND_CHARS (re-exported from input-otp) for the pattern prop.
  • onComplete fires once every slot is filled, separate from the per-keystroke onChange.
  • InputOTPSeparator drops a visual divider between groups (e.g. 3 + 3 for a 6-digit code) without affecting the value.

API reference

Props for each part of the component. All native element props are also forwarded.

InputOTP

One-time-code input built on input-otp.

maxLengthnumber

Total number of slots (required).

valuestring

The controlled value — the concatenated characters.

onChange(value: string) => void

Called with the concatenated characters when the value changes.

onComplete(value: string) => void

Fires when every slot is filled.

patternstring (RegExp source)

Restrict input, e.g. REGEXP_ONLY_DIGITS.

InputOTPSlot

A single character cell; reads its char/caret/active state from OTPInputContext by index.

indexnumber

The slot's position among all slots (required).

InputOTPSeparator

A decorative divider between InputOTPGroups (e.g. 3 + 3 for a 6-digit code).

classNamestring

No dedicated props — renders a fixed dash icon; accepts standard div attributes.

Accessibility

Built to be keyboard-navigable and screen-reader friendly out of the box.

KeyFunction
Arrow+LeftMoves to the previous slot.
Arrow+RightMoves to the next slot.
BackspaceClears the current slot and steps back.
  • Renders a single hidden input — paste fills every slot at once.
  • Set autoComplete="one-time-code" so mobile keyboards offer the SMS code.
  • Always pair with a visible instruction telling users where the code came from.

Guidelines

Conventions that keep usage consistent across an app.

Do

  • Match the slot count to the real code length.
  • Group long codes (3 + 3) with a separator for readability.

Don’t

  • Don’t use OTP slots for general text — use Input.
  • Don’t block paste; many users paste the code from another app.

Explore further

Ready to integrate?

Follow the Getting Started guide to install @codefast/ui, or browse the full component gallery.