Forms

Input OTP.

A row of single-character slots for pairing codes and one-time passwords, with paste support.

Run lantern pair on your computer to get a code.

Installation#

With the CLI

$npx lanterncn add input-otp

This also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/input-otp. See Installation if your project is not set up yet.

Manually

  1. Install the dependencies

    npm install input-otp lucide-react
  2. Copy the source into your project

    components/ui/input-otp.tsx
    "use client";
    
    import * as React from "react";
    import { OTPInput, OTPInputContext } from "input-otp";
    import { MinusIcon } from "lucide-react";
    
    import { cn } from "@/lib/utils";
    
    function InputOTP({
      className,
      containerClassName,
      ...props
    }: React.ComponentProps<typeof OTPInput> & { containerClassName?: string }) {
      return (
        <OTPInput
          data-slot="input-otp"
          containerClassName={cn("flex items-center gap-2 has-disabled:opacity-50", containerClassName)}
          className={cn("disabled:cursor-not-allowed", className)}
          {...props}
        />
      );
    }
    
    function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
      return <div data-slot="input-otp-group" className={cn("flex items-center", className)} {...props} />;
    }
    
    function InputOTPSlot({ index, className, ...props }: React.ComponentProps<"div"> & { index: number }) {
      const inputOTPContext = React.useContext(OTPInputContext);
      const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
    
      return (
        <div
          data-slot="input-otp-slot"
          data-active={isActive}
          className={cn(
            "relative flex h-11 w-10 items-center justify-center border-y border-r border-input bg-background/40 font-mono text-base text-foreground transition-[border-color,box-shadow] first:rounded-l-md first:border-l last:rounded-r-md",
            "data-[active=true]:z-10 data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-ring/25",
            "aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20",
            className,
          )}
          {...props}
        >
          {char}
          {hasFakeCaret && (
            <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
              <div className="h-5 w-2 animate-lantern-blink bg-primary" />
            </div>
          )}
        </div>
      );
    }
    
    function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
      return (
        <div data-slot="input-otp-separator" role="separator" className="text-muted-foreground" {...props}>
          <MinusIcon className="size-4" />
        </div>
      );
    }
    
    export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
  3. Update the import paths to match your project

    The source imports cn from @/lib/utils.

Usage#

import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/components/ui/input-otp";
<InputOTP maxLength={6}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Built on the input-otp package by Guilherme Rodz. Use the pattern prop to limit which characters are accepted.

Examples#

With separator

6 characters left