OTP Input for React

One time passcode Input. Accessible & unstyled.

README

OTP Input for React


https://github.com/guilhermerodz/input-otp/assets/10366880/753751f5-eda8-4145-a4b9-7ef51ca5e453

Usage


  1. ```bash
  2. npm install input-otp
  3. ```

Then import the component.

  1. ```diff
  2. +'use client'
  3. +import { OTPInput } from 'input-otp'

  4. function MyForm() {
  5.   return <form>
  6. +   <OTPInput maxLength={6} render={({slots})  => (...)} />
  7.   </form>
  8. }
  9. ```

Default example


The example below uses tailwindcss @shadcn/ui tailwind-merge clsx:

  1. ```tsx
  2. 'use client'
  3. import { OTPInput, SlotProps } from 'input-otp'
  4. <OTPInput
  5.   maxLength={6}
  6.   containerClassName="group flex items-center has-[:disabled]:opacity-30"
  7.   render={({ slots }) => (
  8.     <>
  9.       <div className="flex">
  10.         {slots.slice(0, 3).map((slot, idx) => (
  11.           <Slot key={idx} {...slot} />
  12.         ))}
  13.       </div>

  14.       <FakeDash />

  15.       <div className="flex">
  16.         {slots.slice(3).map((slot, idx) => (
  17.           <Slot key={idx} {...slot} />
  18.         ))}
  19.       </div>
  20.     </>
  21.   )}
  22. />

  23. // Feel free to copy. Uses @shadcn/ui tailwind colors.
  24. function Slot(props: SlotProps) {
  25.   return (
  26.     <div
  27.       className={cn(
  28.         'relative w-10 h-14 text-[2rem]',
  29.         'flex items-center justify-center',
  30.         'transition-all duration-300',
  31.         'border-border border-y border-r first:border-l first:rounded-l-md last:rounded-r-md',
  32.         'group-hover:border-accent-foreground/20 group-focus-within:border-accent-foreground/20',
  33.         'outline outline-0 outline-accent-foreground/20',
  34.         { 'outline-4 outline-accent-foreground': props.isActive },
  35.       )}
  36.     >
  37.       {props.char !== null && <div>{props.char}</div>}
  38.       {props.hasFakeCaret && <FakeCaret />}
  39.     </div>
  40.   )
  41. }

  42. // You can emulate a fake textbox caret!
  43. function FakeCaret() {
  44.   return (
  45.     <div className="absolute pointer-events-none inset-0 flex items-center justify-center animate-caret-blink">
  46.       <div className="w-px h-8 bg-white" />
  47.     </div>
  48.   )
  49. }

  50. // Inspired by Stripe's MFA input.
  51. function FakeDash() {
  52.   return (
  53.     <div className="flex w-10 justify-center items-center">
  54.       <div className="w-3 h-1 rounded-full bg-border" />
  55.     </div>
  56.   )
  57. }

  58. // tailwind.config.ts for the blinking caret animation.
  59. const config = {
  60.   theme: {
  61.     extend: {
  62.       keyframes: {
  63.         'caret-blink': {
  64.           '0%,70%,100%': { opacity: '1' },
  65.           '20%,50%': { opacity: '0' },
  66.         },
  67.       },
  68.       animation: {
  69.         'caret-blink': 'caret-blink 1.2s ease-out infinite',
  70.       },
  71.     },
  72.   },
  73. }

  74. // Small utility to merge class names.
  75. import { clsx } from 'clsx'
  76. import { twMerge } from 'tailwind-merge'

  77. import type { ClassValue } from 'clsx'

  78. export function cn(...inputs: ClassValue[]) {
  79.   return twMerge(clsx(inputs))
  80. }
  81. ```

How it works


There's currently no native OTP/2FA/MFA input in HTML, which means people are either going with 1. a simple input design or 2. custom designs like this one.
This library works by rendering an invisible input as a sibling of the slots, contained by a relatively positioned parent (the container root called _OTPInput_).